melta-app 0.1.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 +21 -0
- package/README.md +197 -0
- package/THIRD_PARTY_LICENSES.md +198 -0
- package/lib/module/components/ActionSheet.js +87 -0
- package/lib/module/components/ActionSheet.js.map +1 -0
- package/lib/module/components/Alert.js +93 -0
- package/lib/module/components/Alert.js.map +1 -0
- package/lib/module/components/Avatar.js +93 -0
- package/lib/module/components/Avatar.js.map +1 -0
- package/lib/module/components/BottomSheet.js +69 -0
- package/lib/module/components/BottomSheet.js.map +1 -0
- package/lib/module/components/Card.js +93 -0
- package/lib/module/components/Card.js.map +1 -0
- package/lib/module/components/Checkbox.js +115 -0
- package/lib/module/components/Checkbox.js.map +1 -0
- package/lib/module/components/EmptyState.js +70 -0
- package/lib/module/components/EmptyState.js.map +1 -0
- package/lib/module/components/Header.js +53 -0
- package/lib/module/components/Header.js.map +1 -0
- package/lib/module/components/Image.js +67 -0
- package/lib/module/components/Image.js.map +1 -0
- package/lib/module/components/Modal.js +95 -0
- package/lib/module/components/Modal.js.map +1 -0
- package/lib/module/components/Progress.js +113 -0
- package/lib/module/components/Progress.js.map +1 -0
- package/lib/module/components/Radio.js +134 -0
- package/lib/module/components/Radio.js.map +1 -0
- package/lib/module/components/Screen.js +57 -0
- package/lib/module/components/Screen.js.map +1 -0
- package/lib/module/components/Skeleton.js +169 -0
- package/lib/module/components/Skeleton.js.map +1 -0
- package/lib/module/components/Surface.js +49 -0
- package/lib/module/components/Surface.js.map +1 -0
- package/lib/module/components/TextField.js +84 -0
- package/lib/module/components/TextField.js.map +1 -0
- package/lib/module/components/Toast.js +105 -0
- package/lib/module/components/Toast.js.map +1 -0
- package/lib/module/components/Toggle.js +77 -0
- package/lib/module/components/Toggle.js.map +1 -0
- package/lib/module/components/action-sheet.styles.js +70 -0
- package/lib/module/components/action-sheet.styles.js.map +1 -0
- package/lib/module/components/alert.styles.js +80 -0
- package/lib/module/components/alert.styles.js.map +1 -0
- package/lib/module/components/avatar.styles.js +94 -0
- package/lib/module/components/avatar.styles.js.map +1 -0
- package/lib/module/components/bottom-sheet.styles.js +55 -0
- package/lib/module/components/bottom-sheet.styles.js.map +1 -0
- package/lib/module/components/card.styles.js +50 -0
- package/lib/module/components/card.styles.js.map +1 -0
- package/lib/module/components/checkbox.styles.js +55 -0
- package/lib/module/components/checkbox.styles.js.map +1 -0
- package/lib/module/components/empty-state.styles.js +60 -0
- package/lib/module/components/empty-state.styles.js.map +1 -0
- package/lib/module/components/header.styles.js +33 -0
- package/lib/module/components/header.styles.js.map +1 -0
- package/lib/module/components/image.styles.js +37 -0
- package/lib/module/components/image.styles.js.map +1 -0
- package/lib/module/components/index.js +26 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/components/modal.styles.js +68 -0
- package/lib/module/components/modal.styles.js.map +1 -0
- package/lib/module/components/progress.styles.js +55 -0
- package/lib/module/components/progress.styles.js.map +1 -0
- package/lib/module/components/radio.styles.js +96 -0
- package/lib/module/components/radio.styles.js.map +1 -0
- package/lib/module/components/screen.styles.js +38 -0
- package/lib/module/components/screen.styles.js.map +1 -0
- package/lib/module/components/skeleton.styles.js +114 -0
- package/lib/module/components/skeleton.styles.js.map +1 -0
- package/lib/module/components/surface.styles.js +37 -0
- package/lib/module/components/surface.styles.js.map +1 -0
- package/lib/module/components/textfield.styles.js +121 -0
- package/lib/module/components/textfield.styles.js.map +1 -0
- package/lib/module/components/toast.styles.js +81 -0
- package/lib/module/components/toast.styles.js.map +1 -0
- package/lib/module/components/toggle.styles.js +69 -0
- package/lib/module/components/toggle.styles.js.map +1 -0
- package/lib/module/contracts/contract-types.js +192 -0
- package/lib/module/contracts/contract-types.js.map +1 -0
- package/lib/module/icons/Icon.js +57 -0
- package/lib/module/icons/Icon.js.map +1 -0
- package/lib/module/icons/glyphs.js +257 -0
- package/lib/module/icons/glyphs.js.map +1 -0
- package/lib/module/icons/icon.styles.js +28 -0
- package/lib/module/icons/icon.styles.js.map +1 -0
- package/lib/module/icons/index.js +14 -0
- package/lib/module/icons/index.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/primitives/Button.js +105 -0
- package/lib/module/primitives/Button.js.map +1 -0
- package/lib/module/primitives/Metric.js +68 -0
- package/lib/module/primitives/Metric.js.map +1 -0
- package/lib/module/primitives/Row.js +43 -0
- package/lib/module/primitives/Row.js.map +1 -0
- package/lib/module/primitives/Stack.js +39 -0
- package/lib/module/primitives/Stack.js.map +1 -0
- package/lib/module/primitives/Tag.js +160 -0
- package/lib/module/primitives/Tag.js.map +1 -0
- package/lib/module/primitives/Text.js +54 -0
- package/lib/module/primitives/Text.js.map +1 -0
- package/lib/module/primitives/_internal/focus-ring.js +57 -0
- package/lib/module/primitives/_internal/focus-ring.js.map +1 -0
- package/lib/module/primitives/button.styles.js +88 -0
- package/lib/module/primitives/button.styles.js.map +1 -0
- package/lib/module/primitives/index.js +11 -0
- package/lib/module/primitives/index.js.map +1 -0
- package/lib/module/primitives/metric.styles.js +54 -0
- package/lib/module/primitives/metric.styles.js.map +1 -0
- package/lib/module/primitives/row.styles.js +53 -0
- package/lib/module/primitives/row.styles.js.map +1 -0
- package/lib/module/primitives/stack.styles.js +37 -0
- package/lib/module/primitives/stack.styles.js.map +1 -0
- package/lib/module/primitives/tag.styles.js +56 -0
- package/lib/module/primitives/tag.styles.js.map +1 -0
- package/lib/module/primitives/text.styles.js +33 -0
- package/lib/module/primitives/text.styles.js.map +1 -0
- package/lib/module/theme/ThemeProvider.js +41 -0
- package/lib/module/theme/ThemeProvider.js.map +1 -0
- package/lib/module/theme/index.js +19 -0
- package/lib/module/theme/index.js.map +1 -0
- package/lib/module/theme/native-theme.js +201 -0
- package/lib/module/theme/native-theme.js.map +1 -0
- package/lib/module/theme/types.js +4 -0
- package/lib/module/theme/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/ActionSheet.d.ts +45 -0
- package/lib/typescript/src/components/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/src/components/Alert.d.ts +43 -0
- package/lib/typescript/src/components/Alert.d.ts.map +1 -0
- package/lib/typescript/src/components/Avatar.d.ts +45 -0
- package/lib/typescript/src/components/Avatar.d.ts.map +1 -0
- package/lib/typescript/src/components/BottomSheet.d.ts +36 -0
- package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
- package/lib/typescript/src/components/Card.d.ts +46 -0
- package/lib/typescript/src/components/Card.d.ts.map +1 -0
- package/lib/typescript/src/components/Checkbox.d.ts +40 -0
- package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
- package/lib/typescript/src/components/EmptyState.d.ts +36 -0
- package/lib/typescript/src/components/EmptyState.d.ts.map +1 -0
- package/lib/typescript/src/components/Header.d.ts +34 -0
- package/lib/typescript/src/components/Header.d.ts.map +1 -0
- package/lib/typescript/src/components/Image.d.ts +41 -0
- package/lib/typescript/src/components/Image.d.ts.map +1 -0
- package/lib/typescript/src/components/Modal.d.ts +46 -0
- package/lib/typescript/src/components/Modal.d.ts.map +1 -0
- package/lib/typescript/src/components/Progress.d.ts +39 -0
- package/lib/typescript/src/components/Progress.d.ts.map +1 -0
- package/lib/typescript/src/components/Radio.d.ts +52 -0
- package/lib/typescript/src/components/Radio.d.ts.map +1 -0
- package/lib/typescript/src/components/Screen.d.ts +42 -0
- package/lib/typescript/src/components/Screen.d.ts.map +1 -0
- package/lib/typescript/src/components/Skeleton.d.ts +37 -0
- package/lib/typescript/src/components/Skeleton.d.ts.map +1 -0
- package/lib/typescript/src/components/Surface.d.ts +31 -0
- package/lib/typescript/src/components/Surface.d.ts.map +1 -0
- package/lib/typescript/src/components/TextField.d.ts +45 -0
- package/lib/typescript/src/components/TextField.d.ts.map +1 -0
- package/lib/typescript/src/components/Toast.d.ts +45 -0
- package/lib/typescript/src/components/Toast.d.ts.map +1 -0
- package/lib/typescript/src/components/Toggle.d.ts +39 -0
- package/lib/typescript/src/components/Toggle.d.ts.map +1 -0
- package/lib/typescript/src/components/action-sheet.styles.d.ts +65 -0
- package/lib/typescript/src/components/action-sheet.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/alert.styles.d.ts +61 -0
- package/lib/typescript/src/components/alert.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar.styles.d.ts +60 -0
- package/lib/typescript/src/components/avatar.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet.styles.d.ts +50 -0
- package/lib/typescript/src/components/bottom-sheet.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/card.styles.d.ts +27 -0
- package/lib/typescript/src/components/card.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/checkbox.styles.d.ts +55 -0
- package/lib/typescript/src/components/checkbox.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/empty-state.styles.d.ts +58 -0
- package/lib/typescript/src/components/empty-state.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/header.styles.d.ts +30 -0
- package/lib/typescript/src/components/header.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/image.styles.d.ts +28 -0
- package/lib/typescript/src/components/image.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +23 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -0
- package/lib/typescript/src/components/modal.styles.d.ts +66 -0
- package/lib/typescript/src/components/modal.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/progress.styles.d.ts +44 -0
- package/lib/typescript/src/components/progress.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/radio.styles.d.ts +91 -0
- package/lib/typescript/src/components/radio.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/screen.styles.d.ts +34 -0
- package/lib/typescript/src/components/screen.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/skeleton.styles.d.ts +105 -0
- package/lib/typescript/src/components/skeleton.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/surface.styles.d.ts +31 -0
- package/lib/typescript/src/components/surface.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/textfield.styles.d.ts +75 -0
- package/lib/typescript/src/components/textfield.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/toast.styles.d.ts +62 -0
- package/lib/typescript/src/components/toast.styles.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle.styles.d.ts +53 -0
- package/lib/typescript/src/components/toggle.styles.d.ts.map +1 -0
- package/lib/typescript/src/contracts/contract-types.d.ts +193 -0
- package/lib/typescript/src/contracts/contract-types.d.ts.map +1 -0
- package/lib/typescript/src/icons/Icon.d.ts +40 -0
- package/lib/typescript/src/icons/Icon.d.ts.map +1 -0
- package/lib/typescript/src/icons/glyphs.d.ts +259 -0
- package/lib/typescript/src/icons/glyphs.d.ts.map +1 -0
- package/lib/typescript/src/icons/icon.styles.d.ts +20 -0
- package/lib/typescript/src/icons/icon.styles.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +12 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +12 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Button.d.ts +48 -0
- package/lib/typescript/src/primitives/Button.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Metric.d.ts +38 -0
- package/lib/typescript/src/primitives/Metric.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Row.d.ts +37 -0
- package/lib/typescript/src/primitives/Row.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Stack.d.ts +33 -0
- package/lib/typescript/src/primitives/Stack.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Tag.d.ts +43 -0
- package/lib/typescript/src/primitives/Tag.d.ts.map +1 -0
- package/lib/typescript/src/primitives/Text.d.ts +45 -0
- package/lib/typescript/src/primitives/Text.d.ts.map +1 -0
- package/lib/typescript/src/primitives/_internal/focus-ring.d.ts +31 -0
- package/lib/typescript/src/primitives/_internal/focus-ring.d.ts.map +1 -0
- package/lib/typescript/src/primitives/button.styles.d.ts +29 -0
- package/lib/typescript/src/primitives/button.styles.d.ts.map +1 -0
- package/lib/typescript/src/primitives/index.d.ts +8 -0
- package/lib/typescript/src/primitives/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/metric.styles.d.ts +43 -0
- package/lib/typescript/src/primitives/metric.styles.d.ts.map +1 -0
- package/lib/typescript/src/primitives/row.styles.d.ts +50 -0
- package/lib/typescript/src/primitives/row.styles.d.ts.map +1 -0
- package/lib/typescript/src/primitives/stack.styles.d.ts +34 -0
- package/lib/typescript/src/primitives/stack.styles.d.ts.map +1 -0
- package/lib/typescript/src/primitives/tag.styles.d.ts +32 -0
- package/lib/typescript/src/primitives/tag.styles.d.ts.map +1 -0
- package/lib/typescript/src/primitives/text.styles.d.ts +26 -0
- package/lib/typescript/src/primitives/text.styles.d.ts.map +1 -0
- package/lib/typescript/src/theme/ThemeProvider.d.ts +32 -0
- package/lib/typescript/src/theme/ThemeProvider.d.ts.map +1 -0
- package/lib/typescript/src/theme/index.d.ts +15 -0
- package/lib/typescript/src/theme/index.d.ts.map +1 -0
- package/lib/typescript/src/theme/native-theme.d.ts +3 -0
- package/lib/typescript/src/theme/native-theme.d.ts.map +1 -0
- package/lib/typescript/src/theme/types.d.ts +121 -0
- package/lib/typescript/src/theme/types.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/components/ActionSheet.tsx +112 -0
- package/src/components/Alert.tsx +84 -0
- package/src/components/Avatar.tsx +110 -0
- package/src/components/BottomSheet.tsx +81 -0
- package/src/components/Card.tsx +102 -0
- package/src/components/Checkbox.tsx +107 -0
- package/src/components/EmptyState.tsx +64 -0
- package/src/components/Header.tsx +51 -0
- package/src/components/Image.tsx +89 -0
- package/src/components/Modal.tsx +113 -0
- package/src/components/Progress.tsx +124 -0
- package/src/components/Radio.tsx +151 -0
- package/src/components/Screen.tsx +62 -0
- package/src/components/Skeleton.tsx +154 -0
- package/src/components/Surface.tsx +37 -0
- package/src/components/TextField.tsx +102 -0
- package/src/components/Toast.tsx +103 -0
- package/src/components/Toggle.tsx +94 -0
- package/src/components/action-sheet.styles.ts +102 -0
- package/src/components/alert.styles.ts +115 -0
- package/src/components/avatar.styles.ts +110 -0
- package/src/components/bottom-sheet.styles.ts +78 -0
- package/src/components/card.styles.ts +56 -0
- package/src/components/checkbox.styles.ts +83 -0
- package/src/components/empty-state.styles.ts +98 -0
- package/src/components/header.styles.ts +44 -0
- package/src/components/image.styles.ts +36 -0
- package/src/components/index.ts +23 -0
- package/src/components/modal.styles.ts +106 -0
- package/src/components/progress.styles.ts +74 -0
- package/src/components/radio.styles.ts +132 -0
- package/src/components/screen.styles.ts +44 -0
- package/src/components/skeleton.styles.ts +155 -0
- package/src/components/surface.styles.ts +50 -0
- package/src/components/textfield.styles.ts +135 -0
- package/src/components/toast.styles.ts +115 -0
- package/src/components/toggle.styles.ts +73 -0
- package/src/contracts/contract-types.ts +199 -0
- package/src/icons/Icon.tsx +65 -0
- package/src/icons/glyphs.ts +54 -0
- package/src/icons/icon.styles.ts +37 -0
- package/src/icons/index.ts +12 -0
- package/src/index.ts +24 -0
- package/src/primitives/Button.tsx +114 -0
- package/src/primitives/Metric.tsx +67 -0
- package/src/primitives/Row.tsx +54 -0
- package/src/primitives/Stack.tsx +42 -0
- package/src/primitives/Tag.tsx +126 -0
- package/src/primitives/Text.tsx +71 -0
- package/src/primitives/_internal/focus-ring.tsx +59 -0
- package/src/primitives/button.styles.ts +69 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/metric.styles.ts +75 -0
- package/src/primitives/row.styles.ts +66 -0
- package/src/primitives/stack.styles.ts +47 -0
- package/src/primitives/tag.styles.ts +65 -0
- package/src/primitives/text.styles.ts +42 -0
- package/src/theme/ThemeProvider.tsx +60 -0
- package/src/theme/index.ts +18 -0
- package/src/theme/native-theme.ts +215 -0
- package/src/theme/types.ts +137 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card — コンテンツコンテナ component(設計書 §1)。contract: card。
|
|
3
|
+
*
|
|
4
|
+
* - 土台の解決(bg/radius/elevation/border/padding)は pure resolver(card.styles.ts の
|
|
5
|
+
* resolveCardShape)に一元化し、非インタラクティブ(View)・インタラクティブ(Pressable) の
|
|
6
|
+
* 両分岐で共有する(DRY、形状二重定義を防ぐ)。recipe との機械照合は
|
|
7
|
+
* scripts/lib/card-conformance.test.ts が行う。
|
|
8
|
+
* - variant: basic/media は非インタラクティブ、action/link は Pressable。
|
|
9
|
+
* action/link は pressed 時 elevation.sm→md(§2 hover→pressed mapping)。
|
|
10
|
+
* focus-within は RN 非サポートとして Phase1 明示 drop(§2)。
|
|
11
|
+
* - 型で action/link は onPress 必須を強制(§4 の DU 精神。無言で非対話に落ちる事故を防ぐ)。
|
|
12
|
+
* - Card は Image/Metric に依存しない(§1)。media/header/footer/children に呼び出し側が差し込む。
|
|
13
|
+
* ツー活カードは D2I が slot に Image+Metric を compose する。
|
|
14
|
+
* - media variant は overflow:hidden で角丸に画像をクリップし、body だけにパディング(contract と整合)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { Pressable, View, type StyleProp, type ViewStyle } from "react-native";
|
|
19
|
+
import { useTheme } from "../theme";
|
|
20
|
+
import { useFocusRing, FocusRing } from "../primitives/_internal/focus-ring";
|
|
21
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
22
|
+
import { CARD_INTERACTIVE, resolveCardShape, resolveCardBodyStyle } from "./card.styles";
|
|
23
|
+
|
|
24
|
+
interface CardBase {
|
|
25
|
+
header?: ReactNode;
|
|
26
|
+
footer?: ReactNode;
|
|
27
|
+
media?: ReactNode;
|
|
28
|
+
accessibilityLabel?: string;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
testID?: string;
|
|
31
|
+
/** body。 */
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// action/link(インタラクティブ)は onPress 必須、basic/media は onPress を取らない(§4 DU 精神)。
|
|
36
|
+
type CardProps = CardBase &
|
|
37
|
+
(
|
|
38
|
+
| { variant?: "basic" | "media"; onPress?: never }
|
|
39
|
+
| { variant: "action" | "link"; onPress: () => void }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export function Card({
|
|
43
|
+
variant = "basic",
|
|
44
|
+
header,
|
|
45
|
+
footer,
|
|
46
|
+
media,
|
|
47
|
+
onPress,
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
style,
|
|
50
|
+
testID,
|
|
51
|
+
children,
|
|
52
|
+
}: CardProps) {
|
|
53
|
+
const { theme, mode } = useTheme();
|
|
54
|
+
const { focused, focusHandlers } = useFocusRing();
|
|
55
|
+
const interactive = CARD_INTERACTIVE[variant] && onPress != null;
|
|
56
|
+
const isMedia = variant === "media";
|
|
57
|
+
|
|
58
|
+
const inner = (
|
|
59
|
+
<>
|
|
60
|
+
{media != null && <View>{media}</View>}
|
|
61
|
+
<View style={resolveCardBodyStyle(theme, variant)}>
|
|
62
|
+
{header}
|
|
63
|
+
{children}
|
|
64
|
+
{footer}
|
|
65
|
+
</View>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// media は内側 clip View で角丸クリップ(外枠の影を消さないため overflow は内側だけに置く、M-1)。
|
|
70
|
+
// borderRadius は外枠 radius.lg から border 分を引かず同値でクリップ(視覚差は実機で微調整可)。
|
|
71
|
+
const content = isMedia ? (
|
|
72
|
+
<View style={{ borderRadius: theme.radius.lg, overflow: "hidden" }}>{inner}</View>
|
|
73
|
+
) : (
|
|
74
|
+
inner
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!interactive) {
|
|
78
|
+
return (
|
|
79
|
+
<View testID={testID} style={[resolveCardShape(theme, mode, variant), style]}>
|
|
80
|
+
{content}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// action/link: pressed 時 elevation.sm→md、focus 時 ring overlay。
|
|
86
|
+
return (
|
|
87
|
+
<Pressable
|
|
88
|
+
{...focusHandlers}
|
|
89
|
+
onPress={onPress}
|
|
90
|
+
accessibilityRole="button"
|
|
91
|
+
accessibilityLabel={accessibilityLabel}
|
|
92
|
+
testID={testID}
|
|
93
|
+
style={({ pressed }) => [resolveCardShape(theme, mode, variant, pressed), style]}
|
|
94
|
+
>
|
|
95
|
+
{content}
|
|
96
|
+
<FocusRing visible={focused} radius={theme.radius.lg} />
|
|
97
|
+
</Pressable>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase 2 conformance test 用の contract メタ(§2 A-3)。
|
|
102
|
+
Card.__contract = CONTRACTS.card;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkbox — 複数選択フォームコントロール(form-input 第1弾)。contract: checkbox。
|
|
3
|
+
*
|
|
4
|
+
* - RN にネイティブ checkbox が無いため Pressable(行全体タップ)+ 描画で実装する。
|
|
5
|
+
* checkmark は rotate した View の L 字 border、indeterminate は横棒
|
|
6
|
+
* (react-native-svg 非依存 = 本体エントリの依存ゼロ維持。recipe description 参照)。
|
|
7
|
+
* - box は 20px(web の 16px より大きいのはタッチ前提)。実タッチ領域は hitSlop 12 で
|
|
8
|
+
* 44pt(20 + 12×2)を確保する。
|
|
9
|
+
* - label 必須(FORM_NO_LABEL_OMIT を props 型で強制)。label は Text primitive
|
|
10
|
+
* (base/text-default = recipe labelStyle と同値)。
|
|
11
|
+
* - states(contract → prop の写像): checked/unchecked → checked:boolean、
|
|
12
|
+
* indeterminate → indeterminate:boolean(variant "indeterminate"。a11y は "mixed")、
|
|
13
|
+
* error → error:boolean(borderColor 差分、checked より優先)、disabled → prop
|
|
14
|
+
* (opacity 0.5 + 非活性)。focus は Phase1 非対応(recipe にも focus 差分なし)。
|
|
15
|
+
* - 色・寸法の決定は pure resolver(checkbox.styles.ts)に分離。
|
|
16
|
+
* recipes/app/checkbox.recipe.json との機械照合は scripts/lib/checkbox-conformance.test.ts が行う。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useMemo } from "react";
|
|
20
|
+
import { Pressable, View } from "react-native";
|
|
21
|
+
import { Text } from "../primitives/Text";
|
|
22
|
+
import { useTheme } from "../theme";
|
|
23
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
24
|
+
import { CHECKBOX_SPEC, resolveCheckboxStyle } from "./checkbox.styles";
|
|
25
|
+
|
|
26
|
+
/** checkmark / 横棒の幾何値(recipe に token 化されていない描画トリック側の定数)。 */
|
|
27
|
+
const CHECKMARK = { width: 10, height: 6, borderWidth: 2, offsetY: -1 } as const;
|
|
28
|
+
const INDETERMINATE_BAR = { width: 10, height: 2 } as const;
|
|
29
|
+
|
|
30
|
+
export interface CheckboxProps {
|
|
31
|
+
/** ラベル(必須。FORM_NO_LABEL_OMIT)。 */
|
|
32
|
+
label: string;
|
|
33
|
+
checked: boolean;
|
|
34
|
+
onChange: (checked: boolean) => void;
|
|
35
|
+
/** true で不確定状態(親子関係の一部選択)。mark は横棒、a11y は "mixed"。 */
|
|
36
|
+
indeterminate?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** バリデーションエラー(box の borderColor が danger になる)。 */
|
|
39
|
+
error?: boolean;
|
|
40
|
+
testID?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Checkbox({
|
|
44
|
+
label,
|
|
45
|
+
checked,
|
|
46
|
+
onChange,
|
|
47
|
+
indeterminate = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
error = false,
|
|
50
|
+
testID,
|
|
51
|
+
}: CheckboxProps) {
|
|
52
|
+
const { theme, mode } = useTheme();
|
|
53
|
+
const resolved = useMemo(
|
|
54
|
+
() => resolveCheckboxStyle(theme, mode, { checked, indeterminate, error }),
|
|
55
|
+
[theme, mode, checked, indeterminate, error],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Pressable
|
|
60
|
+
onPress={() => onChange(!checked)}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
hitSlop={CHECKBOX_SPEC.hitSlop}
|
|
63
|
+
accessibilityRole="checkbox"
|
|
64
|
+
accessibilityState={{ checked: indeterminate ? "mixed" : checked, disabled }}
|
|
65
|
+
accessibilityLabel={label}
|
|
66
|
+
testID={testID}
|
|
67
|
+
style={{
|
|
68
|
+
flexDirection: "row",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
gap: resolved.gap,
|
|
71
|
+
opacity: disabled ? CHECKBOX_SPEC.disabledOpacity : 1,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<View style={[resolved.boxStyle, { alignItems: "center", justifyContent: "center" }]}>
|
|
75
|
+
{indeterminate ? (
|
|
76
|
+
// 横棒(一部選択)。markColor = text-on-accent(states.checked.markColor と同源)。
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
width: INDETERMINATE_BAR.width,
|
|
80
|
+
height: INDETERMINATE_BAR.height,
|
|
81
|
+
backgroundColor: resolved.markColor,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
) : checked ? (
|
|
85
|
+
// L 字 border を -45deg 回転させた checkmark(svg 非依存の描画トリック)。
|
|
86
|
+
<View
|
|
87
|
+
style={{
|
|
88
|
+
width: CHECKMARK.width,
|
|
89
|
+
height: CHECKMARK.height,
|
|
90
|
+
borderLeftWidth: CHECKMARK.borderWidth,
|
|
91
|
+
borderBottomWidth: CHECKMARK.borderWidth,
|
|
92
|
+
borderColor: resolved.markColor,
|
|
93
|
+
marginTop: CHECKMARK.offsetY,
|
|
94
|
+
transform: [{ rotate: "-45deg" }],
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
) : null}
|
|
98
|
+
</View>
|
|
99
|
+
<Text variant="base" color="text-default">
|
|
100
|
+
{label}
|
|
101
|
+
</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Phase 2 conformance test 用の contract メタ(§2 A-3)。
|
|
107
|
+
Checkbox.__contract = CONTRACTS.checkbox;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyState — データ無し時のプレースホルダー(設計書 §1)。contract: empty-state。
|
|
3
|
+
* Text + Button の compose(§1)。icon + title + description + 任意の action。
|
|
4
|
+
*
|
|
5
|
+
* - title は見出しとして読み上げ(Text role="heading")。
|
|
6
|
+
* - action は Button(contained) を内部 compose。
|
|
7
|
+
* - 中央寄せレイアウト。色は token(title=text-heading, description=text-muted)。
|
|
8
|
+
* - 色・寸法の決定は pure resolver(empty-state.styles.ts)に分離。Text へ渡すキーも
|
|
9
|
+
* EMPTY_STATE_SPEC(同ファイル)を SSOT として共有し、
|
|
10
|
+
* recipes/app/empty-state.recipe.json との機械照合は scripts/lib/empty-state-conformance.test.ts が行う。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useMemo, type ReactNode } from "react";
|
|
14
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
15
|
+
import { Text } from "../primitives/Text";
|
|
16
|
+
import { Button } from "../primitives/Button";
|
|
17
|
+
import { useTheme } from "../theme";
|
|
18
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
19
|
+
import { EMPTY_STATE_SPEC, resolveEmptyStateStyles } from "./empty-state.styles";
|
|
20
|
+
|
|
21
|
+
interface EmptyStateProps {
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
action?: { label: string; onPress: () => void };
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
testID?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function EmptyState({ icon, title, description, action, style, testID }: EmptyStateProps) {
|
|
31
|
+
const { theme, mode } = useTheme();
|
|
32
|
+
const styles = useMemo(() => resolveEmptyStateStyles(theme, mode), [theme, mode]);
|
|
33
|
+
return (
|
|
34
|
+
<View testID={testID} style={[styles.style, style]}>
|
|
35
|
+
{icon != null && <View>{icon}</View>}
|
|
36
|
+
<Text
|
|
37
|
+
variant={EMPTY_STATE_SPEC.titleFont}
|
|
38
|
+
role="heading"
|
|
39
|
+
weight={EMPTY_STATE_SPEC.titleWeight}
|
|
40
|
+
color={EMPTY_STATE_SPEC.titleColor}
|
|
41
|
+
style={{ textAlign: styles.titleStyle.textAlign }}
|
|
42
|
+
>
|
|
43
|
+
{title}
|
|
44
|
+
</Text>
|
|
45
|
+
{description != null && (
|
|
46
|
+
<Text
|
|
47
|
+
variant={EMPTY_STATE_SPEC.descriptionFont}
|
|
48
|
+
color={EMPTY_STATE_SPEC.descriptionColor}
|
|
49
|
+
style={{ textAlign: styles.descriptionStyle.textAlign }}
|
|
50
|
+
>
|
|
51
|
+
{description}
|
|
52
|
+
</Text>
|
|
53
|
+
)}
|
|
54
|
+
{action != null && (
|
|
55
|
+
<View style={styles.actionStyle}>
|
|
56
|
+
<Button variant="contained" label={action.label} onPress={action.onPress} />
|
|
57
|
+
</View>
|
|
58
|
+
)}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Phase 2 conformance test 用の contract メタ(§2 A-3)。
|
|
64
|
+
EmptyState.__contract = CONTRACTS.emptyState;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header — 画面ヘッダー primitive(title + leading? / trailing? slot + 下 border)。
|
|
3
|
+
* contract: header(app 先行定義、melta-contracts)。dogfood 不足-3 の解消。
|
|
4
|
+
* Screen の header slot に差して使う想定(単体でも使える)。
|
|
5
|
+
*
|
|
6
|
+
* - title は melta の Text(xl / bold / text-heading / heading role)で描画し、
|
|
7
|
+
* heading として読み上げられる(contract a11y required)。
|
|
8
|
+
* - titleWrapStyle の flex:1 で title が余白を占有し trailing を右端へ押す。
|
|
9
|
+
* - 決定ロジックは pure resolver(header.styles.ts)に分離 — recipe との機械照合対象。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useMemo, type ReactNode } from "react";
|
|
13
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
14
|
+
import { useTheme } from "../theme";
|
|
15
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
16
|
+
import { Text } from "../primitives/Text";
|
|
17
|
+
import { resolveHeaderStyle, type HeaderStyle } from "./header.styles";
|
|
18
|
+
|
|
19
|
+
interface HeaderProps {
|
|
20
|
+
/** 画面タイトル(heading として読み上げ)。 */
|
|
21
|
+
title: string;
|
|
22
|
+
/** title の左の slot(戻るボタン等)。 */
|
|
23
|
+
leading?: ReactNode;
|
|
24
|
+
/** 右端の slot(アクション等)。 */
|
|
25
|
+
trailing?: ReactNode;
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
testID?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Header({ title, leading, trailing, style, testID }: HeaderProps) {
|
|
31
|
+
const { theme, mode } = useTheme();
|
|
32
|
+
|
|
33
|
+
const { containerStyle, titleWrapStyle } = useMemo<HeaderStyle>(
|
|
34
|
+
() => resolveHeaderStyle(theme, mode),
|
|
35
|
+
[theme, mode],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={[containerStyle, style]} testID={testID}>
|
|
40
|
+
{leading}
|
|
41
|
+
<View style={titleWrapStyle}>
|
|
42
|
+
<Text variant="xl" weight="bold" color="text-heading" role="heading" numberOfLines={1}>
|
|
43
|
+
{title}
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
{trailing}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Header.__contract = CONTRACTS.header;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image — 画像表示 component(設計書 §1)。contract: image(app 先行定義)。
|
|
3
|
+
*
|
|
4
|
+
* - Phase 1 は RN コア Image。expo-image は Phase 2(blurhash / 高度 cache 要件化時)。
|
|
5
|
+
* 差し替えコスト最小化のためここで包む(呼び出し側は melta-app の Image だけ見る)。
|
|
6
|
+
* - contentFit("cover"|"contain") → RN の resizeMode に変換。
|
|
7
|
+
* - onError 時に fallback(ReactNode)を表示。fallback 未指定なら何も出さない。
|
|
8
|
+
* - radius は token キーのみ(生数値不可、§5)。shape の解決は pure resolver
|
|
9
|
+
* (image.styles.ts)に分離。recipes/app/image.recipe.json との機械照合は
|
|
10
|
+
* scripts/lib/image-conformance.test.ts が行う。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, type ReactNode } from "react";
|
|
14
|
+
import {
|
|
15
|
+
Image as RNImage,
|
|
16
|
+
View,
|
|
17
|
+
type ImageSourcePropType,
|
|
18
|
+
type ImageStyle,
|
|
19
|
+
type StyleProp,
|
|
20
|
+
type ViewStyle,
|
|
21
|
+
} from "react-native";
|
|
22
|
+
import { useTheme } from "../theme";
|
|
23
|
+
import type { RadiusKey } from "../theme";
|
|
24
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
25
|
+
import { resolveImageShape } from "./image.styles";
|
|
26
|
+
|
|
27
|
+
type ContentFit = "cover" | "contain";
|
|
28
|
+
|
|
29
|
+
interface ImageProps {
|
|
30
|
+
source: { uri: string } | number;
|
|
31
|
+
/** 縦横比(width:height)。指定すると width 基準で高さが決まる。 */
|
|
32
|
+
aspectRatio?: number;
|
|
33
|
+
radius?: RadiusKey;
|
|
34
|
+
contentFit?: ContentFit;
|
|
35
|
+
/** 読み込み失敗時に表示する代替(onError)。 */
|
|
36
|
+
fallback?: ReactNode;
|
|
37
|
+
accessibilityLabel?: string;
|
|
38
|
+
style?: StyleProp<ImageStyle>;
|
|
39
|
+
testID?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Image({
|
|
43
|
+
source,
|
|
44
|
+
aspectRatio,
|
|
45
|
+
radius,
|
|
46
|
+
contentFit = "cover",
|
|
47
|
+
fallback,
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
style,
|
|
50
|
+
testID,
|
|
51
|
+
}: ImageProps) {
|
|
52
|
+
const { theme } = useTheme();
|
|
53
|
+
const [errored, setErrored] = useState(false);
|
|
54
|
+
|
|
55
|
+
// shape は ImageStyle / ViewStyle 双方に有効な共通サブセット(resolver 参照)なので、
|
|
56
|
+
// RNImage と fallback View で同じ値を共用できる。
|
|
57
|
+
const shape = resolveImageShape(theme, { aspectRatio, radius });
|
|
58
|
+
|
|
59
|
+
if (errored && fallback != null) {
|
|
60
|
+
// caller の style(width/height/flex 等の layout)を fallback にも反映する(通常画像と同寸法)。
|
|
61
|
+
// ImageStyle ⊃ ViewStyle ではないが layout プロパティは共通なので cast で流す
|
|
62
|
+
// (overflow:"scroll" 等 Image 固有値を caller が入れた稀ケースのみ無視される)。
|
|
63
|
+
return (
|
|
64
|
+
<View
|
|
65
|
+
style={[shape, style as unknown as StyleProp<ViewStyle>]}
|
|
66
|
+
testID={testID}
|
|
67
|
+
accessible={accessibilityLabel != null}
|
|
68
|
+
accessibilityLabel={accessibilityLabel}
|
|
69
|
+
>
|
|
70
|
+
{fallback}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<RNImage
|
|
77
|
+
testID={testID}
|
|
78
|
+
source={source as ImageSourcePropType}
|
|
79
|
+
resizeMode={contentFit}
|
|
80
|
+
onError={() => setErrored(true)}
|
|
81
|
+
accessible={accessibilityLabel != null}
|
|
82
|
+
accessibilityLabel={accessibilityLabel}
|
|
83
|
+
style={[shape, style]}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Phase 2 conformance test 用の contract メタ(§2 A-3)。
|
|
89
|
+
Image.__contract = CONTRACTS.image;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal — フォーカストラップとバックドロップを備えたモーダルダイアログ。
|
|
3
|
+
* contract: modal(feedback)。RN core Modal(transparent + fade)に overlay + panel を載せる。
|
|
4
|
+
*
|
|
5
|
+
* - variant(confirmation/form/alert)は意味分類で style は共通(contract / recipe と同じ)。
|
|
6
|
+
* - size は panel の maxWidth(タブレット以上で効く。モバイルは width 100% - overlay padding が先)。
|
|
7
|
+
* - 契約の対応物: focus trap → accessibilityViewIsModal(iOS のフォーカス閉じ込め)、
|
|
8
|
+
* Escape → Android back(onRequestClose)、open/closing state → RN Modal の visible + animationType。
|
|
9
|
+
* - overlay tap で閉じる(MODAL_OVERLAY_REQUIRED)。panel 側は onStartShouldSetResponder で
|
|
10
|
+
* touch を claim し、overlay への伝播を止める(panel 内 tap では閉じない)。
|
|
11
|
+
* - title 行の右端に閉じるボタン「×」(MODAL_CLOSE_REQUIRED、accessibilityLabel="閉じる")。
|
|
12
|
+
* title は melta の Text ではなく titleStyle 直(recipe が typography を持つ)。
|
|
13
|
+
* - style の決定は pure resolver(modal.styles.ts)に分離 — recipe との機械照合対象。
|
|
14
|
+
* title 行の row layout と × の見た目は recipe 外の構造 glue(token 経由で解決)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useMemo, type ReactNode } from "react";
|
|
18
|
+
import {
|
|
19
|
+
Modal as RNModal,
|
|
20
|
+
Pressable,
|
|
21
|
+
Text as RNText,
|
|
22
|
+
View,
|
|
23
|
+
} from "react-native";
|
|
24
|
+
import { useTheme } from "../theme";
|
|
25
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
26
|
+
import {
|
|
27
|
+
resolveModalStyle,
|
|
28
|
+
type ModalSize,
|
|
29
|
+
type ModalStyle,
|
|
30
|
+
type ModalVariant,
|
|
31
|
+
} from "./modal.styles";
|
|
32
|
+
|
|
33
|
+
interface ModalProps {
|
|
34
|
+
/** 表示制御(契約の open state に対応。閉じるアニメは RN Modal に委譲)。 */
|
|
35
|
+
visible: boolean;
|
|
36
|
+
/** 閉じ要求(overlay tap / × / Android back の全経路で呼ばれる)。 */
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
/** タイトル(契約 aria-labelledby 相当、必須)。 */
|
|
39
|
+
title: string;
|
|
40
|
+
/** 意味分類(style は 3 つとも同一)。default "confirmation"。 */
|
|
41
|
+
variant?: ModalVariant;
|
|
42
|
+
/** panel の maxWidth 段階。default "medium"。 */
|
|
43
|
+
size?: ModalSize;
|
|
44
|
+
/** アクション群(任意)。footerStyle の row に載る。 */
|
|
45
|
+
footer?: ReactNode;
|
|
46
|
+
testID?: string;
|
|
47
|
+
/** body。 */
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Modal({
|
|
52
|
+
visible,
|
|
53
|
+
onClose,
|
|
54
|
+
title,
|
|
55
|
+
variant = "confirmation",
|
|
56
|
+
size = "medium",
|
|
57
|
+
footer,
|
|
58
|
+
testID,
|
|
59
|
+
children,
|
|
60
|
+
}: ModalProps) {
|
|
61
|
+
const { theme, mode, colors } = useTheme();
|
|
62
|
+
|
|
63
|
+
const styles = useMemo<ModalStyle>(
|
|
64
|
+
() => resolveModalStyle(theme, mode, { variant, size }),
|
|
65
|
+
[theme, mode, variant, size],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<RNModal
|
|
70
|
+
visible={visible}
|
|
71
|
+
transparent
|
|
72
|
+
animationType="fade"
|
|
73
|
+
onRequestClose={onClose}
|
|
74
|
+
statusBarTranslucent
|
|
75
|
+
>
|
|
76
|
+
{/* overlay: tap で閉じる。a11y 要素にはしない(閉じ導線は × と Android back が担う) */}
|
|
77
|
+
<Pressable style={styles.overlayStyle} onPress={onClose} accessible={false}>
|
|
78
|
+
<View
|
|
79
|
+
style={[styles.panelStyle, styles.panelElevation]}
|
|
80
|
+
accessibilityViewIsModal
|
|
81
|
+
onStartShouldSetResponder={() => true}
|
|
82
|
+
testID={testID}
|
|
83
|
+
>
|
|
84
|
+
{/* title 行: title が余白を占有し × を右端へ押す(recipe 外の構造 glue) */}
|
|
85
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start" }}>
|
|
86
|
+
<RNText style={[styles.titleStyle, { flex: 1 }]} accessibilityRole="header">
|
|
87
|
+
{title}
|
|
88
|
+
</RNText>
|
|
89
|
+
<Pressable
|
|
90
|
+
onPress={onClose}
|
|
91
|
+
accessibilityRole="button"
|
|
92
|
+
accessibilityLabel="閉じる"
|
|
93
|
+
hitSlop={theme.spacing["2"]}
|
|
94
|
+
>
|
|
95
|
+
<RNText
|
|
96
|
+
style={{
|
|
97
|
+
fontSize: theme.typography.fontSize.xl.fontSize,
|
|
98
|
+
color: colors["text-muted"],
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
×
|
|
102
|
+
</RNText>
|
|
103
|
+
</Pressable>
|
|
104
|
+
</View>
|
|
105
|
+
<View style={styles.bodyStyle}>{children}</View>
|
|
106
|
+
{footer != null && <View style={styles.footerStyle}>{footer}</View>}
|
|
107
|
+
</View>
|
|
108
|
+
</Pressable>
|
|
109
|
+
</RNModal>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Modal.__contract = CONTRACTS.modal;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress — リニアプログレスバー(feedback)。contract: progress。
|
|
3
|
+
* track の中を fill が value%(determinate)または往復アニメ(indeterminate variant)で埋める。
|
|
4
|
+
*
|
|
5
|
+
* - variant: primary / success(determinate、value 0〜100 を clamp)/ indeterminate(幅 40% の fill を
|
|
6
|
+
* Animated.loop の translateX 往復で流す。duration は theme.motion.duration.slow 基準)。
|
|
7
|
+
* - MOTION_REDUCED_MOTION_REQUIRED(contract rule): AccessibilityInfo.isReduceMotionEnabled() を
|
|
8
|
+
* 初期読取り + reduceMotionChanged 購読で追従し、reduce motion 時は indeterminate のアニメを
|
|
9
|
+
* 止めて静止 40% にする。translateX は 0 初期化なので同期起動でも一瞬動く事故は無い
|
|
10
|
+
* (Skeleton の順序ハザード対策と同趣旨)。
|
|
11
|
+
* - a11y: accessibilityRole="progressbar" + accessibilityValue {min:0, max:100, now}
|
|
12
|
+
* (indeterminate は now を渡さない)+ accessibilityLabel=label(契約 required)。
|
|
13
|
+
* - 色・寸法の決定は pure resolver(progress.styles.ts)に分離。track の semantic 色は dark 側解決。
|
|
14
|
+
* recipes/app/progress.recipe.json との機械照合は scripts/lib/progress-conformance.test.ts が行う。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import {
|
|
19
|
+
AccessibilityInfo,
|
|
20
|
+
Animated,
|
|
21
|
+
View,
|
|
22
|
+
type LayoutChangeEvent,
|
|
23
|
+
type StyleProp,
|
|
24
|
+
type ViewStyle,
|
|
25
|
+
} from "react-native";
|
|
26
|
+
import { useTheme } from "../theme";
|
|
27
|
+
import { CONTRACTS } from "../contracts/contract-types";
|
|
28
|
+
import {
|
|
29
|
+
PROGRESS_INDETERMINATE_FILL_RATIO,
|
|
30
|
+
clampProgressValue,
|
|
31
|
+
resolveProgressStyles,
|
|
32
|
+
type ProgressVariant,
|
|
33
|
+
} from "./progress.styles";
|
|
34
|
+
|
|
35
|
+
interface ProgressProps {
|
|
36
|
+
/** 進捗タイプ(contract variant)。default "primary"。 */
|
|
37
|
+
variant?: ProgressVariant;
|
|
38
|
+
/** 進捗値 0〜100(determinate 用。範囲外は clamp)。 */
|
|
39
|
+
value?: number;
|
|
40
|
+
/** 読み上げラベル(accessibilityLabel。契約 required)。 */
|
|
41
|
+
label: string;
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
testID?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Progress({ variant = "primary", value, label, style, testID }: ProgressProps) {
|
|
47
|
+
const { theme, mode } = useTheme();
|
|
48
|
+
const styles = useMemo(() => resolveProgressStyles(theme, mode, variant), [theme, mode, variant]);
|
|
49
|
+
const indeterminate = variant === "indeterminate";
|
|
50
|
+
const clamped = clampProgressValue(value ?? 0);
|
|
51
|
+
|
|
52
|
+
// indeterminate の往復アニメ: 0〜1 の進行値を track 実測幅で translateX に interpolate する。
|
|
53
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
54
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
55
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
|
|
60
|
+
if (!cancelled) setReduceMotion(enabled);
|
|
61
|
+
});
|
|
62
|
+
const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", setReduceMotion);
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
sub.remove();
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
// reduce motion ON / determinate / 幅未計測 → アニメ無し(fill は左端 40% で静止)。
|
|
71
|
+
if (!indeterminate || reduceMotion || trackWidth <= 0) {
|
|
72
|
+
progress.setValue(0);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const duration = theme.motion.duration.slow;
|
|
76
|
+
const loop = Animated.loop(
|
|
77
|
+
Animated.sequence([
|
|
78
|
+
Animated.timing(progress, { toValue: 1, duration, useNativeDriver: true }),
|
|
79
|
+
Animated.timing(progress, { toValue: 0, duration, useNativeDriver: true }),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
loop.start();
|
|
83
|
+
return () => loop.stop();
|
|
84
|
+
}, [indeterminate, reduceMotion, trackWidth, progress, theme]);
|
|
85
|
+
|
|
86
|
+
const onLayout = indeterminate
|
|
87
|
+
? (e: LayoutChangeEvent) => setTrackWidth(e.nativeEvent.layout.width)
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
// fill(40%) が track 内を左端 → 右端で往復する移動量。
|
|
91
|
+
const translateX = progress.interpolate({
|
|
92
|
+
inputRange: [0, 1],
|
|
93
|
+
outputRange: [0, Math.max(0, trackWidth * (1 - PROGRESS_INDETERMINATE_FILL_RATIO))],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<View
|
|
98
|
+
accessible
|
|
99
|
+
accessibilityRole="progressbar"
|
|
100
|
+
accessibilityValue={indeterminate ? { min: 0, max: 100 } : { min: 0, max: 100, now: clamped }}
|
|
101
|
+
accessibilityLabel={label}
|
|
102
|
+
testID={testID}
|
|
103
|
+
onLayout={onLayout}
|
|
104
|
+
style={[styles.trackStyle, style]}
|
|
105
|
+
>
|
|
106
|
+
{indeterminate ? (
|
|
107
|
+
<Animated.View
|
|
108
|
+
style={[
|
|
109
|
+
styles.fillStyle,
|
|
110
|
+
{
|
|
111
|
+
width: `${PROGRESS_INDETERMINATE_FILL_RATIO * 100}%`,
|
|
112
|
+
transform: [{ translateX }],
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<View style={[styles.fillStyle, { width: `${clamped}%` }]} />
|
|
118
|
+
)}
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Phase 2 conformance test 用の contract メタ(§2 A-3)。
|
|
124
|
+
Progress.__contract = CONTRACTS.progress;
|