@@ -627,6 +627,51 @@ export default function DashboardPage() {
627627 }
628628 return { targetMs : wakeupTiming . wakeMs } ;
629629 } , [ wakeupTiming ] ) ;
630+ const routineTimeline = useMemo ( ( ) => {
631+ const nowMs = Date . now ( ) ;
632+ if ( ! wakeupTiming ) {
633+ return morningRoutine . map ( ( item ) => ( {
634+ ...item ,
635+ startMs : null as number | null ,
636+ endMs : null as number | null ,
637+ status : "upcoming" as const ,
638+ } ) ) ;
639+ }
640+
641+ let cursorMs = wakeupTiming . wakeMs ;
642+ const withTime = morningRoutine . map ( ( item ) => {
643+ const durationMs = clampMinutes ( item . minutes ) * 60 * 1000 ;
644+ const startMs = cursorMs ;
645+ const endMs = cursorMs + durationMs ;
646+ cursorMs = endMs ;
647+
648+ return {
649+ ...item ,
650+ startMs,
651+ endMs,
652+ status : "upcoming" as const ,
653+ } ;
654+ } ) ;
655+
656+ const nextIndex = withTime . findIndex ( ( item ) => ( item . endMs ?? 0 ) > nowMs ) ;
657+ if ( nextIndex === - 1 ) {
658+ return withTime . map ( ( item ) => ( { ...item , status : "finished" as const } ) ) ;
659+ }
660+
661+ return withTime . map ( ( item , index ) => ( {
662+ ...item ,
663+ status :
664+ index < nextIndex
665+ ? ( "finished" as const )
666+ : index === nextIndex
667+ ? ( "next" as const )
668+ : ( "upcoming" as const ) ,
669+ } ) ) ;
670+ } , [ morningRoutine , wakeupTiming ] ) ;
671+ const nextRoutine = useMemo (
672+ ( ) => routineTimeline . find ( ( item ) => item . status === "next" ) ?? null ,
673+ [ routineTimeline ] ,
674+ ) ;
630675
631676 const startAlarmSound = useCallback ( async ( ) => {
632677 clearAlarmTimeout ( ) ;
@@ -927,7 +972,7 @@ export default function DashboardPage() {
927972 fontSize = { { base : "sm" , md : "md" } }
928973 letterSpacing = "0.08em"
929974 >
930- 出発まで
975+ 出発時刻
931976 </ Text >
932977 < Text
933978 fontSize = { { base : "5xl" , md : "7xl" } }
@@ -954,6 +999,7 @@ export default function DashboardPage() {
954999 </ Text >
9551000 </ Text >
9561001 < Text > 移動 { transitMinutes } 分</ Text >
1002+ < Text > 余裕 { Math . max ( 0 , slack ) } 分</ Text >
9571003 </ HStack >
9581004 < Text
9591005 color = "gray.500"
@@ -966,60 +1012,6 @@ export default function DashboardPage() {
9661012 </ Text >
9671013
9681014 < Stack gap = { 2 } mt = { 2 } >
969- < Text fontSize = "xs" color = "gray.600" >
970- 朝ルーティン(起床から出発まで)合計:{ " " }
971- { routineTotalMinutes } 分
972- </ Text >
973- { routineStatus === "loading" ? (
974- < Text fontSize = "xs" color = "gray.500" >
975- 朝ルーティンを読み込み中...
976- </ Text >
977- ) : null }
978-
979- < Stack gap = { 1 } >
980- { morningRoutine . map ( ( item ) => (
981- < HStack key = { item . id } gap = { 2 } align = "center" >
982- < Box
983- w = "7px"
984- h = "7px"
985- borderRadius = "full"
986- bg = "gray.400"
987- flexShrink = { 0 }
988- />
989- < Text
990- fontSize = "sm"
991- color = "gray.700"
992- overflow = "hidden"
993- textOverflow = "ellipsis"
994- whiteSpace = "nowrap"
995- >
996- { truncateText ( item . label , 22 ) }
997- </ Text >
998- < Text fontSize = "sm" color = "gray.500" ml = "auto" >
999- { clampMinutes ( item . minutes ) } 分
1000- </ Text >
1001- </ HStack >
1002- ) ) }
1003- </ Stack >
1004-
1005- < HStack gap = { 2 } flexWrap = "wrap" >
1006- < Button
1007- size = "xs"
1008- variant = "outline"
1009- colorPalette = "blue"
1010- onClick = { handleOpenRoutineEditor }
1011- disabled = { routineStatus === "loading" }
1012- >
1013- ルーティンを編集
1014- </ Button >
1015- </ HStack >
1016-
1017- { routineError ? (
1018- < Text fontSize = "xs" color = "orange.700" >
1019- { routineError }
1020- </ Text >
1021- ) : null }
1022-
10231015 < HStack gap = { 3 } flexWrap = "wrap" >
10241016 < Text
10251017 fontSize = { { base : "sm" , md : "md" } }
@@ -1105,48 +1097,154 @@ export default function DashboardPage() {
11051097 < GridItem colSpan = { { base : 1 , md : 1 , xl : 3 } } >
11061098 < Card minH = { { base : "160px" , md : "210px" } } >
11071099 < Text fontSize = "md" color = "gray.500" mb = { 1 } >
1108- 交通
1109- </ Text >
1110- < Text
1111- fontSize = { { base : "3xl" , md : "4xl" } }
1112- fontWeight = "semibold"
1113- color = "gray.800"
1114- >
1115- { transitMinutes }
1116- < Text
1117- as = "span"
1118- fontSize = "xl"
1119- color = "gray.500"
1120- fontWeight = "normal"
1121- >
1122- 分
1123- </ Text >
1100+ 朝ルーティン
11241101 </ Text >
1125- < Text
1126- color = "gray.700"
1127- fontSize = { { base : "md" , md : "lg" } }
1128- fontWeight = "semibold"
1129- mt = { 1 }
1130- overflow = "hidden"
1131- textOverflow = "ellipsis"
1132- whiteSpace = "nowrap"
1133- >
1134- { truncateText ( transitSummary , 22 ) }
1102+ < Text fontSize = "xs" color = "gray.600" >
1103+ 合計 { routineTotalMinutes } 分
11351104 </ Text >
1105+ { routineStatus === "loading" ? (
1106+ < Text mt = { 2 } color = "gray.500" fontSize = "sm" >
1107+ 朝ルーティンを読み込み中...
1108+ </ Text >
1109+ ) : (
1110+ < Stack gap = { 2 } mt = { 2 } >
1111+ { nextRoutine ? (
1112+ < Stack gap = { 0 } >
1113+ < Text
1114+ fontSize = { { base : "md" , md : "lg" } }
1115+ fontWeight = "semibold"
1116+ color = "gray.800"
1117+ lineHeight = { 1.3 }
1118+ overflow = "hidden"
1119+ textOverflow = "ellipsis"
1120+ whiteSpace = "nowrap"
1121+ >
1122+ 次: { truncateText ( nextRoutine . label , 16 ) }
1123+ </ Text >
1124+ { nextRoutine . startMs !== null &&
1125+ nextRoutine . endMs !== null ? (
1126+ < Text fontSize = "xs" color = "gray.500" >
1127+ { toJstHHmm (
1128+ new Date ( nextRoutine . startMs ) . toISOString ( ) ,
1129+ ) }
1130+ { " - " }
1131+ { toJstHHmm (
1132+ new Date ( nextRoutine . endMs ) . toISOString ( ) ,
1133+ ) }
1134+ </ Text >
1135+ ) : null }
1136+ </ Stack >
1137+ ) : (
1138+ < Text fontSize = "sm" color = "gray.500" >
1139+ 次のルーティンはありません
1140+ </ Text >
1141+ ) }
1142+
1143+ < Stack gap = { 1.5 } >
1144+ { routineTimeline . map ( ( item ) => (
1145+ < HStack key = { item . id } gap = { 2 } align = "start" >
1146+ < Box
1147+ mt = "6px"
1148+ w = "6px"
1149+ h = "6px"
1150+ borderRadius = "full"
1151+ bg = {
1152+ item . status === "finished"
1153+ ? "gray.300"
1154+ : item . status === "next"
1155+ ? "blue.500"
1156+ : "gray.500"
1157+ }
1158+ flexShrink = { 0 }
1159+ />
1160+ < Stack gap = { 0 } minW = { 0 } flex = "1" >
1161+ < Text
1162+ fontSize = {
1163+ item . status === "next"
1164+ ? { base : "sm" , md : "md" }
1165+ : "xs"
1166+ }
1167+ fontWeight = {
1168+ item . status === "next" ? "semibold" : "medium"
1169+ }
1170+ color = {
1171+ item . status === "finished"
1172+ ? "gray.400"
1173+ : item . status === "next"
1174+ ? "gray.800"
1175+ : "gray.600"
1176+ }
1177+ lineHeight = { 1.25 }
1178+ overflow = "hidden"
1179+ textOverflow = "ellipsis"
1180+ whiteSpace = "nowrap"
1181+ >
1182+ { truncateText ( item . label , 18 ) }
1183+ </ Text >
1184+ { item . startMs !== null && item . endMs !== null ? (
1185+ < Text
1186+ fontSize = "xs"
1187+ color = {
1188+ item . status === "finished"
1189+ ? "gray.400"
1190+ : "gray.500"
1191+ }
1192+ >
1193+ { toJstHHmm (
1194+ new Date ( item . startMs ) . toISOString ( ) ,
1195+ ) }
1196+ { " - " }
1197+ { toJstHHmm (
1198+ new Date ( item . endMs ) . toISOString ( ) ,
1199+ ) }
1200+ </ Text >
1201+ ) : null }
1202+ </ Stack >
1203+ < Text
1204+ fontSize = "xs"
1205+ color = {
1206+ item . status === "finished"
1207+ ? "gray.400"
1208+ : "gray.500"
1209+ }
1210+ >
1211+ { clampMinutes ( item . minutes ) } 分
1212+ </ Text >
1213+ </ HStack >
1214+ ) ) }
1215+ </ Stack >
1216+
1217+ < HStack gap = { 2 } flexWrap = "wrap" >
1218+ < Button
1219+ size = "xs"
1220+ variant = "outline"
1221+ colorPalette = "blue"
1222+ onClick = { handleOpenRoutineEditor }
1223+ >
1224+ ルーティンを編集
1225+ </ Button >
1226+ </ HStack >
1227+ </ Stack >
1228+ ) }
1229+ { routineError ? (
1230+ < Text fontSize = "xs" color = "orange.700" mt = { 2 } >
1231+ { routineError }
1232+ </ Text >
1233+ ) : null }
11361234 </ Card >
11371235 </ GridItem >
11381236
11391237 < GridItem colSpan = { { base : 1 , md : 1 , xl : 3 } } >
11401238 < Card minH = { { base : "160px" , md : "210px" } } >
11411239 < Text fontSize = "md" color = "gray.500" mb = { 1 } >
1142- 余裕
1240+ 交通
11431241 </ Text >
11441242 < Text
11451243 fontSize = { { base : "3xl" , md : "4xl" } }
11461244 fontWeight = "semibold"
11471245 color = "gray.800"
11481246 >
1149- { Math . max ( 0 , slack ) }
1247+ { transitMinutes }
11501248 < Text
11511249 as = "span"
11521250 fontSize = "xl"
@@ -1157,29 +1255,18 @@ export default function DashboardPage() {
11571255 </ Text >
11581256 </ Text >
11591257 < Text
1160- color = "gray.600 "
1258+ color = "gray.700 "
11611259 fontSize = { { base : "md" , md : "lg" } }
11621260 mt = { 1 }
11631261 overflow = "hidden"
11641262 textOverflow = "ellipsis"
11651263 whiteSpace = "nowrap"
11661264 >
1167- { departure } に出れば
1265+ { truncateText ( transitSummary , 22 ) }
1266+ </ Text >
1267+ < Text mt = { 1 } color = "gray.500" fontSize = "sm" >
1268+ 推奨出発 { departure }
11681269 </ Text >
1169- < Box
1170- mt = { 3 }
1171- h = "8px"
1172- bg = "gray.200"
1173- borderRadius = "full"
1174- overflow = "hidden"
1175- >
1176- < Box
1177- h = "full"
1178- borderRadius = "full"
1179- w = { `${ Math . min ( 100 , Math . max ( 0 , ( Math . max ( 0 , slack ) / 60 ) * 100 ) ) } %` }
1180- bg = { slack < 5 ? "red.400" : "green.500" }
1181- />
1182- </ Box >
11831270 </ Card >
11841271 </ GridItem >
11851272
0 commit comments