melta-app 0.5.3 → 0.7.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 +493 -186
- package/docs/architecture.md +99 -0
- package/eslint-rules/melta.d.mts +44 -0
- package/lib/module/a11y/tap-target.js +41 -0
- package/lib/module/a11y/tap-target.js.map +1 -0
- package/lib/module/components/ActionSheet.js +20 -6
- package/lib/module/components/ActionSheet.js.map +1 -1
- package/lib/module/components/Alert.js +11 -7
- package/lib/module/components/Alert.js.map +1 -1
- package/lib/module/components/Avatar.js +7 -2
- package/lib/module/components/Avatar.js.map +1 -1
- package/lib/module/components/BottomSheet.js +11 -3
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/Card.js +4 -3
- package/lib/module/components/Card.js.map +1 -1
- package/lib/module/components/Checkbox.js +8 -5
- package/lib/module/components/Checkbox.js.map +1 -1
- package/lib/module/components/EmptyState.js +2 -1
- package/lib/module/components/EmptyState.js.map +1 -1
- package/lib/module/components/Header.js.map +1 -1
- package/lib/module/components/Image.js +5 -3
- package/lib/module/components/Image.js.map +1 -1
- package/lib/module/components/Modal.js +12 -4
- package/lib/module/components/Modal.js.map +1 -1
- package/lib/module/components/Progress.js +2 -1
- package/lib/module/components/Progress.js.map +1 -1
- package/lib/module/components/Radio.js +11 -3
- package/lib/module/components/Radio.js.map +1 -1
- package/lib/module/components/Screen.js +28 -3
- package/lib/module/components/Screen.js.map +1 -1
- package/lib/module/components/Skeleton.js +2 -1
- package/lib/module/components/Skeleton.js.map +1 -1
- package/lib/module/components/Surface.js +2 -1
- package/lib/module/components/Surface.js.map +1 -1
- package/lib/module/components/TextField.js +16 -4
- package/lib/module/components/TextField.js.map +1 -1
- package/lib/module/components/Toast.js +17 -9
- package/lib/module/components/Toast.js.map +1 -1
- package/lib/module/components/Toggle.js +13 -4
- package/lib/module/components/Toggle.js.map +1 -1
- package/lib/module/components/alert.styles.js +12 -26
- package/lib/module/components/alert.styles.js.map +1 -1
- package/lib/module/components/avatar.styles.js +24 -2
- package/lib/module/components/avatar.styles.js.map +1 -1
- package/lib/module/components/card.validate.js.map +1 -1
- package/lib/module/components/checkbox.styles.js +5 -2
- package/lib/module/components/checkbox.styles.js.map +1 -1
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/modal.styles.js +9 -0
- package/lib/module/components/modal.styles.js.map +1 -1
- package/lib/module/components/radio.styles.js +11 -2
- package/lib/module/components/radio.styles.js.map +1 -1
- package/lib/module/components/safe-area-registry.js +64 -13
- package/lib/module/components/safe-area-registry.js.map +1 -1
- package/lib/module/components/status-colors.js +53 -0
- package/lib/module/components/status-colors.js.map +1 -0
- package/lib/module/components/textfield.styles.js +15 -8
- package/lib/module/components/textfield.styles.js.map +1 -1
- package/lib/module/components/toast.styles.js +36 -25
- package/lib/module/components/toast.styles.js.map +1 -1
- package/lib/module/components/toggle.styles.js +20 -0
- package/lib/module/components/toggle.styles.js.map +1 -1
- package/lib/module/icons/Icon.js +4 -1
- package/lib/module/icons/Icon.js.map +1 -1
- package/lib/module/icons/icon.styles.js +32 -1
- package/lib/module/icons/icon.styles.js.map +1 -1
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/Button.js +23 -11
- package/lib/module/primitives/Button.js.map +1 -1
- package/lib/module/primitives/Metric.js +2 -1
- package/lib/module/primitives/Metric.js.map +1 -1
- package/lib/module/primitives/Row.js.map +1 -1
- package/lib/module/primitives/Stack.js.map +1 -1
- package/lib/module/primitives/Tag.js +15 -7
- package/lib/module/primitives/Tag.js.map +1 -1
- package/lib/module/primitives/Text.js +28 -7
- package/lib/module/primitives/Text.js.map +1 -1
- package/lib/module/primitives/_internal/focus-ring.js.map +1 -1
- package/lib/module/primitives/button.styles.js +58 -8
- package/lib/module/primitives/button.styles.js.map +1 -1
- package/lib/module/primitives/index.js.map +1 -1
- package/lib/module/primitives/metric.styles.js +4 -1
- package/lib/module/primitives/metric.styles.js.map +1 -1
- package/lib/module/primitives/tag.styles.js +42 -3
- package/lib/module/primitives/tag.styles.js.map +1 -1
- package/lib/module/primitives/text.styles.js +10 -5
- package/lib/module/primitives/text.styles.js.map +1 -1
- package/lib/module/safe-area/index.js +11 -5
- package/lib/module/safe-area/index.js.map +1 -1
- package/lib/module/theme/ThemeProvider.js +5 -2
- package/lib/module/theme/ThemeProvider.js.map +1 -1
- package/lib/module/theme/define-theme.js +27 -1
- package/lib/module/theme/define-theme.js.map +1 -1
- package/lib/module/theme/index.js +7 -8
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/letter-spacing.js +20 -0
- package/lib/module/theme/letter-spacing.js.map +1 -0
- package/lib/module/theme/line-height.js +52 -0
- package/lib/module/theme/line-height.js.map +1 -0
- package/lib/module/theme/native-theme.js +8 -7
- package/lib/module/theme/native-theme.js.map +1 -1
- package/lib/typescript/src/a11y/tap-target.d.ts +34 -0
- package/lib/typescript/src/a11y/tap-target.d.ts.map +1 -0
- package/lib/typescript/src/components/ActionSheet.d.ts +8 -1
- package/lib/typescript/src/components/ActionSheet.d.ts.map +1 -1
- package/lib/typescript/src/components/Alert.d.ts +9 -2
- package/lib/typescript/src/components/Alert.d.ts.map +1 -1
- package/lib/typescript/src/components/Avatar.d.ts +1 -1
- package/lib/typescript/src/components/Avatar.d.ts.map +1 -1
- package/lib/typescript/src/components/BottomSheet.d.ts +4 -1
- package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/components/Card.d.ts +2 -2
- package/lib/typescript/src/components/Checkbox.d.ts +3 -3
- package/lib/typescript/src/components/Checkbox.d.ts.map +1 -1
- package/lib/typescript/src/components/Image.d.ts +4 -3
- package/lib/typescript/src/components/Image.d.ts.map +1 -1
- package/lib/typescript/src/components/Modal.d.ts +9 -3
- package/lib/typescript/src/components/Modal.d.ts.map +1 -1
- package/lib/typescript/src/components/Progress.d.ts +1 -1
- package/lib/typescript/src/components/Progress.d.ts.map +1 -1
- package/lib/typescript/src/components/Radio.d.ts +3 -2
- package/lib/typescript/src/components/Radio.d.ts.map +1 -1
- package/lib/typescript/src/components/Screen.d.ts +28 -5
- package/lib/typescript/src/components/Screen.d.ts.map +1 -1
- package/lib/typescript/src/components/Skeleton.d.ts +1 -1
- package/lib/typescript/src/components/Skeleton.d.ts.map +1 -1
- package/lib/typescript/src/components/Surface.d.ts +1 -1
- package/lib/typescript/src/components/Surface.d.ts.map +1 -1
- package/lib/typescript/src/components/TextField.d.ts +11 -4
- package/lib/typescript/src/components/TextField.d.ts.map +1 -1
- package/lib/typescript/src/components/Toast.d.ts +10 -3
- package/lib/typescript/src/components/Toast.d.ts.map +1 -1
- package/lib/typescript/src/components/Toggle.d.ts +3 -2
- package/lib/typescript/src/components/Toggle.d.ts.map +1 -1
- package/lib/typescript/src/components/action-sheet.styles.d.ts +1 -1
- package/lib/typescript/src/components/action-sheet.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/alert.styles.d.ts +14 -14
- package/lib/typescript/src/components/alert.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/avatar.styles.d.ts +36 -1
- package/lib/typescript/src/components/avatar.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet.styles.d.ts +1 -1
- package/lib/typescript/src/components/bottom-sheet.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/card.styles.d.ts +1 -1
- package/lib/typescript/src/components/card.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/card.validate.d.ts +1 -1
- package/lib/typescript/src/components/card.validate.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox.styles.d.ts +6 -3
- package/lib/typescript/src/components/checkbox.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state.styles.d.ts +1 -1
- package/lib/typescript/src/components/empty-state.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/header.styles.d.ts +1 -1
- package/lib/typescript/src/components/header.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/image.styles.d.ts +1 -1
- package/lib/typescript/src/components/image.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/index.d.ts +18 -18
- package/lib/typescript/src/components/index.d.ts.map +1 -1
- package/lib/typescript/src/components/modal.styles.d.ts +13 -2
- package/lib/typescript/src/components/modal.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/progress.styles.d.ts +1 -1
- package/lib/typescript/src/components/progress.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/radio.styles.d.ts +10 -2
- package/lib/typescript/src/components/radio.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/safe-area-registry.d.ts +47 -11
- package/lib/typescript/src/components/safe-area-registry.d.ts.map +1 -1
- package/lib/typescript/src/components/screen.styles.d.ts +1 -1
- package/lib/typescript/src/components/screen.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/skeleton.styles.d.ts +1 -1
- package/lib/typescript/src/components/skeleton.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/status-colors.d.ts +33 -0
- package/lib/typescript/src/components/status-colors.d.ts.map +1 -0
- package/lib/typescript/src/components/surface.styles.d.ts +1 -1
- package/lib/typescript/src/components/surface.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/textfield.styles.d.ts +9 -5
- package/lib/typescript/src/components/textfield.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/toast.styles.d.ts +34 -14
- package/lib/typescript/src/components/toast.styles.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle.styles.d.ts +17 -1
- package/lib/typescript/src/components/toggle.styles.d.ts.map +1 -1
- package/lib/typescript/src/icons/Icon.d.ts +12 -6
- package/lib/typescript/src/icons/Icon.d.ts.map +1 -1
- package/lib/typescript/src/icons/icon.styles.d.ts +14 -2
- package/lib/typescript/src/icons/icon.styles.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +3 -3
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Button.d.ts +8 -4
- package/lib/typescript/src/primitives/Button.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Metric.d.ts +1 -1
- package/lib/typescript/src/primitives/Metric.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Row.d.ts +2 -2
- package/lib/typescript/src/primitives/Row.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Stack.d.ts +2 -2
- package/lib/typescript/src/primitives/Stack.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Tag.d.ts +2 -2
- package/lib/typescript/src/primitives/Tag.d.ts.map +1 -1
- package/lib/typescript/src/primitives/Text.d.ts +20 -6
- package/lib/typescript/src/primitives/Text.d.ts.map +1 -1
- package/lib/typescript/src/primitives/button.styles.d.ts +44 -7
- package/lib/typescript/src/primitives/button.styles.d.ts.map +1 -1
- package/lib/typescript/src/primitives/index.d.ts +6 -6
- package/lib/typescript/src/primitives/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/metric.styles.d.ts +1 -1
- package/lib/typescript/src/primitives/metric.styles.d.ts.map +1 -1
- package/lib/typescript/src/primitives/row.styles.d.ts +1 -1
- package/lib/typescript/src/primitives/row.styles.d.ts.map +1 -1
- package/lib/typescript/src/primitives/stack.styles.d.ts +1 -1
- package/lib/typescript/src/primitives/stack.styles.d.ts.map +1 -1
- package/lib/typescript/src/primitives/tag.styles.d.ts +34 -1
- package/lib/typescript/src/primitives/tag.styles.d.ts.map +1 -1
- package/lib/typescript/src/primitives/text.styles.d.ts +2 -4
- package/lib/typescript/src/primitives/text.styles.d.ts.map +1 -1
- package/lib/typescript/src/safe-area/index.d.ts +19 -5
- package/lib/typescript/src/safe-area/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/ThemeProvider.d.ts +2 -2
- package/lib/typescript/src/theme/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/theme/define-theme.d.ts +15 -2
- package/lib/typescript/src/theme/define-theme.d.ts.map +1 -1
- package/lib/typescript/src/theme/index.d.ts +14 -13
- package/lib/typescript/src/theme/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/letter-spacing.d.ts +15 -0
- package/lib/typescript/src/theme/letter-spacing.d.ts.map +1 -0
- package/lib/typescript/src/theme/line-height.d.ts +44 -0
- package/lib/typescript/src/theme/line-height.d.ts.map +1 -0
- package/lib/typescript/src/theme/native-theme.d.ts +1 -1
- package/lib/typescript/src/theme/native-theme.d.ts.map +1 -1
- package/lib/typescript/src/theme/types.d.ts +10 -1
- package/lib/typescript/src/theme/types.d.ts.map +1 -1
- package/llms.txt +2 -1
- package/package.json +22 -7
- package/src/a11y/tap-target.ts +38 -0
- package/src/components/ActionSheet.tsx +25 -15
- package/src/components/Alert.tsx +37 -10
- package/src/components/Avatar.tsx +12 -4
- package/src/components/BottomSheet.tsx +16 -13
- package/src/components/Card.tsx +9 -8
- package/src/components/Checkbox.tsx +12 -9
- package/src/components/EmptyState.tsx +7 -6
- package/src/components/Header.tsx +4 -4
- package/src/components/Image.tsx +9 -7
- package/src/components/Modal.tsx +22 -6
- package/src/components/Progress.tsx +5 -4
- package/src/components/Radio.tsx +11 -6
- package/src/components/Screen.tsx +66 -8
- package/src/components/Skeleton.tsx +6 -5
- package/src/components/Surface.tsx +5 -4
- package/src/components/TextField.tsx +25 -7
- package/src/components/Toast.tsx +33 -11
- package/src/components/Toggle.tsx +15 -6
- package/src/components/action-sheet.styles.ts +1 -1
- package/src/components/alert.styles.ts +13 -25
- package/src/components/avatar.styles.ts +25 -3
- package/src/components/bottom-sheet.styles.ts +1 -1
- package/src/components/card.styles.ts +1 -1
- package/src/components/card.validate.ts +2 -2
- package/src/components/checkbox.styles.ts +6 -3
- package/src/components/empty-state.styles.ts +1 -1
- package/src/components/header.styles.ts +1 -1
- package/src/components/image.styles.ts +1 -1
- package/src/components/index.ts +18 -18
- package/src/components/modal.styles.ts +11 -2
- package/src/components/progress.styles.ts +1 -1
- package/src/components/radio.styles.ts +11 -2
- package/src/components/safe-area-registry.ts +81 -14
- package/src/components/screen.styles.ts +1 -1
- package/src/components/skeleton.styles.ts +1 -1
- package/src/components/status-colors.ts +59 -0
- package/src/components/surface.styles.ts +1 -1
- package/src/components/textfield.styles.ts +18 -15
- package/src/components/toast.styles.ts +32 -25
- package/src/components/toggle.styles.ts +21 -1
- package/src/icons/Icon.tsx +13 -8
- package/src/icons/icon.styles.ts +38 -3
- package/src/icons/index.ts +3 -3
- package/src/index.ts +4 -4
- package/src/primitives/Button.tsx +28 -14
- package/src/primitives/Metric.tsx +5 -4
- package/src/primitives/Row.tsx +4 -4
- package/src/primitives/Stack.tsx +4 -4
- package/src/primitives/Tag.tsx +29 -11
- package/src/primitives/Text.tsx +42 -9
- package/src/primitives/_internal/focus-ring.tsx +1 -1
- package/src/primitives/button.styles.ts +63 -10
- package/src/primitives/index.ts +6 -6
- package/src/primitives/metric.styles.ts +9 -2
- package/src/primitives/row.styles.ts +1 -1
- package/src/primitives/stack.styles.ts +1 -1
- package/src/primitives/tag.styles.ts +43 -4
- package/src/primitives/text.styles.ts +10 -6
- package/src/safe-area/index.ts +21 -7
- package/src/theme/ThemeProvider.tsx +8 -5
- package/src/theme/define-theme.ts +33 -2
- package/src/theme/index.ts +18 -16
- package/src/theme/letter-spacing.ts +17 -0
- package/src/theme/line-height.ts +53 -0
- package/src/theme/native-theme.ts +9 -8
- package/src/theme/types.ts +10 -1
package/README.md
CHANGED
|
@@ -1,23 +1,50 @@
|
|
|
1
1
|
# melta for APP(melta-app)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/tsubotax/melta-app/actions/workflows/check.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/melta-app)
|
|
5
|
+
|
|
6
|
+
**web と同じデザイン契約を React Native に降ろす UI kit。使う側のコードにも lint 強制層を npm で配る。**
|
|
7
|
+
アプリ本体ではなく `npm install melta-app` で使うライブラリ。
|
|
8
|
+
|
|
9
|
+
- **契約は共有** — web 版 [melta-ui](https://github.com/tsubotax/melta-ui) と同じ `melta-contracts`(JSON)が tokens / 禁止ルール / component 契約の single source of truth
|
|
10
|
+
- **RN コンポーネント** — その契約を満たす React Native(Expo)実装。[対応表](#コンポーネント)は契約から生成する
|
|
11
|
+
- **利用側コードの lint 強制層** — [`melta-app/eslint-plugin`](#利用側コードの-lint-強制層melta-appeslint-plugin) を同梱。DS を**使う側**のプロジェクトで生値の直書きが lint で止まる
|
|
4
12
|
|
|
5
13
|
> 1 つのデザイン言語が、web では Tailwind に、app では React Native に降りる。
|
|
6
14
|
|
|
7
15
|
- **Showcase / Live Catalog**: https://app.melta.tsubotax.com (Live Catalog は実 RN コンポーネントの web export。HTML 再現デモではない)
|
|
8
16
|
- **Web 版 showcase**: https://melta.tsubotax.com
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
## Proof(機械で担保している範囲)
|
|
19
|
+
|
|
20
|
+
このリポジトリで検証できる主張には実行コマンドを付けてある。証拠として挙げたリポジトリ内の
|
|
21
|
+
パスとアンカーは drift 検査(`npm run check:drift`)が実在を照合するので、リンクが腐れば CI が落ちる。
|
|
22
|
+
※実導入検証(下記 2 項目め)だけは非公開プロジェクトでの**実測報告**で、読者が再実行できるのは
|
|
23
|
+
同じ欠陥を固定した E2E テストまで。
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
- **ドキュメントは契約からの生成物** — 下の[コンポーネント表](#コンポーネント)・[llms.txt](https://app.melta.tsubotax.com/llms.txt)・showcase の統計は `melta-contracts` から生成し、手書きの表はこのリポに存在しない。腐りは drift 検査が落とす([scripts/check-drift.ts](./scripts/check-drift.ts) / [CI](./.github/workflows/check.yml))
|
|
26
|
+
- **消費者プロジェクトでの実導入検証(2026-08-04)** — 別リポジトリの自プロジェクト(非公開 RN アプリ)に npm 経由で導入し、AI が違反コードを書いた直後に検出 → 修正フィードバック → 自己修正、のループを実測で確認。導入時に見つかった hook の欠陥(実行失敗時に無言で素通りする)は同日中に本体へ還元し、故障系を含む E2E 14 ケースで固定([scripts/lib/hook-lint.test.ts](./scripts/lib/hook-lint.test.ts) / [CHANGELOG 0.5.3](./CHANGELOG.md#053--2026-08-04))
|
|
27
|
+
- **「npm install すれば動く」の機械証明** — pack → tarball 実体検査 → [attw](https://github.com/arethetypeswrong/arethetypeswrong.github.io) で exports の型解決を resolution mode ごとに検査 → fixture へ install → 本体 / icons / safe-area / eslint-plugin の import + typecheck を **`moduleResolution` 3 種(bundler / node16 / nodenext)すべてで**実行 → lint plugin を実 import してルール 4 本と推奨 severity を照合、までを公開ゲートにしている([scripts/check-installability.sh](./scripts/check-installability.sh)。`npm run release` の必須ステップ)
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
15
30
|
|
|
16
31
|
```bash
|
|
17
32
|
npm install melta-app
|
|
18
33
|
```
|
|
19
34
|
|
|
20
|
-
peerDependencies は `react` / `react-native`
|
|
35
|
+
必須の peerDependencies は `react` / `react-native` の 2 つ(runtime 依存ゼロ)。機能別の optional peer が 2 つ — `react-native-svg`(`melta-app/icons` を使う場合のみ)と `react-native-safe-area-context`(`melta-app/safe-area` を使う場合のみ)。配布物は **ESM のみ**(`require()` では読めない。検証済みは Expo 56 / Metro — 他バンドラは ESM 対応が前提)。型は `moduleResolution` が `bundler` / `node16` / `nodenext` のいずれでも解決できることを CI で検査している(`skipLibCheck: true` の消費者でも型が欠落しない)。eslint plugin / hook などの Node ツール面は Node.js 22 で検証している(`engines` 宣言)。
|
|
36
|
+
|
|
37
|
+
**React Native バージョン対応**(下限 0.71 は `gap` / `role` 使用のため。型・Jest で検証済みなのは Expo 56 / RN 0.85 / React 19.2 の組のみ):
|
|
38
|
+
|
|
39
|
+
| RN | melta-app | optional peer の目安 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 0.85(検証済み) | ✅ | safe-area-context 5.x / svg 15.13+ |
|
|
42
|
+
| 0.74〜0.84 | 型上は互換(未検証) | safe-area-context 5.x / svg 15.8+ |
|
|
43
|
+
| 0.73 | 型上は互換(未検証) | safe-area-context 4.x / svg 15.8+ |
|
|
44
|
+
| 0.71〜0.72 | 型上は互換(未検証) | safe-area-context 4.x / svg 15.0〜15.7 |
|
|
45
|
+
| 〜0.70 | ❌(`gap` レイアウトが崩れる) | — |
|
|
46
|
+
|
|
47
|
+
> ⚠️ **0.x 系につき破壊的変更は minor で入る**(CHANGELOG 方針)。`^` 範囲でなく**バージョン固定か `~` 範囲**を推奨。
|
|
21
48
|
|
|
22
49
|
```tsx
|
|
23
50
|
import { ThemeProvider, Screen, Header, Card, Text, Button } from "melta-app";
|
|
@@ -36,142 +63,175 @@ export default function App() {
|
|
|
36
63
|
}
|
|
37
64
|
```
|
|
38
65
|
|
|
39
|
-
テーマは `ThemeProvider` が OS の light / dark に自動追従(`forcedMode` で固定も可)。トークンは `useTheme()` / `nativeTheme`
|
|
66
|
+
テーマは `ThemeProvider` が OS の light / dark に自動追従(`forcedMode` で固定も可)。トークンは `useTheme()` / `nativeTheme` から取れる。自分のブランドで塗り替えるなら[テーマを注入する](#テーマを注入するブランドトークン)。本体エントリ以外は subpath 3 つ([Icon](#iconmelta-appicons) / [SafeArea 差替](#safearea-の差し替えmelta-appsafe-area) / [lint plugin](#利用側コードの-lint-強制層melta-appeslint-plugin))。
|
|
40
67
|
|
|
41
|
-
###
|
|
68
|
+
### bare React Native(Expo を使わない場合)
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
melta-app 本体は JS のみで、ネイティブモジュールを含まない。ただし**optional peer の 2 つ
|
|
71
|
+
(`react-native-safe-area-context` / `react-native-svg`)は本物のネイティブモジュール**なので、
|
|
72
|
+
bare RN では npm install だけでは動かない。iOS は Pods の再インストールと、両 OS ともネイティブの
|
|
73
|
+
再ビルドが要る(JS の reload では反映されない):
|
|
44
74
|
|
|
45
|
-
```
|
|
46
|
-
|
|
75
|
+
```bash
|
|
76
|
+
# bare RN — 使う subpath に対応する peer だけ入れる
|
|
77
|
+
npm install react-native-svg react-native-safe-area-context
|
|
78
|
+
npx pod-install # iOS(= cd ios && pod install)
|
|
79
|
+
# その後 Xcode / Gradle でネイティブを**再ビルド**する(Metro の reload では足りない)
|
|
80
|
+
```
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
id: "acme",
|
|
51
|
-
color: { /* primary / body / semantic / status */ },
|
|
52
|
-
typography: { /* … */ },
|
|
53
|
-
spacing: { /* … */ }, radius: { /* … */ },
|
|
54
|
-
elevation: { /* … */ }, motion: { /* … */ }, zIndex: { /* … */ },
|
|
55
|
-
});
|
|
82
|
+
Expo(managed / prebuild)なら従来どおり `npx expo install` が RN 版に合う版を選び、
|
|
83
|
+
config plugin と prebuild が iOS/Android 側を面倒見る:
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
85
|
+
```bash
|
|
86
|
+
npx expo install react-native-svg react-native-safe-area-context
|
|
60
87
|
```
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
どちらの peer も使わない(`melta-app` 本体エントリだけ import する)なら、
|
|
90
|
+
melta-app の導入にネイティブ作業は発生しない。
|
|
63
91
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
id: "acme",
|
|
67
|
-
color: { /* … */ semantic: { dark: darkColors } }, // light は書かない
|
|
68
|
-
/* … */
|
|
69
|
-
});
|
|
70
|
-
```
|
|
92
|
+
> 検証済みの構成は **Expo 56 / RN 0.85 / React 19.2** の組だけ([制約と正直な範囲](#制約と正直な範囲))。
|
|
93
|
+
> bare RN 構成は型・依存関係の上では成立するが、実機での検証は行っていない。
|
|
71
94
|
|
|
72
|
-
|
|
73
|
-
- OS が light を返しても **dark で描画する**(警告は出さない。light を作らないのは設計判断で、OS 設定は事故ではない)
|
|
74
|
-
- `forcedMode="light"` のように**対応していない mode を明示指定**した場合も描画は止めず clamp するが、開発時は `console.error` で1回報告する。light/dark トグル UI を出すなら `capabilities.colorScheme` を見て出し分けること
|
|
75
|
-
- `theme.color.semantic.light` を直接読むと、原因を名指しするエラーで落ちる(値を捏造して静かに間違った色を返すことはしない)。現在 mode の色は `useTheme().colors` から取る
|
|
95
|
+
### dev 検証と `__DEV__`
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
melta の dev 専用チェックは `__DEV__`(Metro のグローバル)が真のときだけ動く:
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
- `defineTheme()` の `validateTheme` — 壊れた theme をその場で throw
|
|
100
|
+
- 解決済み theme の `deepFreeze` — 後からの書き換えを凍結して検出
|
|
101
|
+
- `ThemeProvider` の mode 違反レポート / `Card` の不正 prop 組み合わせの `console.error`
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
**Metro 以外のバンドラ(Vite / webpack での react-native-web など)は `__DEV__` を定義しない。**
|
|
104
|
+
その環境では `process.env.NODE_ENV === "development"` にフォールバックし、
|
|
105
|
+
どちらも判定できなければ dev 検証は**自動で無効**になる(本番に dev コストを漏らさない側に倒す設計)。
|
|
106
|
+
つまり「警告が一度も出ない = 違反が無い」ではない。有効にしたい場合はどちらかを与える:
|
|
84
107
|
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<Text variant="lg" role="heading">工房 #12</Text>
|
|
92
|
-
</Card>
|
|
108
|
+
```js
|
|
109
|
+
// Vite
|
|
110
|
+
export default { define: { __DEV__: JSON.stringify(true) } };
|
|
111
|
+
// webpack
|
|
112
|
+
new webpack.DefinePlugin({ __DEV__: JSON.stringify(true) });
|
|
113
|
+
// もしくは NODE_ENV=development でビルド / 起動する
|
|
93
114
|
```
|
|
94
115
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- カード内に操作要素が複数あってよい。`primaryAction` に置くのは**主アクション**(面を押したときの遷移先と一致するもの)
|
|
98
|
-
- 面に `accessibilityRole="button"` は付かない。`Button` を内包しても DOM が壊れない
|
|
116
|
+
production ビルドでは `__DEV__` を偽(または未定義)のままにする — dev 検証は本番で不要な
|
|
117
|
+
コストであり、Metro は偽に畳み込まれた分岐ごとバンドルから落とす。
|
|
99
118
|
|
|
100
|
-
|
|
119
|
+
## 利用側コードの lint 強制層(`melta-app/eslint-plugin`)
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
melta 契約からの逸脱を機械検知する eslint カスタムルール 4 本を同梱している。
|
|
122
|
+
消費者プロジェクトの flat config に組み込むと、生値の直書きが**使う側のコードで**止まる。
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
- **公開 DS の純度を守る**: 汎用 UI プリミティブだけを置く。アプリ固有の UI(特定サービスの画面・機能・語彙)は**各アプリ側**に置き、ここには混ぜない。
|
|
106
|
-
- **依存最小**: 初期は RN `StyleSheet` 固定(nativewind / unistyles 等の runtime styling lib を入れない)。Storybook RN も使わず自前カタログ。
|
|
124
|
+
### 動く完全例
|
|
107
125
|
|
|
108
|
-
|
|
126
|
+
melta の config は parser を持たない(**単独では TS/TSX が lint されない** — 後述)。
|
|
127
|
+
RN の base config と組み合わせた、コピペで動く最小構成が以下:
|
|
109
128
|
|
|
110
|
-
|
|
129
|
+
```bash
|
|
130
|
+
npm i -D eslint @react-native/eslint-config prettier
|
|
131
|
+
```
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
```js
|
|
134
|
+
// eslint.config.mjs
|
|
135
|
+
import rnConfig from "@react-native/eslint-config/flat";
|
|
136
|
+
import { meltaPlugin } from "melta-app/eslint-plugin";
|
|
137
|
+
|
|
138
|
+
export default [
|
|
139
|
+
// theme 定義ファイルはブランドの生値そのものなので lint 対象から外す
|
|
140
|
+
{ ignores: ["**/theme.ts"] },
|
|
116
141
|
|
|
117
|
-
|
|
142
|
+
// 1. RN 標準の base config。parser(TS/TSX)と React / RN ルールはこちらが持つ
|
|
143
|
+
...rnConfig,
|
|
118
144
|
|
|
145
|
+
// 2. melta の推奨 severity を追加する(この 1 行)。適用範囲だけ自分で決める
|
|
146
|
+
{ ...meltaPlugin.configs.recommended, files: ["**/*.{ts,tsx}"] },
|
|
147
|
+
];
|
|
119
148
|
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
types.ts NativeTheme の型
|
|
124
|
-
native-theme.ts ⚠️ 自動生成(melta-contracts → RN 正規化済み)
|
|
125
|
-
index.ts theme エントリ(ThemeProvider / useTheme / nativeTheme)
|
|
126
|
-
primitives/ Text / Button / Tag / Metric / Stack / Row
|
|
127
|
-
components/ Card / Image / Surface / Skeleton / EmptyState / Screen / Header /
|
|
128
|
-
Avatar / TextField / Toggle / Checkbox / Radio / Alert / Toast /
|
|
129
|
-
Progress / Modal / ActionSheet / BottomSheet
|
|
130
|
-
icons/ Icon + glyphs(subpath エントリ melta-app/icons、react-native-svg 隔離)
|
|
131
|
-
contracts/
|
|
132
|
-
contract-types.ts ⚠️ 自動生成(契約メタ CONTRACTS + 型)
|
|
133
|
-
index.ts 公開エントリ
|
|
134
|
-
example/ カタログアプリ(Expo、publish 除外)
|
|
135
|
-
App.tsx / catalog/ / screens/
|
|
136
|
-
scripts/
|
|
137
|
-
generate-native-theme.ts melta-contracts/tokens.json → src/theme/native-theme.ts
|
|
138
|
-
generate-contract-types.ts 契約 JSON → src/contracts/contract-types.ts
|
|
139
|
-
check-drift.ts README / catalog / allowlist の drift 検査(--write で heal)
|
|
140
|
-
check-installability.sh pack → install → import → typecheck の公開ゲート
|
|
141
|
-
lib/ token 正規化 + conformance / consumer テスト
|
|
149
|
+
|
|
150
|
+
**成功判定**: 生値を書いたファイルで `npx eslint .` を実行すると、4 ルールが名前付きで出る。
|
|
151
|
+
|
|
142
152
|
```
|
|
153
|
+
src/Bad.tsx
|
|
154
|
+
14:22 error 生の色 "#ff0000" は禁止。theme.color.* を使う。 melta/no-raw-color
|
|
155
|
+
15:5 error borderRadius の生数値は禁止。theme.radius.* を使う。 melta/no-raw-radius
|
|
156
|
+
16:5 warning spacing の生数値は theme.spacing.* 推奨。 melta/no-raw-spacing
|
|
157
|
+
19:5 warning fontSize の生数値は theme.typography.fontSize.* 推奨。 melta/no-raw-fontsize
|
|
143
158
|
|
|
144
|
-
|
|
159
|
+
✖ 4 problems (2 errors, 2 warnings)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`theme.*` 経由で書き直すと 0 件になる。ここまでが「利用側に強制層が入った」状態。
|
|
163
|
+
|
|
164
|
+
> この例は eslint 9.39 / `@react-native/eslint-config` 0.85.3 / melta-app 0.5.3 の
|
|
165
|
+
> 使い捨て fixture(tarball install)で実行して確認したもの。ルール 4 本の実在と severity は
|
|
166
|
+
> `check:installability` が毎リリース照合する。
|
|
145
167
|
|
|
146
|
-
`
|
|
168
|
+
### `configs.recommended` の中身
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
`configs.recommended`(flat config 形式)は plugin の登録とルール 4 本の severity を含む。
|
|
171
|
+
**severity の正本は plugin 側**なので、消費者がドキュメントから写して持つ必要はない。
|
|
172
|
+
|
|
173
|
+
| ルール | 既定 severity | 内容 |
|
|
149
174
|
|---|---|---|
|
|
150
|
-
| color |
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
| lineHeight | 比率("2.0") | `fontSize × ratio` を px 算出 |
|
|
155
|
-
| letterSpacing | em | **em ratio の数値で保持**(fontSize 相対のため token 単体で px 化不可。適用側が `fontSize × ratio` で解決) |
|
|
156
|
-
| motion duration | "150ms" | 150(数値) |
|
|
157
|
-
| motion easing | cubic-bezier | `[a,b,c,d]` tuple(`Easing.bezier(...)` に展開) |
|
|
175
|
+
| `melta/no-raw-color` | error | 生 hex/rgb/hsl → `theme.color.*` |
|
|
176
|
+
| `melta/no-raw-radius` | error | `borderRadius` の数値直書き → `theme.radius.*` |
|
|
177
|
+
| `melta/no-raw-spacing` | warn | `padding`/`margin`/`gap` の数値直書き(false positive あり) |
|
|
178
|
+
| `melta/no-raw-fontsize` | warn | `fontSize` の数値直書き |
|
|
158
179
|
|
|
159
|
-
|
|
180
|
+
⚠️ **base config は省略できない。** `meltaPlugin.configs.recommended` は parser も `files` も
|
|
181
|
+
持たない(適用範囲を消費者側に委ねる設計)。実測される挙動は 2 つ:
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
- **recommended だけを書いた場合** — `.ts` / `.tsx` はそもそも lint 対象に入らず、
|
|
184
|
+
`File ignored because no matching configuration was supplied` で**黙って素通りする**(eslint 9 / 10 で確認)
|
|
185
|
+
- **`files` だけ足して parser を足さない場合** — `Parsing error: Unexpected token` で落ちる
|
|
186
|
+
|
|
187
|
+
どちらも「強制層が入ったつもりで入っていない」状態なので、上の完全例のように
|
|
188
|
+
parser を持つ base config(`@react-native/eslint-config/flat` や typescript-eslint 等)の**上に追加**すること。
|
|
189
|
+
|
|
190
|
+
### カスタマイズ
|
|
163
191
|
|
|
164
|
-
|
|
165
|
-
# 未 install 時のみ隣の melta-ui を fallback で読む開発モード)
|
|
166
|
-
npm run generate
|
|
192
|
+
severity を変える・適用範囲を絞る場合は spread して上書きする:
|
|
167
193
|
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
```js
|
|
195
|
+
// eslint.config.mjs(カスタマイズする場合)
|
|
196
|
+
import { meltaPlugin } from "melta-app/eslint-plugin";
|
|
170
197
|
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
export default [
|
|
199
|
+
{
|
|
200
|
+
...meltaPlugin.configs.recommended,
|
|
201
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
202
|
+
rules: {
|
|
203
|
+
...meltaPlugin.configs.recommended.rules,
|
|
204
|
+
"melta/no-raw-spacing": "off", // 例: false positive が多い環境では落とす
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
plugin の登録とルール指定を最初から自分で書く形(従来の書き方)も動く:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
// eslint.config.mjs(すべて手書きする場合)
|
|
214
|
+
import { meltaPlugin } from "melta-app/eslint-plugin";
|
|
215
|
+
|
|
216
|
+
export default [
|
|
217
|
+
{
|
|
218
|
+
plugins: { melta: meltaPlugin },
|
|
219
|
+
rules: {
|
|
220
|
+
"melta/no-raw-color": "error", // 生 hex/rgb/hsl → theme.color.*
|
|
221
|
+
"melta/no-raw-radius": "error", // borderRadius 数値直書き → theme.radius.*
|
|
222
|
+
"melta/no-raw-spacing": "warn", // padding/margin/gap 数値直書き(false positive あり)
|
|
223
|
+
"melta/no-raw-fontsize": "warn", // fontSize 数値直書き
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
];
|
|
173
227
|
```
|
|
174
228
|
|
|
229
|
+
- 依存ゼロの自己完結 ESM(eslint 本体以外に何も要らない)。ESLint 9+ の flat config を想定(このリポでは 10.x で検証)
|
|
230
|
+
- `meltaPlugin` は **named export のみ**(default export なし)
|
|
231
|
+
- eslintrc(`.eslintrc.*`)からは登録できない(ESM の named export のみのため)。flat config への移行が要る
|
|
232
|
+
- 検知は AST の構文形状ベースの補助線。変数経由・spread は漏れるので、値の純度の本丸は
|
|
233
|
+
token 経由(`theme.*`)で書く習慣の側にある([制約](#制約と正直な範囲))
|
|
234
|
+
|
|
175
235
|
## コンポーネント
|
|
176
236
|
|
|
177
237
|
契約(melta-contracts)とコンポーネントの対応。この表は手書きせず契約から生成する(`npm run check:drift` が同期を検査)。
|
|
@@ -221,139 +281,386 @@ npm run ios # または android / web
|
|
|
221
281
|
| tooltip | — | 🚫 not-planned | — | hover 前提のため。iPhone HIG に tooltip 概念なし。必要になれば長押し Hint として別契約を切る |
|
|
222
282
|
<!-- END GENERATED: component-status -->
|
|
223
283
|
|
|
224
|
-
###
|
|
284
|
+
### 押せるカード(`variant="action"` / `"link"`)
|
|
285
|
+
|
|
286
|
+
**カード面そのものは操作要素ではない。** 面を押せるのはポインタ利用者向けの近道で、
|
|
287
|
+
キーボードとスクリーンリーダーからの到達手段は**カード内の操作要素**が担う。
|
|
288
|
+
そのため `primaryAction` が必須になっている。
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Card
|
|
292
|
+
variant="action"
|
|
293
|
+
onPress={openLog}
|
|
294
|
+
primaryAction={<Button label="製作ログを見る" onPress={openLog} />}
|
|
295
|
+
>
|
|
296
|
+
<Text variant="lg" role="heading">工房 #12</Text>
|
|
297
|
+
</Card>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- `primaryAction` は面の `onPress` と**同じ操作**を指す(別の遷移先にしない)
|
|
301
|
+
- ラベルは `primaryAction` 側に付ける。カードは名前を持たない(`role="article"` の領域)
|
|
302
|
+
- カード内に操作要素が複数あってよい。`primaryAction` に置くのは**主アクション**(面を押したときの遷移先と一致するもの)
|
|
303
|
+
- 面に `accessibilityRole="button"` は付かない。`Button` を内包しても DOM が壊れない
|
|
304
|
+
|
|
305
|
+
0.4.x からの移行は [CHANGELOG](./CHANGELOG.md#050--2026-07-29) を参照。
|
|
306
|
+
|
|
307
|
+
### 画面骨格(`Screen`)
|
|
308
|
+
|
|
309
|
+
`Screen` は safe-area + `bg-page` + content padding + ScrollView を 1 つにまとめた画面の器。
|
|
225
310
|
|
|
226
|
-
|
|
227
|
-
|
|
311
|
+
| prop | 型 | default | 意味 |
|
|
312
|
+
|---|---|---|---|
|
|
313
|
+
| `variant` | `"scroll"` \| `"fixed"` | `"scroll"` | content を ScrollView に載せるか、`flex: 1` の View に載せるか |
|
|
314
|
+
| `padding` | spacing token キー \| `"none"` | `"4"` | content の padding |
|
|
315
|
+
| `edges` | `readonly ("top" \| "right" \| "bottom" \| "left")[]` | [`enableSafeAreaContext`](#safearea-の差し替えmelta-appsafe-area) の既定 | safe-area を適用する辺 |
|
|
316
|
+
| `header` | `ReactNode` | — | scroll の外(safe-area 直下)に固定されるヘッダー slot |
|
|
317
|
+
| `scrollViewProps` | `Omit<ScrollViewProps, "children">` | — | 内部 ScrollView への passthrough(`variant="scroll"` のみ) |
|
|
318
|
+
| `scrollViewRef` | `Ref<ScrollView>` | — | 内部 ScrollView の ref(`scrollTo` / `scrollToEnd` 用) |
|
|
319
|
+
| `style` | `StyleProp<ViewStyle>` | — | safe-area の器に重ねる style |
|
|
320
|
+
| `testID` | `string` | — | — |
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
const listRef = useRef<ScrollView>(null);
|
|
324
|
+
|
|
325
|
+
<Screen
|
|
326
|
+
header={<Header title="フィード" />}
|
|
327
|
+
scrollViewProps={{ onScroll: handleScroll, keyboardShouldPersistTaps: "handled" }}
|
|
328
|
+
scrollViewRef={listRef}
|
|
329
|
+
>
|
|
330
|
+
<Text>本文</Text>
|
|
331
|
+
</Screen>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
契約(passthrough は「素通し」ではない):
|
|
335
|
+
|
|
336
|
+
- **`contentInsetAdjustmentBehavior` は渡さないこと。** safe-area は Screen が持つので二重 inset になる
|
|
337
|
+
- `contentContainerStyle` は DS の padding と**配列合成**される(渡した側が後勝ち)。padding を**消す**目的では使えない(消したいときは `padding="none"`)
|
|
338
|
+
- `scrollEventThrottle` は Screen 側の既定が `16`(iOS の既定 0 では `onScroll` が 1 ドラッグ 1 発しか来ずスクロール連動ヘッダが動かない)。消費者の指定で上書きできる
|
|
339
|
+
- `variant="fixed"` に `scrollViewProps` / `scrollViewRef` を渡すと dev ビルドで警告する(内部 ScrollView が無いので黙って捨てられる)
|
|
340
|
+
|
|
341
|
+
タブバーと併用する画面は、bottom をタブバー側に任せる:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// タブ配下の画面だけ edge を絞る(アプリ全体を top-only にしない。下記 SafeArea 節を参照)
|
|
345
|
+
<Screen edges={["top"]}>…</Screen>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## アクセシビリティの既定
|
|
349
|
+
|
|
350
|
+
3 点(タップ標的 / OS の文字サイズ拡大 / 読み上げラベル)は**利用側で何もしなくても既定で満たす**ように組んである。
|
|
351
|
+
|
|
352
|
+
### タップ標的 44pt
|
|
353
|
+
|
|
354
|
+
すべての操作要素は**視覚寸法を変えないまま**実効タップ標的 44pt を満たす。
|
|
355
|
+
契約側の正本は melta-contracts の `A11Y_MIN_TAP_TARGET_44`、実装との照合は
|
|
356
|
+
[scripts/lib/tap-target-conformance.test.ts](./scripts/lib/tap-target-conformance.test.ts)(CI で毎回検査)。
|
|
357
|
+
|
|
358
|
+
| 要素 | 視覚寸法 | 手当て | 実効 |
|
|
359
|
+
|---|---|---|---|
|
|
360
|
+
| `Button` small / medium / large(labeled) | minHeight 32 / 40 / 48 | 縦 hitSlop 6 / 2 / 0 | 44 / 44 / 48 |
|
|
361
|
+
| `Button`(iconOnly) | 32 / 40 / 48 の正方形 | 四方 hitSlop 6 / 2 / 0 | 同上(縦横とも) |
|
|
362
|
+
| `Tag variant="filter-chip"` | 高さ 34(padding 駆動) | 縦 hitSlop 5 | 44 |
|
|
363
|
+
| `Tag variant="removable"` の × / `Toast` の × / `Alert` の × / `Modal` の × | 24 の箱 | hitSlop 10 | 44 |
|
|
364
|
+
| `Radio` の option 行 | 行の高さ 36 | `minHeight: 44` | 44 |
|
|
365
|
+
| `Toggle` medium / large | track 24 / 28 | 縦 hitSlop 10 / 8 | 44 |
|
|
366
|
+
| `Checkbox` の行 | box 20(行は label 込み) | `minHeight: 44` | 44 |
|
|
367
|
+
|
|
368
|
+
- **背景を持つ要素は hitSlop、持たない要素は `minHeight`**。背景がある要素を minHeight で伸ばすと見た目が変わるため
|
|
369
|
+
- **横方向の hitSlop は隣接する操作要素との gap の 1/2 まで**。超えると当たり判定が重なって押し違いが起きる
|
|
370
|
+
(`Toast` の action と × はこれで実際に 8pt 重なっていた。0.7.0 で修正)
|
|
371
|
+
- ⚠️ **iconOnly の `Button` だけは横にも hitSlop が付く**(幅 32/40 では 44pt に届かないため)。
|
|
372
|
+
iconOnly を横に並べるときは **gap を hitSlop の 2 倍以上**(small: 12 / medium: 4)取ること。
|
|
373
|
+
gap 0 で密着させると当たり判定が重なる
|
|
374
|
+
|
|
375
|
+
### OS の文字サイズ拡大(fontScale)
|
|
376
|
+
|
|
377
|
+
- `Button` / `TextField` の高さは **`minHeight`**(`height` 固定ではない)。文字が拡大しても縦にクリップしない
|
|
378
|
+
- `Text` は `allowFontScaling` / `maxFontSizeMultiplier` を RN `Text` へ透過する。**既定は未指定 = RN 既定**(拡大に追随)。
|
|
379
|
+
固定寸法の図版ラベルなど、拡大するとレイアウトが壊れる箇所だけ opt-in で絞る
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
<Text maxFontSizeMultiplier={1.3}>グラフの軸ラベル</Text>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- `Avatar` の initials は器(円)が伸びないため、size 別に上限を掛けている(small 1.6 / medium 1.5 / large 1.3)。
|
|
386
|
+
拡大は許しつつ円からの溢れだけを止める設定で、利用側の指定は不要
|
|
387
|
+
|
|
388
|
+
### 読み上げラベルの差し替え(i18n)
|
|
389
|
+
|
|
390
|
+
内蔵の日本語ラベルは props で差し替えられる。**既定値は日本語のまま**(変えると既存アプリの VoiceOver 読み上げが変わるため)。
|
|
391
|
+
|
|
392
|
+
| コンポーネント | prop | 既定 |
|
|
393
|
+
|---|---|---|
|
|
394
|
+
| `Toast` / `Alert` / `Modal` | `closeAccessibilityLabel` | `"閉じる"` |
|
|
395
|
+
| `TextField` | `formatErrorAccessibilityLabel: (label, errorText) => string` | `` `${label}。エラー: ${errorText}` `` |
|
|
396
|
+
| `ActionSheet` | `cancelLabel` | `"キャンセル"` |
|
|
397
|
+
| `Skeleton` | `accessibilityLabel` | `"読み込み中"` |
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
<Modal title="Delete route" closeAccessibilityLabel="Close" onClose={close} visible>…</Modal>
|
|
401
|
+
|
|
402
|
+
<TextField
|
|
403
|
+
label="Email"
|
|
404
|
+
value={email}
|
|
405
|
+
variant="error"
|
|
406
|
+
errorText="Invalid format"
|
|
407
|
+
formatErrorAccessibilityLabel={(label, error) => `${label}. Error: ${error}`}
|
|
408
|
+
/>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## テーマを注入する(ブランドトークン)
|
|
412
|
+
|
|
413
|
+
`theme` を渡すと自分のブランドトークンで塗り替わる。未指定なら melta 既定(`nativeTheme`)。
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import { defineTheme, ThemeProvider } from "melta-app";
|
|
417
|
+
|
|
418
|
+
// module スコープで1回だけ組み立てる(render 中に作ると毎レンダー参照が変わり全体が再レンダーする)
|
|
419
|
+
const theme = defineTheme({
|
|
420
|
+
id: "acme",
|
|
421
|
+
color: { /* primary / body / semantic / status */ },
|
|
422
|
+
typography: { /* … */ },
|
|
423
|
+
spacing: { /* … */ }, radius: { /* … */ },
|
|
424
|
+
elevation: { /* … */ }, motion: { /* … */ }, zIndex: { /* … */ },
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
export default function App() {
|
|
428
|
+
return <ThemeProvider theme={theme}>{/* … */}</ThemeProvider>;
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
省略した欄は補完されない(`defineTheme` は dev で欠けたキーを名指しして throw する)。
|
|
433
|
+
下は**全欄を埋めたコピペ可能な最小完全例** — そのまま `theme.ts` に置けば型が通る。
|
|
434
|
+
|
|
435
|
+
<details>
|
|
436
|
+
<summary><code>theme.ts</code>(全フィールドを埋めた完全例)</summary>
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
import { defineTheme, type SemanticColors } from "melta-app";
|
|
440
|
+
|
|
441
|
+
const light: SemanticColors = {
|
|
442
|
+
"bg-page": "#faf7f2", "bg-page-alt": "#f2ece2", "bg-surface": "#ffffff",
|
|
443
|
+
"bg-surface-alt": "#faf7f2", "text-heading": "#1a1410", "text-default": "#3c332b",
|
|
444
|
+
"text-muted": "#7a6f63", "border-default": "#e5ddd1", "border-strong": "#cfc3b2",
|
|
445
|
+
"input-bg": "#ffffff", "input-border": "#cfc3b2", "text-accent": "#b4531f",
|
|
446
|
+
"text-on-accent": "#ffffff",
|
|
447
|
+
};
|
|
448
|
+
const dark: SemanticColors = {
|
|
449
|
+
"bg-page": "#14100c", "bg-page-alt": "#1f1913", "bg-surface": "#1f1913",
|
|
450
|
+
"bg-surface-alt": "#14100c", "text-heading": "#f5efe6", "text-default": "#ded4c6",
|
|
451
|
+
"text-muted": "#a29686", "border-default": "#33291f", "border-strong": "#4a3d2f",
|
|
452
|
+
"input-bg": "#14100c", "input-border": "#4a3d2f", "text-accent": "#e0803f",
|
|
453
|
+
"text-on-accent": "#14100c",
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
export const theme = defineTheme({
|
|
457
|
+
id: "acme",
|
|
458
|
+
color: {
|
|
459
|
+
primary: {
|
|
460
|
+
"50": "#fdf5ef", "100": "#f9e6d6", "200": "#f2c9a9", "300": "#e8a674",
|
|
461
|
+
"400": "#dd8446", "500": "#c96a2b", "600": "#b4531f", "700": "#93401a",
|
|
462
|
+
"800": "#73331a", "900": "#5a2a18", "950": "#31150b",
|
|
463
|
+
},
|
|
464
|
+
body: "#3c332b",
|
|
465
|
+
semantic: { light, dark },
|
|
466
|
+
status: {
|
|
467
|
+
success: { base: "#2f7d4f", subtleLight: "#eaf6ee", textLight: "#256540",
|
|
468
|
+
subtleDark: "rgba(47,125,79,0.16)", textDark: "#7fc79a" },
|
|
469
|
+
warning: { base: "#a8720d", subtleLight: "#fdf3e0", textLight: "#8a5d0a",
|
|
470
|
+
subtleDark: "rgba(168,114,13,0.16)", textDark: "#e0b054" },
|
|
471
|
+
danger: { base: "#c0362c", subtleLight: "#fbeceb", textLight: "#9d2c24",
|
|
472
|
+
subtleDark: "rgba(192,54,44,0.16)", textDark: "#f08b83" },
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
typography: {
|
|
476
|
+
fontFamily: {}, // 未指定 = OS デフォルト(RN の fontFamily は string 一本。embed は expo-font 側の責務)
|
|
477
|
+
fontSize: {
|
|
478
|
+
// lineHeight は fontSize × minLineHeightRatio(既定 1.45)以上にする。未満を書いても
|
|
479
|
+
// 実行時に下限へクランプされ、宣言値と描画値がズレるだけ(機序は src/theme/line-height.ts)
|
|
480
|
+
xxs: { fontSize: 10, lineHeight: 15 }, xs: { fontSize: 12, lineHeight: 18 },
|
|
481
|
+
sm: { fontSize: 14, lineHeight: 21 }, base: { fontSize: 16, lineHeight: 26 },
|
|
482
|
+
lg: { fontSize: 18, lineHeight: 27 }, xl: { fontSize: 20, lineHeight: 29 },
|
|
483
|
+
"2xl": { fontSize: 24, lineHeight: 35 }, "3xl": { fontSize: 30, lineHeight: 44 },
|
|
484
|
+
},
|
|
485
|
+
fontWeight: { normal: "400", medium: "500", semibold: "600", bold: "700" },
|
|
486
|
+
letterSpacingRatio: { heading: -0.02, body: 0 }, // em 比率(px ではない)
|
|
487
|
+
// フォントを同梱するなら、そのフォントが要求する最小行間比を宣言する(例: LINE Seed JP = 1.61。
|
|
488
|
+
// 下回る lineHeight は RN Android で字形が欠けるため実行時に下限へクランプされる)。
|
|
489
|
+
// 未宣言は日本語安全側の 1.45(Android system の Noto Sans CJK JP 実測):
|
|
490
|
+
// minLineHeightRatio: 1.61,
|
|
491
|
+
},
|
|
492
|
+
spacing: { "1": 4, "2": 8, "3": 12, "4": 16, "5": 20, "6": 24, "8": 32, "10": 40, "12": 48, "14": 56, "16": 64 },
|
|
493
|
+
radius: { sm: 4, md: 8, lg: 16, full: 9999 },
|
|
494
|
+
elevation: {
|
|
495
|
+
none: { shadowColor: "#000000", shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0, shadowRadius: 0, elevation: 0 },
|
|
496
|
+
sm: { shadowColor: "#000000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 2 },
|
|
497
|
+
md: { shadowColor: "#000000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 6, elevation: 5 },
|
|
498
|
+
overlay: { shadowColor: "#000000", shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.1, shadowRadius: 25, elevation: 10 },
|
|
499
|
+
},
|
|
500
|
+
motion: {
|
|
501
|
+
duration: { fast: 150, normal: 200, slow: 300 },
|
|
502
|
+
easing: { default: [0.4, 0, 0.2, 1], in: [0.4, 0, 1, 1], out: [0, 0, 0.2, 1] },
|
|
503
|
+
},
|
|
504
|
+
zIndex: { base: 0, dropdown: 20, sticky: 30, overlay: 40, modal: 50 },
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
(tsc 6.0 / `strict: true` で型が通ることを fixture で確認済み。この theme 定義ファイル自体は
|
|
509
|
+
ブランドの生値を持つので、[lint の完全例](#動く完全例)のように `ignores` に入れる)
|
|
510
|
+
|
|
511
|
+
</details>
|
|
512
|
+
|
|
513
|
+
### 単一の配色しか持たないブランド
|
|
514
|
+
|
|
515
|
+
**単一の配色しか持たないブランド**(例: dark しか作らない)は、持っていない mode を **書かない**。
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
const theme = defineTheme({
|
|
519
|
+
id: "acme",
|
|
520
|
+
color: { /* … */ semantic: { dark: darkColors } }, // light は書かない
|
|
521
|
+
/* … */
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
- `useTheme().capabilities.colorScheme` が `"single-dark"` として導出される(宣言する欄は無い。`color.semantic` のキー集合がそのまま能力になる)
|
|
526
|
+
- OS が light を返しても **dark で描画する**(警告は出さない。light を作らないのは設計判断で、OS 設定は事故ではない)
|
|
527
|
+
- `forcedMode="light"` のように**対応していない mode を明示指定**した場合も描画は止めず clamp するが、開発時は `console.error` で1回報告する。light/dark トグル UI を出すなら `capabilities.colorScheme` を見て出し分けること
|
|
528
|
+
- `theme.color.semantic.light` を直接読むと、原因を名指しするエラーで落ちる(値を捏造して静かに間違った色を返すことはしない)。現在 mode の色は `useTheme().colors` から取る
|
|
529
|
+
|
|
530
|
+
> 現状 `color.primary` / `text-accent` / `elevation` / status の light 値は、単一 dark のテーマでも**必須のまま**。省略できるのは `color.semantic` の mode だけで、他の軸を「持たない」と宣言する仕組みは後続で入れる。
|
|
531
|
+
|
|
532
|
+
## サブパス(Icon / SafeArea)
|
|
533
|
+
|
|
534
|
+
subpath は Icon(`melta-app/icons`)/ SafeArea 差替(`melta-app/safe-area`)/
|
|
535
|
+
[lint plugin](#利用側コードの-lint-強制層melta-appeslint-plugin)(`melta-app/eslint-plugin`)の 3 つ。
|
|
536
|
+
|
|
537
|
+
### Icon(`melta-app/icons`)
|
|
538
|
+
|
|
539
|
+
Icon は唯一 `react-native-svg`(optional peerDependency)に依存するため、本体エントリから分離している。
|
|
228
540
|
本体 `melta-app` は依存ゼロのまま — Icon を使うアプリだけが以下を行う:
|
|
229
541
|
|
|
230
542
|
```bash
|
|
231
543
|
npx expo install react-native-svg
|
|
232
544
|
```
|
|
233
545
|
|
|
546
|
+
> bare RN(Expo なし)は `npm install` + `npx pod-install` + ネイティブ再ビルド。
|
|
547
|
+
> [bare React Native](#bare-react-nativeexpo-を使わない場合) を参照。
|
|
548
|
+
|
|
234
549
|
```tsx
|
|
235
550
|
import { Icon } from "melta-app/icons";
|
|
236
551
|
|
|
237
552
|
<Icon name="like-on" accessibilityLabel="いいね" /> // 意味を持つ icon は label 必須
|
|
238
553
|
<Icon name="close" size="sm" color="text-muted" /> // 省略時は装飾扱い(a11y ツリーから除外)
|
|
554
|
+
<Icon name="check" color="status-success" /> // status 色(Alert / Toast と同じ status token)
|
|
239
555
|
```
|
|
240
556
|
|
|
557
|
+
`color` は semantic token キーに加えて `"status-success"` / `"status-warning"` / `"status-error"` を受ける
|
|
558
|
+
(`theme.color.status.*.base` を引く。生 hex は受けない)。
|
|
559
|
+
`"status-info"` は**無い** — status token に info の実体が無いため(web 側と同じ割り切り)。
|
|
560
|
+
info 相当は `color="text-accent"` で表す。
|
|
561
|
+
|
|
241
562
|
グリフは Charcoal Icons(pixiv、Apache-2.0)の curated サブセット + Charcoal に無いグリフの
|
|
242
563
|
Material Symbols Rounded(Google、Apache-2.0)補完(`assets/icons/*.svg` →
|
|
243
564
|
`npm run generate:icons` で `src/icons/glyphs.ts` に codegen、commit 済みを配布)。
|
|
244
|
-
帰属表示は
|
|
565
|
+
帰属表示は [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md)。
|
|
245
566
|
|
|
246
567
|
### SafeArea の差し替え(`melta-app/safe-area`)
|
|
247
568
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Screen
|
|
569
|
+
safe-area は default で RN core の SafeAreaView(deprecated / iOS のみの最小対応、依存ゼロ維持。
|
|
570
|
+
**Android では完全な no-op**)。`react-native-safe-area-context` を使うアプリは subpath から
|
|
571
|
+
一度有効化すると `Screen` / `ActionSheet` / `BottomSheet` が context 版に切り替わり、
|
|
572
|
+
Android でも inset が入り、RN 0.85+ の deprecation 警告も出なくなる:
|
|
251
573
|
|
|
252
574
|
```bash
|
|
253
575
|
npx expo install react-native-safe-area-context
|
|
254
576
|
```
|
|
255
577
|
|
|
578
|
+
> bare RN(Expo なし)は `npm install` + `npx pod-install` + ネイティブ再ビルド。
|
|
579
|
+
> [bare React Native](#bare-react-nativeexpo-を使わない場合) を参照。
|
|
580
|
+
|
|
256
581
|
```tsx
|
|
257
|
-
// アプリの entry
|
|
582
|
+
// アプリの entry(初回 render より前)で一度だけ
|
|
258
583
|
import { enableSafeAreaContext } from "melta-app/safe-area";
|
|
259
584
|
enableSafeAreaContext();
|
|
260
585
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
enableSafeAreaContext({ edges: ["top"] });
|
|
586
|
+
// edges は「Screen の既定値」。省略した Screen にだけ効く
|
|
587
|
+
enableSafeAreaContext({ edges: ["top", "left", "right"] });
|
|
264
588
|
```
|
|
265
589
|
|
|
590
|
+
適用する辺の決まり方は 3 段(後のものが勝つ):
|
|
591
|
+
|
|
592
|
+
| 対象 | 適用される edge |
|
|
593
|
+
|---|---|
|
|
594
|
+
| `Screen`(`edges` 省略) | `enableSafeAreaContext({ edges })` の既定(未指定なら全 edge) |
|
|
595
|
+
| `Screen edges={[...]}` | その指定(画面ごとに変えられる) |
|
|
596
|
+
| `ActionSheet` / `BottomSheet` | 常に bottom + 左右(画面下端に出るため top は不要) |
|
|
597
|
+
|
|
598
|
+
> ボトムタブバーがある画面は、アプリ全体を `edges: ["top"]` にするのではなく
|
|
599
|
+
> **タブ配下の画面だけ [`<Screen edges={["top"]}>`](#画面骨格screen)** で絞る。シート系は
|
|
600
|
+
> bottom inset を自前で持つので、グローバル top-only はシートの下余白まで削ってしまう
|
|
601
|
+
> (0.5.x までのアプリ全体 1 個のグローバル指定はこの理由で推奨から外した)。
|
|
602
|
+
|
|
266
603
|
⚠️ 前提と契約:
|
|
267
604
|
|
|
268
605
|
- 祖先に `SafeAreaProvider` が**必須**(無いと `useSafeAreaInsets` が **throw する**。
|
|
269
606
|
React Navigation / Expo Router を使っていれば設置済みのことが多い)。初回 render から
|
|
270
607
|
正しい inset を使うには Provider に `initialMetrics` を渡す
|
|
608
|
+
- **adapter 未登録(RN core フォールバック)では `edges` は無視される。** core の SafeAreaView は
|
|
609
|
+
「安全域と交差する辺すべて」に padding を入れる仕様で辺を選べないため(Android は no-op)
|
|
271
610
|
- adapter は `useSafeAreaInsets()` を render 中に同期参照して View padding に加算する方式
|
|
272
611
|
(native SafeAreaView の初回フレーム inset 未適用によるフラッシュを避けるため)
|
|
273
612
|
- safe-area と合成する padding は**数値のみ**サポート(`"5%"` 等の非数値は基底として扱えず、
|
|
274
613
|
対象 edge は inset 値に置き換わる)。RTL / `paddingStart`・`paddingEnd` は対応済み
|
|
275
614
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
melta 契約からの逸脱を機械検知する eslint カスタムルール 4 本を同梱している。
|
|
279
|
-
消費者プロジェクトの flat config に組み込むと、生値の直書きが lint で止まる。
|
|
280
|
-
推奨 severity 込みの config を配布しているので **1 行で入る**:
|
|
281
|
-
|
|
282
|
-
```js
|
|
283
|
-
// eslint.config.mjs
|
|
284
|
-
import { meltaPlugin } from "melta-app/eslint-plugin";
|
|
285
|
-
|
|
286
|
-
export default [meltaPlugin.configs.recommended];
|
|
287
|
-
```
|
|
615
|
+
## 設計の核
|
|
288
616
|
|
|
289
|
-
|
|
290
|
-
|
|
617
|
+
- **契約は共有、実装は各最適**: tokens / 禁止ルール / component 契約は `melta-contracts`(JSON)が SSOT。melta-app に**手書きの token 正本**は持たない(`native-theme.ts` は契約からの生成物。二重管理を物理防止)。
|
|
618
|
+
- **公開 DS の純度を守る**: 汎用 UI プリミティブだけを置く。アプリ固有の UI(特定サービスの画面・機能・語彙)は**各アプリ側**に置き、ここには混ぜない。
|
|
619
|
+
- **依存最小**: 初期は RN `StyleSheet` 固定(nativewind / unistyles 等の runtime styling lib を入れない)。Storybook RN も使わず自前カタログ。
|
|
620
|
+
- **人間と AI の両方が読める**: variants / sizes / states / tokens / a11y はすべて契約の JSON。実装状態も `appStatus`(implemented / planned / not-planned)と `appMapping`(adapted = モバイル慣習への変換)が機械可読で、この README の表も showcase の表もそこから生成される。
|
|
621
|
+
- **ズレは CI が拾う**: ライブラリ内部の契約準拠は conformance テストが機械判定し、ドキュメントの腐りは drift 検査が落とす。利用側コードは consumer lint(`melta-app/eslint-plugin`)が**直接リテラル 4 類型を補助検査**する(変数・spread 経由は漏れる。純度の本丸は token 経由で書く習慣の側)。
|
|
622
|
+
- web 版には MCP サーバー(`melta-ds-mcp`)もあり、Claude Code / Cursor から契約・トークン・ルールを直接引ける(RN 対応は今後)。
|
|
291
623
|
|
|
292
|
-
|
|
293
|
-
(`@react-native/eslint-config/flat` や typescript-eslint 等、parser を持つもの)の**上に追加**すること。
|
|
294
|
-
recommended 単独の構成では TS/TSX の構文解析に失敗する。
|
|
624
|
+
内部構造(ディレクトリ / トークン正規化表 / 実装ステータスの詳細 / CI)は [docs/architecture.md](./docs/architecture.md)。
|
|
295
625
|
|
|
296
|
-
|
|
297
|
-
severity を変える、といった**カスタマイズをする場合**は spread して上書きする:
|
|
626
|
+
## 制約と正直な範囲
|
|
298
627
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
628
|
+
- **検証済み構成は Expo 56 / RN 0.85 / React 19.2**。他の組み合わせは動く可能性が高いが検証していない
|
|
629
|
+
- **consumer lint は補助線**。検知するのは**直接リテラルの 4 類型**(color / radius / spacing / fontSize)だけで、変数経由・spread 経由は漏れる。純度の本丸は token 経由で書く習慣の側にある
|
|
630
|
+
- **conformance(ライブラリ内部の契約準拠)と consumer lint(利用側コードの検査)は別物**。前者は CI で機械判定、後者は消費者の flat config に組み込んで初めて効く
|
|
631
|
+
- **lint plugin は flat config 専用**(eslintrc からは登録できない)。base config(parser)も必須
|
|
632
|
+
- **0.x 系につき破壊的変更は minor で入る**。移行手順は [CHANGELOG](./CHANGELOG.md) に毎回書く
|
|
633
|
+
- **planned / not-planned のコンポーネントは実装されていない**([表](#コンポーネント)参照)。not-planned はモバイルで別の形に変換すべきものなので、今後も web と 1:1 にはならない
|
|
302
634
|
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
...meltaPlugin.configs.recommended,
|
|
306
|
-
files: ["src/**/*.{ts,tsx}"],
|
|
307
|
-
rules: {
|
|
308
|
-
...meltaPlugin.configs.recommended.rules,
|
|
309
|
-
"melta/no-raw-spacing": "off", // 例: false positive が多い環境では落とす
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
];
|
|
313
|
-
```
|
|
635
|
+
## 成熟度・メンテナンス
|
|
314
636
|
|
|
315
|
-
|
|
637
|
+
- **0.x**。API は安定に向かっているが、破壊的変更は minor で入る([CHANGELOG](./CHANGELOG.md) が変更の正本。npm に公開した版だけを記録する)
|
|
638
|
+
- **バグ・要望は [GitHub Issues](https://github.com/tsubotax/melta-app/issues)**、コントリビュートの手引きは [CONTRIBUTING.md](./CONTRIBUTING.md)、脆弱性は [SECURITY.md](./SECURITY.md) の非公開経路へ
|
|
639
|
+
- **個人メンテナンスのプロジェクト**(tsubotax)。SLA・サポート窓口は無い。issue / PR は歓迎するが応答は best effort
|
|
640
|
+
- **現在の adopter は 1 つ**(非公開プロジェクトのモバイルアプリ)。dogfood はそこで回している。D2I は当初想定していた最初の adopter だが、mobile 側はまだ melta-app を導入していない(D2I の web 側が使っているのは melta-ui)
|
|
641
|
+
- React Native Directory 登録は [PR #2606](https://github.com/react-native-community/directory/pull/2606) がレビュー待ち
|
|
316
642
|
|
|
317
|
-
|
|
318
|
-
// eslint.config.mjs(すべて手書きする場合)
|
|
319
|
-
import { meltaPlugin } from "melta-app/eslint-plugin";
|
|
643
|
+
### 安定性の読み方
|
|
320
644
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
rules: {
|
|
325
|
-
"melta/no-raw-color": "error", // 生 hex/rgb/hsl → theme.color.*
|
|
326
|
-
"melta/no-raw-radius": "error", // borderRadius 数値直書き → theme.radius.*
|
|
327
|
-
"melta/no-raw-spacing": "warn", // padding/margin/gap 数値直書き(false positive あり)
|
|
328
|
-
"melta/no-raw-fontsize": "warn", // fontSize 数値直書き
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
];
|
|
332
|
-
```
|
|
645
|
+
- **何も書いていない公開 API は安定**として扱ってよい。変更は semver に従う(0.x なので破壊的変更は minor bump で入り、移行手順を CHANGELOG に書く)
|
|
646
|
+
- **`@experimental` 注記が付いたものは予告なく変わる**(挙動・シグネチャ・メッセージとも)。現在は theme の解決規則ヘルパ(`validateTheme` / `resolveMode` / `supportedModes` / `deriveColorScheme` / `declaredModes`)が該当し、JSDoc と型定義に注記が出る
|
|
647
|
+
- **公開面の増減は機械強制**。export の増減は snapshot テスト([scripts/lib/export-surface.test.ts](./scripts/lib/export-surface.test.ts))が、契約の破壊的変更は melta-ui 側の `npm run design:compat` が semver bump を要求する。「気づいたら消えていた」は CI で止まる
|
|
333
648
|
|
|
334
|
-
|
|
335
|
-
- `meltaPlugin` は **named export のみ**(default export なし)
|
|
336
|
-
- eslintrc(`.eslintrc.*`)からは登録できない(ESM の named export のみのため)。flat config への移行が要る
|
|
337
|
-
- 検知は AST の構文形状ベースの補助線。変数経由・spread は漏れるので、値の純度の本丸は
|
|
338
|
-
token 経由(`theme.*`)で書く習慣の側にある
|
|
649
|
+
## セキュリティ・データ境界
|
|
339
650
|
|
|
340
|
-
|
|
651
|
+
lint も theme 生成もすべて**ローカル処理**で完結する。コード・トークン・生成物を外部へ送信しない。
|
|
652
|
+
telemetry / 使用状況の収集は無い。runtime 依存ゼロ(peerDependencies のみ)なので、
|
|
653
|
+
アプリのバンドルに melta 由来のネットワーク処理は入らない。
|
|
341
654
|
|
|
342
|
-
|
|
343
|
-
- ✅ `melta-contracts` を npm 依存として購読(recipes/app の styleRefs 同梱)
|
|
344
|
-
- ✅ conformance: 契約源 ↔ 生成メタ ↔ `__contract` 宣言の照合 + consumer テスト(契約 subset / token 実在 / contractVersion 同期)+ styleRefs conformance(全実装コンポーネント展開済み)+ RN mount smoke(light/dark × 全公開コンポーネント)
|
|
345
|
-
- ✅ ハーネス: design lint(CI `--max-warnings 0` + PostToolUse hook)/ drift 検査(README・catalog・allowlist 突合)/ installability ゲート(pack → tarball 実体検査 → fixture へ install → 本体 / icons / safe-area の import + typecheck → eslint plugin を実 import してルール 4 本の実在を照合 → exports の解決先確認。`npm run release` チェーンの必須ステップ)
|
|
346
|
-
- ✅ layout 6 個(Stack / Row / Screen / Header / Icon / Avatar)— dogfood 不足 1〜4 を解消、ProjectFeedScreen は公開 primitive だけで構成
|
|
347
|
-
- Screen の SafeArea は adapter registry 化済み: default は RN core SafeAreaView(依存ゼロ維持)、`melta-app/safe-area` の `enableSafeAreaContext()` で react-native-safe-area-context に差し替え可(optional peer)
|
|
348
|
-
- ✅ form / feedback 10 個(TextField / Toggle / Checkbox / Radio / Alert / Toast / Progress / Modal / ActionSheet / BottomSheet)— checkbox / radio は Pressable + 描画(svg 非依存)、ActionSheet / BottomSheet は select / dropdown の adapted 変換先の受け皿
|
|
349
|
-
- ✅ showcase(https://app.melta.tsubotax.com — melta-ui 様式シェル + 実 RN カタログの Live 埋め込み。表・統計は契約からビルド時生成)
|
|
350
|
-
- ✅ AI 入口: [llms.txt](https://app.melta.tsubotax.com/llms.txt)(契約から生成・drift 検査対象)+ [docs/patterns.md](docs/patterns.md)(フォームの組み方規範。スニペットは実コードと機械同期)
|
|
351
|
-
- ✅ lint 強制層の npm 配布(0.5.2): `melta-app/eslint-plugin` を公開 subpath 化。消費者プロジェクトの flat config に組めば、生値の直書きが消費者側でも lint で止まる。推奨 severity は `configs.recommended` で配布(0.5.3。消費者が手書きで写さない)
|
|
352
|
-
- ✅ [npm publish(0.5.2)](https://www.npmjs.com/package/melta-app)
|
|
353
|
-
- ⬜ React Native Directory 登録([PR #2606](https://github.com/react-native-community/directory/pull/2606) レビュー待ち)
|
|
655
|
+
## Learn more
|
|
354
656
|
|
|
355
|
-
|
|
657
|
+
- [docs/architecture.md](./docs/architecture.md) — ディレクトリ構成 / トークン正規化(web → RN)/ 実装ステータス詳細 / CI(このリポを触る人向け)
|
|
658
|
+
- [docs/patterns.md](./docs/patterns.md) — フォームの組み方規範 + コピペ可能スニペット(実コードと機械同期)
|
|
659
|
+
- [llms.txt](https://app.melta.tsubotax.com/llms.txt) — AI エージェント向けの入口(契約から生成)
|
|
660
|
+
- [CHANGELOG.md](./CHANGELOG.md) — 変更の正本(破壊的変更の移行手順つき)
|
|
661
|
+
- [melta-ui](https://github.com/tsubotax/melta-ui) — web 版(契約の SSOT・MCP サーバー・禁止ルールの本体)
|
|
662
|
+
- [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md) — Icon グリフの帰属表示
|
|
356
663
|
|
|
357
664
|
## License
|
|
358
665
|
|
|
359
|
-
MIT
|
|
666
|
+
MIT([LICENSE](./LICENSE))。同梱するアイコングリフの帰属表示は [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md)。
|