Files
jrshikoku/components/Settings/DataSourceSettings.tsx
harukin-expo-dev-env 4f4d3cad0a fix: crash on playback start + support multiple recordings
Crash fix (React Hooks violation):
- PlaybackTimeline.tsx: move ALL hooks (useRef for PanResponder, useCallback)
  to before the early return; use seekRef/totalRef to share values into
  PanResponder handlers without stale closure issues

Multiple recordings support:
- trainRecorder.ts: redesign to id-based multi-recording system
  - RecordingMeta type for lightweight list (no snapshots)
  - TrainRecording now includes id field
  - saveRecording/loadRecordingById/deleteRecordingById/loadRecordingList
  - migrateOldRecording() migrates old single MOCK_RECORDING key on first launch
- constants/storage.ts: add MOCK_RECORDINGS_INDEX + MOCK_RECORDING_DATA_PREFIX keys
- useTrainMenu: savedRecording → recordingList (RecordingMeta[]) + activeRecording (TrainRecording|null)
  - startPlayback(id) loads full recording on demand
  - deleteRecording(id) deletes by id and refreshes list
  - stopPlayback clears activeRecording
- DataSourceSettings: recording list UI
  - shows all recordings with date/time, snapshot count, duration
  - ▶ play and 削除 buttons per row
  - recording/playing status indicator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:52:30 +00:00

