diff --git a/android/app/build.gradle b/android/app/build.gradle index ed1667c596..8cbd752227 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -143,12 +143,12 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.dev" versionNameSuffix "-dev" - versionCode 100000536 + versionCode 100000537 versionName "10.9.1" } prod { dimension "environment" - versionCode 100000536 + versionCode 100000537 versionName "10.9.1" } } diff --git a/app.config.ts b/app.config.ts index 07eaec2b68..2f96cc0a9a 100644 --- a/app.config.ts +++ b/app.config.ts @@ -52,7 +52,7 @@ export default { }, }, ios: { - buildNumber: '2749', + buildNumber: '2750', scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD', bundleIdentifier: IS_DEV ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', supportsTablet: true, @@ -60,7 +60,7 @@ export default { android: { package: IS_DEV ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', permissions: [], - versionCode: 100000536, + versionCode: 100000537, }, owner: 'trainlcd', experiments: { diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj index ce88270189..4bd6cadf2c 100644 --- a/ios/TrainLCD.xcodeproj/project.pbxproj +++ b/ios/TrainLCD.xcodeproj/project.pbxproj @@ -2407,7 +2407,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; @@ -2446,7 +2446,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TrainLCD; @@ -2505,7 +2505,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2611,7 +2611,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2690,7 +2690,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2729,7 +2729,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2940,7 +2940,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -2991,7 +2991,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3042,7 +3042,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3100,7 +3100,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3151,7 +3151,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3208,7 +3208,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3256,7 +3256,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3307,7 +3307,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3526,7 +3526,7 @@ CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3582,7 +3582,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3632,7 +3632,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3690,7 +3690,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2749; + CURRENT_PROJECT_VERSION = 2750; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.test.tsx b/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.test.tsx index fbafea3650..eeb4d5775a 100644 --- a/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.test.tsx +++ b/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.test.tsx @@ -1,7 +1,11 @@ import { render } from '@testing-library/react-native'; import { createStore, Provider } from 'jotai'; +import type { AvailableLanguage } from '~/constants'; import type { HeaderTransitionState } from '~/models/HeaderTransitionState'; -import { headerStateAtom } from '~/store/atoms/navigation'; +import { + enabledLanguagesAtom, + headerStateAtom, +} from '~/store/atoms/navigation'; import { EstimatedMinutesUnitLabel } from './EstimatedMinutesUnitLabel'; jest.mock('~/utils/isTablet', () => ({ @@ -9,9 +13,15 @@ jest.mock('~/utils/isTablet', () => ({ default: false, })); -const renderWithState = (headerState: HeaderTransitionState) => { +const renderWithState = ( + headerState: HeaderTransitionState, + enabledLanguages?: AvailableLanguage[] +) => { const store = createStore(); store.set(headerStateAtom, headerState); + if (enabledLanguages) { + store.set(enabledLanguagesAtom, enabledLanguages); + } return render( @@ -42,13 +52,18 @@ describe('EstimatedMinutesUnitLabel', () => { expect(getByText('min.')).toBeTruthy(); }); - it('中国語Stateでは「分」を表示する', () => { - const { getByText } = renderWithState('CURRENT_ZH'); + it('中国語Stateでは駅名の英語表示に追従して「min.」を表示する', () => { + const { getByText } = renderWithState('CURRENT_ZH', ['JA', 'EN', 'ZH']); + expect(getByText('min.')).toBeTruthy(); + }); + + it('中国語StateでもENが無効なら「分」を表示する', () => { + const { getByText } = renderWithState('CURRENT_ZH', ['JA', 'ZH']); expect(getByText('分')).toBeTruthy(); }); - it('韓国語Stateでは「분」を表示する', () => { + it('韓国語Stateでは「分」を表示する(韓国語専用表記は持たない)', () => { const { getByText } = renderWithState('CURRENT_KO'); - expect(getByText('분')).toBeTruthy(); + expect(getByText('分')).toBeTruthy(); }); }); diff --git a/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.tsx b/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.tsx index 599115dc35..975b81772e 100644 --- a/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.tsx +++ b/src/components/LineBoard/shared/components/EstimatedMinutesUnitLabel.tsx @@ -1,7 +1,7 @@ -import { atom, useAtomValue } from 'jotai'; +import { useAtomValue } from 'jotai'; import type React from 'react'; import { StyleSheet, type TextStyle } from 'react-native'; -import { headerStateAtom } from '~/store/atoms/navigation'; +import { isEnAtom } from '~/store/selectors/isEn'; import isTablet from '~/utils/isTablet'; import Typography from '../../../Typography'; @@ -10,6 +10,10 @@ const styles = StyleSheet.create({ color: '#fff', fontWeight: 'bold', fontSize: isTablet ? 21 : 15, + // Yogaはwidth未指定の絶対配置子を親の幅を上限に測るため、幅の狭い + // ドットに載せると最長表記の「min.」が途中で切り詰められてしまう。 + // 最長表記が縁取り込みで収まる固有幅を明示して親幅の影響を断つ + width: isTablet ? 56 : 40, // 路線色バーの上に重ねて描画されるため、どの路線色でも判読できるよう縁取る textShadowColor: '#000', textShadowOffset: { width: 0, height: 0 }, @@ -17,36 +21,21 @@ const styles = StyleSheet.create({ }, }); -// ヘッダーの言語Stateごとの単位表記。headerStateAtomは数秒ごとに -// ローテーションするため、算出済み文字列のderived atomを購読することで -// 単位が実際に変わったときだけ再レンダーされるようにする。 -const estimatedMinutesUnitAtom = atom((get) => { - const langState = get(headerStateAtom).split('_')[1] ?? 'JA'; - switch (langState) { - case 'EN': - return 'min.'; - case 'KO': - return '분'; - // JA・KANA・ZH (中国語も単位は「分」で通じるため共通) - default: - return '分'; - } -}); - export type EstimatedMinutesUnitLabelProps = { style?: TextStyle; }; // ETAの残り分数はドット内に数字のみで表示されるため、最後のドットの右隣に -// 単位を添えて数字の意味を示す。 +// 単位を添えて数字の意味を示す。表記は駅名表示と同じ言語切替(isEnAtom)に +// 追従した「分/min.」の二択で、中国語・韓国語の専用表記は持たない export const EstimatedMinutesUnitLabel: React.FC< EstimatedMinutesUnitLabelProps > = ({ style }) => { - const unit = useAtomValue(estimatedMinutesUnitAtom); + const isEn = useAtomValue(isEnAtom); return ( - {unit} + {isEn ? 'min.' : '分'} ); }; diff --git a/src/components/LineBoard/shared/styles/commonStyles.ts b/src/components/LineBoard/shared/styles/commonStyles.ts index 7ac6d98638..b99d54ed71 100644 --- a/src/components/LineBoard/shared/styles/commonStyles.ts +++ b/src/components/LineBoard/shared/styles/commonStyles.ts @@ -6,6 +6,7 @@ import { RFValue } from '~/utils/rfValue'; // Shared bar positioning constants export const BAR_BOTTOM_WEST = isTablet ? 32 : 48; export const BAR_BOTTOM_JO = isTablet ? 32 : 48; +export const BAR_HEIGHT_JO = isTablet ? 64 : 40; export const BAR_TERMINAL_BOTTOM_JO = isTablet ? 48 : 58; // Androidタブレットでは駅名がヘッダーに食い込むのを防ぐためbottomオフセットを縮小する @@ -43,7 +44,7 @@ export const commonLineBoardStyles = StyleSheet.create({ barJO: { position: 'absolute', bottom: BAR_BOTTOM_JO, - height: isTablet ? 64 : 40, + height: BAR_HEIGHT_JO, }, barTerminal: { position: 'absolute', diff --git a/src/components/LineBoardJO.test.tsx b/src/components/LineBoardJO.test.tsx index f26dbd04ea..e3cc400f36 100644 --- a/src/components/LineBoardJO.test.tsx +++ b/src/components/LineBoardJO.test.tsx @@ -1,7 +1,15 @@ import { render } from '@testing-library/react-native'; +import { StyleSheet, type ViewStyle } from 'react-native'; import type { Line, Station } from '~/@types/graphql'; import LineBoardJO from './LineBoardJO'; +// react-test-rendererの型定義が未導入のため、toJSON()の構造を最小限に表す +type RenderedJSONNode = { + type: string; + props: Record; + children: (RenderedJSONNode | string)[] | null; +}; + // モック設定 jest.mock('jotai', () => ({ useAtomValue: jest.fn(), @@ -294,6 +302,89 @@ describe('LineBoardJO', () => { expect(EstimatedMinutesUnitLabel).not.toHaveBeenCalled(); }); + describe('レイアウト計算(スマホ)', () => { + // useLandscapeWindowDimensionsモックのwidth=812に対応するバー1セグメント幅 + const BAR_WIDTH = (812 - 96) / 7.835; + + const collectNodes = ( + node: RenderedJSONNode | string | (RenderedJSONNode | string)[] | null + ): RenderedJSONNode[] => { + if (!node || typeof node === 'string') { + return []; + } + if (Array.isArray(node)) { + return node.flatMap((n) => collectNodes(n)); + } + return [node, ...collectNodes(node.children)]; + }; + + const renderAndFlattenStyles = () => { + const result = render( + + ); + return collectNodes( + result.toJSON() as RenderedJSONNode | RenderedJSONNode[] | null + ).map((node) => StyleSheet.flatten(node.props.style as ViewStyle) ?? {}); + }; + + it('未通過駅のドットはバー高さ-8pxの正方形としてバー内の縦中央に配置される', () => { + const styles = renderAndFlattenStyles(); + const dots = styles.filter((s) => s.borderRadius === 32); + // バー高さ40 - 8 = 32px。現在駅(index 0)以外の7個が未通過ドット + const futureDots = dots.filter((s) => s.width === 32); + expect(futureDots).toHaveLength(7); + for (const dot of futureDots) { + expect(dot.height).toBe(32); + expect(dot.bottom).toBe(48 + 4); // BAR_BOTTOM_JO + 上下4pxインセット + } + // 現在駅の16pxドットは従来どおりバーの縦中央 + const currentDots = dots.filter((s) => s.width === 16); + expect(currentDots).toHaveLength(1); + expect(currentDots[0].bottom).toBe(48 + 12); + }); + + it('最終バーセグメントと終端矢印が延長分(+14px)だけ右へ伸びる', () => { + const styles = renderAndFlattenStyles(); + const segmentWidths = styles + .filter( + (s) => + s.height === 40 && s.bottom === 48 && typeof s.width === 'number' + ) + .map((s) => s.width as number) + .sort((a, b) => a - b); + expect(segmentWidths).toHaveLength(8); + expect(segmentWidths[6]).toBeCloseTo(BAR_WIDTH + 1); // 通常セグメント + expect(segmentWidths[7]).toBeCloseTo(BAR_WIDTH + 1 + 14); // 最終のみ延長 + const terminal = styles.find( + (s) => s.borderLeftWidth === 20 && s.borderBottomWidth === 20 + ); + expect(terminal?.left).toBeCloseTo(BAR_WIDTH * 8 - 10 + 14); + }); + + it('単位ラベルコンテナが最終ドットの右端に隣接して縦中央配置される', () => { + const { useEstimatedMinutesByStationId } = require('~/hooks'); + useEstimatedMinutesByStationId.mockReturnValueOnce(new Map([[2, 5]])); + const result = render( + + ); + const unitContainer = collectNodes( + result.toJSON() as RenderedJSONNode | RenderedJSONNode[] | null + ).find((node) => node.props.pointerEvents === 'none'); + expect(unitContainer).toBeTruthy(); + const style = StyleSheet.flatten(unitContainer?.props.style as ViewStyle); + expect(style.position).toBe('absolute'); + expect(style.left).toBe(32); // ドット幅32px + 間隔0px + expect(style.top).toBe(0); + expect(style.height).toBe(32); // ドットと同じ高さで縦中央揃え + }); + }); + it('通過駅の場合、PassChevronEastが表示される', () => { const getIsPass = require('~/utils/isPass').default; getIsPass.mockReturnValue(true); diff --git a/src/components/LineBoardJO.tsx b/src/components/LineBoardJO.tsx index 42b2abc4a9..8c23c872aa 100644 --- a/src/components/LineBoardJO.tsx +++ b/src/components/LineBoardJO.tsx @@ -28,6 +28,7 @@ import { import { useIncludesLongStationName } from './LineBoard/shared/hooks/useBarStyles'; import { BAR_BOTTOM_JO, + BAR_HEIGHT_JO, commonLineBoardStyles, } from './LineBoard/shared/styles/commonStyles'; import NumberingIcon from './NumberingIcon'; @@ -45,6 +46,14 @@ const useBarWidth = () => { return isTablet ? (dim.width - 120) / 8 : (dim.width - 96) / 7.835; }; +// 旧来のドットサイズ。getLeftはこのサイズのドットの左端位置を前提としている +// ため、拡大後の中心合わせ補正の基準として残している +const LEGACY_DOT_SIZE = 32; +// 未通過駅のドットはバーの高さから上下4pxずつ控えたサイズ(高さ100%-8px)の正方形 +const DOT_SIZE_JO = BAR_HEIGHT_JO - 8; +// 終端矢印が画面右端から離れすぎないよう、最終セグメントを右へ延長する +const BAR_TAIL_EXTENSION_JO = isTablet ? 24 : 14; + // Local style overrides specific to JO const localStyles = StyleSheet.create({ root: { @@ -53,12 +62,13 @@ const localStyles = StyleSheet.create({ bottom: isTablet ? '40%' : undefined, marginLeft: isTablet ? 48 : 32, }, - // barDot(未通過時は32px)の右にドットと同じ高さで縦中央揃え + // 最終ドットの右にドットと同じ高さで縦中央揃え。 + // ラベル自体が固有幅を持つため、ドット幅によるYogaの切り詰めは受けない estimatedMinutesUnitContainer: { position: 'absolute', top: 0, - left: 32 + 24, - height: 32, + left: DOT_SIZE_JO + (isTablet ? 8 : 0), + height: DOT_SIZE_JO, justifyContent: 'center', }, }); @@ -299,14 +309,12 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { const getBottom = useCallback( (index: number) => { - if (isTablet) { - return index <= currentStationIndex - ? BAR_BOTTOM_JO + 24 - : BAR_BOTTOM_JO + 16; + // 通過済み・現在駅の16pxドットはバーの縦中央に揃える + if (index <= currentStationIndex) { + return isTablet ? BAR_BOTTOM_JO + 24 : BAR_BOTTOM_JO + 12; } - return index <= currentStationIndex - ? BAR_BOTTOM_JO + 12 - : BAR_BOTTOM_JO + 5; + // 未通過駅のドット(バー高さ-8px)は上下4pxずつ控えてバー内に収める + return BAR_BOTTOM_JO + (BAR_HEIGHT_JO - DOT_SIZE_JO) / 2; }, [currentStationIndex] ); @@ -317,12 +325,20 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { return ( - {[...lineColors, ...emptyArray].map((lc, i) => { + {[...lineColors, ...emptyArray].map((lc, i, segments) => { const stationId = stations[i]?.id; const estimatedMinutes = stationId != null && i > currentStationIndex ? estimatedMinutesByStationId.get(stationId) : null; + const isLastSegment = i === segments.length - 1; + const dotSize = i <= currentStationIndex ? 16 : DOT_SIZE_JO; + // getLeftは従来の32pxドットの左端位置を返すため、拡大分を左右へ + // 均等に配分して従来と中心位置を揃える(16pxドットは従来位置のまま) + const dotLeft = + i <= currentStationIndex + ? getLeft(i) + : getLeft(i) - (DOT_SIZE_JO - LEGACY_DOT_SIZE) / 2; return ( @@ -335,7 +351,8 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { // 揃わず、丸めの具合で白い継ぎ目が出ることがある。右へ1px // 食み出させて隣接セグメントを重ね、継ぎ目が出ないようにする // (後続セグメントが上に描画されるため境界位置は変わらない)。 - width: barWidth + 1, + width: + barWidth + 1 + (isLastSegment ? BAR_TAIL_EXTENSION_JO : 0), left: barWidth * i, backgroundColor: (() => { if (i <= currentStationIndex) { @@ -379,10 +396,10 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { style={[ styles.barDotJO, { - left: getLeft(i), + left: dotLeft, bottom: getBottom(i), - width: i <= currentStationIndex ? 16 : 32, - height: i <= currentStationIndex ? 16 : 32, + width: dotSize, + height: dotSize, }, ]} > @@ -395,10 +412,10 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { { backgroundColor: stations.length <= i ? 'transparent' : 'white', - left: getLeft(i), + left: dotLeft, bottom: getBottom(i), - width: i <= currentStationIndex ? 16 : 32, - height: i <= currentStationIndex ? 16 : 32, + width: dotSize, + height: dotSize, justifyContent: 'center', alignItems: 'center', }, @@ -451,7 +468,9 @@ const LineBoardJO: React.FC = ({ stations, lineColors }: Props) => { borderBottomColor: line.color ? lineColors.at(-1) || line.color : '#000', - left: isTablet ? barWidth * 8 - 16 : barWidth * 8 - 10, + left: + (isTablet ? barWidth * 8 - 16 : barWidth * 8 - 10) + + BAR_TAIL_EXTENSION_JO, }, ]} />