985 lines
31 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from "react";
import {
View,
Text,
ScrollView,
StyleSheet,
Image,
TouchableOpacity,
Linking,
} from "react-native";
import { Switch } from "@rneui/themed";
import { useNavigation } from "@react-navigation/native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
import { AS } from "../../storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { useThemeColors } from "@/lib/theme";
import {
DEFAULT_JR_DATA_SYSTEM_ENV,
getJrDataSystemTrack,
getJrDataSystemUiVariant,
JR_DATA_SYSTEM_ENV_OPTIONS,
JrDataSystemTrack,
JrDataSystemUiVariant,
JrDataSystemEnvironmentKey,
normalizeJrDataSystemEnvironment,
resolveJrDataSystemEnvironment,
} from "@/lib/jrDataSystemEnvironment";
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg");
/* ------------------------------------------------------------------ */
/* DataSourceAccordionCard */
/* ------------------------------------------------------------------ */
type Feature = { icon: string; label: string; text: string };
type DataSourceAccordionCardProps = {
/** ロゴ画像 (require) */
logo: any;
/** アクセントカラー */
accentColor: string;
/** データソース名 */
title: string;
/** 1行サブタイトル */
tagline: string;
/** スイッチの値 */
enabled: boolean;
/** スイッチ変更ハンドラ */
onToggle: (v: boolean) => void;
/** 説明文 */
description: string;
/** 機能リスト */
features: Feature[];
/** フッターリンクラベル */
linkLabel: string;
/** フッターリンク URL */
linkUrl: string;
/** 詳細ラベル */
detailLabel?: string;
};
const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
logo,
accentColor,
title,
tagline,
enabled,
onToggle,
description,
features,
linkLabel,
linkUrl,
detailLabel,
}) => {
const [expanded, setExpanded] = useState(false);
const { colors } = useThemeColors();
return (
<View
style={[
styles.accordionCard,
{
backgroundColor: colors.surface,
borderColor: colors.borderSecondary,
},
enabled && styles.accordionCardEnabled,
]}
>
{/* ── ヘッダー行(常時表示) ── */}
<View style={styles.accordionHeader}>
{/* 左:ロゴ */}
<Image source={logo} style={styles.accordionLogo} />
{/* 中央:タイトル+タグライン */}
<View style={styles.accordionTitles}>
<Text style={[styles.accordionTitle, { color: colors.textPrimary }]}>
{title}
</Text>
<Text
style={[styles.accordionTagline, { color: colors.textTertiary }]}
>
{tagline}
</Text>
</View>
{/* 右:スイッチ */}
<Switch
value={enabled}
onValueChange={onToggle}
color={accentColor}
style={styles.accordionSwitch}
/>
</View>
{/* スイッチ状態テキスト */}
<View style={styles.accordionStatusRow}>
<View
style={[
styles.statusDot,
{ backgroundColor: enabled ? accentColor : colors.textDisabled },
]}
/>
<Text
style={[
styles.statusText,
{ color: enabled ? accentColor : colors.textQuaternary },
]}
>
{enabled
? "有効 — 編成データを取得します"
: "無効 — データを取得しません"}
</Text>
</View>
{/* ── 展開トリガー ── */}
<TouchableOpacity
style={[
styles.accordionToggleRow,
{ borderTopColor: colors.borderCard },
]}
onPress={() => setExpanded((v) => !v)}
activeOpacity={0.6}
>
<Text
style={[styles.accordionToggleLabel, { color: colors.textSecondary }]}
>
{expanded ? "詳細を閉じる" : (detailLabel ?? `${title} について`)}
</Text>
<MaterialCommunityIcons
name={expanded ? "chevron-up" : "chevron-down"}
size={16}
color={colors.iconSecondary}
/>
</TouchableOpacity>
{/* ── 展開コンテンツ ── */}
{expanded && (
<View
style={[
styles.accordionBody,
{
borderTopColor: colors.borderCard,
backgroundColor: colors.backgroundTertiary,
},
]}
>
{/* 説明文 */}
<Text style={[styles.bodyDesc, { color: colors.textSecondary }]}>
{description}
</Text>
{/* 機能リスト */}
<View
style={[
styles.bodyFeatures,
{ borderTopColor: colors.borderSecondary },
]}
>
{features.map((f) => (
<View key={f.icon} style={styles.featureRow}>
<View style={styles.featureIcon}>
<MaterialCommunityIcons
name={f.icon as any}
size={14}
color={colors.iconSecondary}
/>
</View>
<Text
style={[styles.featureLabel, { color: colors.textPrimary }]}
>
{f.label}
</Text>
<Text
style={[styles.featureText, { color: colors.textSecondary }]}
>
{f.text}
</Text>
</View>
))}
</View>
{/* リンク */}
<TouchableOpacity
style={[
styles.bodyLink,
{ borderTopColor: colors.borderSecondary },
]}
onPress={() => Linking.openURL(linkUrl)}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name="open-in-new"
size={13}
color={colors.iconSecondary}
/>
<Text
style={[styles.bodyLinkText, { color: colors.textSecondary }]}
>
{linkLabel}
</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
/* ------------------------------------------------------------------ */
/* 定数 */
/* ------------------------------------------------------------------ */
const UNYOHUB_FEATURES: Feature[] = [
{
icon: "calendar-today",
label: "運用データ",
text: "当日・過去数日から投稿があった運用の継続予測運用情報を表示",
},
{ icon: "map-outline", label: "対象エリア", text: "JR四国全線" },
{
icon: "train",
label: "対象運用",
text: "JR四国管内営業列車及び貨物列車,定期回送列車に対応、臨時列車/突発運用は非対応",
},
{
icon: "pencil",
label: "入力方式",
text: "アプリ内連携システムにて当日の運用の投稿が可能",
},
];
const ELESITE_FEATURES: Feature[] = [
{
icon: "calendar-today",
label: "運用データ",
text: "当日報告のあった運用情報のみ表示",
},
{
icon: "map-outline",
label: "対象エリア",
text: "予讃線/瀬戸大橋線(直通している特急などの列番は含みます)",
},
{
icon: "train",
label: "対象運用",
text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応",
},
{
icon: "pencil",
label: "入力方式",
text: "アプリ外リンク連携にて当日の運用の投稿が可能",
},
];
/* ------------------------------------------------------------------ */
/* DataSourceSettings */
/* ------------------------------------------------------------------ */
export const DataSourceSettings = () => {
const navigation = useNavigation();
const {
updatePermission,
mockApiFeatureEnabled,
setMockApiFeatureEnabled,
recorderState,
recordingSnapshotCount,
recordingList,
startRecording,
stopRecording,
startPlayback,
stopPlayback,
deleteRecording,
} = useTrainMenu();
const { colors, fixed } = useThemeColors();
const showDebugSelector = __DEV__ || updatePermission;
const [useUnyohub, setUseUnyohub] = useState(false);
const [useElesite, setUseElesite] = useState(false);
const [jrDataSystemEnv, setJrDataSystemEnv] =
useState<JrDataSystemEnvironmentKey>(DEFAULT_JR_DATA_SYSTEM_ENV);
const [jrDataSystemTrack, setJrDataSystemTrack] = useState<JrDataSystemTrack>(
getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const [jrDataSystemUiVariant, setJrDataSystemUiVariant] =
useState<JrDataSystemUiVariant>(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const applyJrDataSystemEnv = (env: JrDataSystemEnvironmentKey) => {
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, env);
};
useEffect(() => {
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
setUseUnyohub(value === true || value === "true");
});
AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => {
setUseElesite(value === true || value === "true");
});
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then((value) => {
const env = normalizeJrDataSystemEnvironment(value);
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
})
.catch(() => {
setJrDataSystemEnv(DEFAULT_JR_DATA_SYSTEM_ENV);
setJrDataSystemTrack(getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV));
setJrDataSystemUiVariant(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
});
}, []);
const handleToggleUnyohub = (value: boolean) => {
setUseUnyohub(value);
AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString());
};
const handleToggleElesite = (value: boolean) => {
setUseElesite(value);
AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString());
};
const handleSelectJrDataSystemTrack = (value: JrDataSystemTrack) => {
setJrDataSystemTrack(value);
const normalizedVariant =
value === "experimental" ? "release" : jrDataSystemUiVariant;
setJrDataSystemUiVariant(normalizedVariant);
const env = resolveJrDataSystemEnvironment(value, normalizedVariant);
applyJrDataSystemEnv(env);
};
const handleSelectJrDataSystemUiVariant = (value: JrDataSystemUiVariant) => {
setJrDataSystemUiVariant(value);
const env = resolveJrDataSystemEnvironment(jrDataSystemTrack, value);
applyJrDataSystemEnv(env);
};
return (
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
<SheetHeaderItem
title="情報ソース設定"
LeftItem={{
title: " 戻る",
onPress: () => navigation.goBack(),
position: "left",
}}
/>
<ScrollView
style={[
styles.content,
{ backgroundColor: colors.backgroundSecondary },
]}
contentContainerStyle={styles.contentInner}
>
<Text style={[styles.sectionTitle, { color: colors.textTertiary }]}>
</Text>
<DataSourceAccordionCard
logo={HUB_LOGO_PNG}
accentColor="#0099CC"
title="鉄道運用Hub"
tagline="コミュニティによる列車運用情報サービス"
enabled={useUnyohub}
onToggle={handleToggleUnyohub}
description={
"鉄道運用Hubはオープンソースのユーザー投稿型鉄道運用情報データベースアプリケーションです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
}
features={UNYOHUB_FEATURES}
linkLabel="unyohub.2pd.jp を開くJR四国"
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
/>
<DataSourceAccordionCard
logo={ELESITE_LOGO_PNG}
accentColor="#44bb44"
title="えれサイト"
tagline="コミュニティによる列車運用情報サービス"
enabled={useElesite}
onToggle={handleToggleElesite}
description={
"えれサイトは、鉄道の運用情報を利用者同士で共有するサービスです。皆様からの投稿をもとに、列車のリアルタイムな動きを反映しています。JR四国の特急・普通列車をはじめ、現在は全国の路線に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
}
features={ELESITE_FEATURES}
linkLabel="elesite-next.com を開く"
linkUrl="https://www.elesite-next.com/"
/>
<View
style={[
styles.infoSection,
{ backgroundColor: colors.backgroundTertiary },
]}
>
<Text style={[styles.infoText, { color: colors.textCaution }]}>
{"\n\n"}
{"\n\n"}
JR四国非公式アプリが管理していないデータであるため
</Text>
</View>
{showDebugSelector && (
<>
<View
style={[
styles.debugSection,
{
backgroundColor: colors.surface,
borderColor: colors.borderSecondary,
},
]}
>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
デバッグ: モックAPI検証
</Text>
<Text
style={[
styles.debugDescription,
{ color: colors.textSecondary },
]}
>
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 8,
}}
>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textPrimary, fontSize: 14 },
]}
>
API検証機能
</Text>
<Switch
value={mockApiFeatureEnabled}
onValueChange={setMockApiFeatureEnabled}
color={fixed.primary}
/>
</View>
</View>
{/* 録画・再生 */}
<View
style={[
styles.debugSection,
{ backgroundColor: colors.surface, borderColor: colors.borderSecondary },
]}
>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
デバッグ: 走行位置録画
</Text>
<Text style={[styles.debugDescription, { color: colors.textSecondary }]}>
OFFになります
</Text>
{/* ステータス行 */}
<View style={{ flexDirection: "row", alignItems: "center", marginTop: 8, gap: 8 }}>
<View
style={{
width: 10, height: 10, borderRadius: 5,
backgroundColor:
recorderState === 'recording' ? '#e53935' :
recorderState === 'playing' ? '#43a047' : colors.borderSecondary,
}}
/>
<Text style={[styles.debugCurrentText, { color: colors.textSecondary, fontSize: 13 }]}>
{recorderState === 'recording'
? `録画中… ${recordingSnapshotCount} スナップショット`
: recorderState === 'playing'
? '再生中'
: `${recordingList.length} 件の録画`}
</Text>
</View>
{/* 録画開始 / 停止ボタン */}
<View style={{ flexDirection: "row", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
{recorderState === 'idle' && (
<TouchableOpacity
onPress={startRecording}
style={{
backgroundColor: '#e53935', borderRadius: 8,
paddingHorizontal: 14, paddingVertical: 8,
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 13 }}> </Text>
</TouchableOpacity>
)}
{recorderState === 'recording' && (
<TouchableOpacity
onPress={stopRecording}
style={{
backgroundColor: colors.borderSecondary, borderRadius: 8,
paddingHorizontal: 14, paddingVertical: 8,
}}
>
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}> </Text>
</TouchableOpacity>
)}
{recorderState === 'playing' && (
<TouchableOpacity
onPress={stopPlayback}
style={{
backgroundColor: colors.borderSecondary, borderRadius: 8,
paddingHorizontal: 14, paddingVertical: 8,
}}
>
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}> </Text>
</TouchableOpacity>
)}
</View>
{/* 録画一覧 */}
{recordingList.length > 0 && recorderState !== 'recording' && (
<View style={{ marginTop: 10, gap: 6 }}>
{recordingList.map((rec) => {
const isPlaying = recorderState === 'playing';
const durationSec = Math.round(rec.durationMs / 1000);
const durationLabel = durationSec >= 60
? `${Math.floor(durationSec / 60)}${durationSec % 60}`
: `${durationSec}`;
const dateLabel = new Date(rec.recordedAt).toLocaleString('ja-JP', {
month: 'numeric', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
return (
<View
key={rec.id}
style={{
flexDirection: 'row', alignItems: 'center',
backgroundColor: colors.backgroundSecondary,
borderRadius: 8, padding: 10, gap: 8,
}}
>
<View style={{ flex: 1 }}>
<Text style={{ color: colors.textPrimary, fontSize: 13, fontWeight: 'bold' }}>
{dateLabel}
</Text>
<Text style={{ color: colors.textSecondary, fontSize: 11 }}>
{rec.snapshotCount} / {durationLabel}
</Text>
</View>
{!isPlaying && (
<TouchableOpacity
onPress={() => startPlayback(rec.id)}
style={{
backgroundColor: '#43a047', borderRadius: 6,
paddingHorizontal: 10, paddingVertical: 6,
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 12 }}></Text>
</TouchableOpacity>
)}
{!isPlaying && (
<TouchableOpacity
onPress={() => deleteRecording(rec.id)}
style={{
borderColor: '#e53935', borderWidth: 1, borderRadius: 6,
paddingHorizontal: 10, paddingVertical: 6,
}}
>
<Text style={{ color: '#e53935', fontSize: 12 }}></Text>
</TouchableOpacity>
)}
</View>
);
})}
</View>
)}
</View>
<View
style={[
styles.debugSection,
{
backgroundColor: colors.surface,
borderColor: colors.borderSecondary,
},
]}
>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
稿
</Text>
<Text
style={[
styles.debugDescription,
{ color: colors.textSecondary },
]}
>
/
</Text>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
</Text>
<View style={styles.debugOptionRow}>
{[
{
key: "production" as const,
label: "本番",
caption: "一般公開向け",
},
{
key: "experimental" as const,
label: "実験",
caption: "毎日リセット",
},
].map((option) => {
const selected = jrDataSystemTrack === option.key;
return (
<TouchableOpacity
key={option.key}
style={[
styles.debugOptionButton,
{
backgroundColor: selected
? fixed.primary
: colors.backgroundTertiary,
borderColor: selected
? fixed.primary
: colors.borderSecondary,
},
]}
onPress={() => handleSelectJrDataSystemTrack(option.key)}
activeOpacity={0.8}
>
<Text
style={[
styles.debugOptionTitle,
{
color: selected
? fixed.textOnPrimary
: colors.textPrimary,
},
]}
>
{option.label}
</Text>
<Text
style={[
styles.debugOptionCaption,
{
color: selected
? fixed.textOnPrimary
: colors.textTertiary,
},
]}
>
{option.caption}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
UIバージョン
</Text>
<View style={styles.debugOptionRow}>
{[
{
key: "release" as const,
label: "リリース",
caption: "安定版",
},
{
key: "beta" as const,
label: "ベータ",
caption: "夜間ビルド",
},
].map((option) => {
const selected = jrDataSystemUiVariant === option.key;
const disabled = jrDataSystemTrack === "experimental";
return (
<TouchableOpacity
key={option.key}
style={[
styles.debugOptionButton,
{
opacity: disabled ? 0.45 : 1,
backgroundColor: selected
? fixed.primary
: colors.backgroundTertiary,
borderColor: selected
? fixed.primary
: colors.borderSecondary,
},
]}
onPress={() => {
if (disabled) return;
handleSelectJrDataSystemUiVariant(option.key);
}}
activeOpacity={0.8}
>
<Text
style={[
styles.debugOptionTitle,
{
color: selected
? fixed.textOnPrimary
: colors.textPrimary,
},
]}
>
{option.label}
</Text>
<Text
style={[
styles.debugOptionCaption,
{
color: selected
? fixed.textOnPrimary
: colors.textTertiary,
},
]}
>
{option.caption}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
:{" "}
{
JR_DATA_SYSTEM_ENV_OPTIONS.find(
(option) => option.key === jrDataSystemEnv,
)?.baseUrl
}
</Text>
</View>
</>
)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
/* ── レイアウト ── */
container: {
flex: 1,
backgroundColor: "#0099CC",
},
content: {
flex: 1,
backgroundColor: "#f8f8fc",
},
contentInner: {
paddingHorizontal: 14,
paddingBottom: 40,
gap: 12,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
color: "#888",
letterSpacing: 0.5,
marginTop: 20,
marginLeft: 4,
},
/* ── アコーディオンカード ── */
accordionCard: {
backgroundColor: "#fff",
borderRadius: 14,
borderWidth: 1,
borderColor: "#e4e4e4",
overflow: "hidden",
},
accordionCardEnabled: {
borderColor: "#0099CC44",
},
accordionHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingTop: 14,
paddingBottom: 6,
gap: 10,
},
accordionLogo: {
width: 40,
height: 40,
borderRadius: 8,
flexShrink: 0,
},
accordionTitles: {
flex: 1,
gap: 2,
},
accordionTitle: {
fontSize: 15,
fontWeight: "bold",
color: "#111",
},
accordionTagline: {
fontSize: 11,
color: "#888",
},
accordionSwitch: {
flexShrink: 0,
},
accordionStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingHorizontal: 14,
paddingBottom: 10,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontSize: 12,
fontWeight: "500",
},
/* ── 展開トリガー ── */
accordionToggleRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
},
accordionToggleLabel: {
fontSize: 12,
color: "#666",
fontWeight: "500",
},
/* ── 展開コンテンツ ── */
accordionBody: {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
padding: 14,
gap: 10,
backgroundColor: "#fafafa",
},
bodyDesc: {
fontSize: 12,
color: "#444",
lineHeight: 19,
},
bodyFeatures: {
gap: 7,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
},
featureRow: {
flexDirection: "row",
alignItems: "flex-start",
gap: 6,
},
featureIcon: {
width: 22,
alignItems: "center",
paddingTop: 1,
flexShrink: 0,
},
featureLabel: {
fontSize: 12,
fontWeight: "bold",
color: "#333",
width: 62,
flexShrink: 0,
},
featureText: {
fontSize: 12,
color: "#555",
flex: 1,
lineHeight: 17,
},
bodyLink: {
flexDirection: "row",
alignItems: "center",
gap: 5,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
marginTop: 2,
},
bodyLinkText: {
fontSize: 12,
color: "#555",
},
/* ── 注意書き ── */
infoSection: {
backgroundColor: "#fff3cd",
borderRadius: 10,
padding: 14,
},
infoText: {
fontSize: 13,
color: "#856404",
lineHeight: 18,
},
debugSection: {
borderRadius: 12,
borderWidth: 1,
padding: 14,
gap: 10,
},
debugTitle: {
fontSize: 15,
fontWeight: "bold",
},
debugDescription: {
fontSize: 12,
lineHeight: 18,
},
debugOptionRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
},
debugOptionButton: {
minWidth: 96,
borderRadius: 10,
borderWidth: 1,
paddingHorizontal: 12,
paddingVertical: 10,
gap: 2,
},
debugOptionTitle: {
fontSize: 13,
fontWeight: "bold",
},
debugOptionCaption: {
fontSize: 10,
},
debugCurrentText: {
fontSize: 11,
lineHeight: 16,
},
});