diff --git a/src/components/Tutorial.jsx b/src/components/Tutorial.jsx
new file mode 100644
index 0000000..be417f7
--- /dev/null
+++ b/src/components/Tutorial.jsx
@@ -0,0 +1,416 @@
+import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
+import { createPortal } from 'react-dom';
+import { ChevronRight, ArrowRight } from 'lucide-react';
+
+const Tutorial = ({
+ activeTab,
+ setActiveTab,
+ isTripEditing,
+ setIsTripEditing,
+ isLoginOpen,
+ setIsLoginOpen,
+ user,
+ pinMode
+}) => {
+ const [step, setStep] = useState(-1); // -1: Loading/Check, 0+: Steps
+ const [rect, setRect] = useState(null);
+ const [isVisible, setIsVisible] = useState(false);
+ const [tooltipStyle, setTooltipStyle] = useState({});
+
+ // Ref for the tooltip card to measure strict dimensions
+ const tooltipRef = useRef(null);
+
+ // Config
+ const STEPS = [
+ {
+ id: 'welcome',
+ target: null, // Center modal
+ title: "Welcome to RailLOOP",
+ content: "Welcome! Let's take a quick tour to help you get started with tracking your railway journeys.",
+ position: 'center',
+ action: 'next'
+ },
+ {
+ id: 'tab-records',
+ target: '#tab-btn-records',
+ title: "Your Journey Log",
+ content: "This is the Records tab. Here you can view and manage all your railway trips.",
+ position: 'top',
+ action: 'switch-tab',
+ tab: 'records'
+ },
+ {
+ id: 'add-trip',
+ target: '#btn-add-trip',
+ title: "Record a Trip",
+ content: "Click here to add a new trip. Let's try opening it now.",
+ position: 'top',
+ action: 'wait-interaction', // Wait for user to click
+ check: () => isTripEditing
+ },
+ {
+ id: 'editor-modes',
+ target: '#trip-editor-toggle-mode',
+ title: "Two Input Modes",
+ content: "You can manually enter details or use the 'Auto Plan' feature to search for routes.",
+ position: 'bottom',
+ action: 'next'
+ },
+ {
+ id: 'close-editor',
+ target: '#btn-close-editor',
+ title: "Close Editor",
+ content: "Let's close this for now to continue the tour.",
+ position: 'bottom',
+ action: 'wait-interaction',
+ check: () => !isTripEditing
+ },
+ {
+ id: 'import-export',
+ target: '#header-actions',
+ title: "Data Management",
+ content: "Here you can export your data (KML/JSON) or import backups. Keep your data safe!",
+ position: 'bottom',
+ action: 'next'
+ },
+ {
+ id: 'tab-map',
+ target: '#tab-btn-map',
+ title: "Map View",
+ content: "Switch to the Map tab to see your travels on the map.",
+ position: 'top',
+ action: 'switch-tab',
+ tab: 'map'
+ },
+ {
+ id: 'map-pins',
+ target: '#btn-pins-fab',
+ title: "Map Pins",
+ content: "Use this button to add pins (photos or comments) to the map.",
+ position: 'right', // FAB is bottom-left
+ action: 'wait-interaction',
+ check: () => pinMode && pinMode !== 'idle'
+ },
+ {
+ id: 'map-layers',
+ target: '.leaflet-control-layers',
+ title: "Switch Map Style",
+ content: "Toggle between Dark and Light map themes here.",
+ position: 'left', // Top-right control
+ action: 'next'
+ },
+ {
+ id: 'tab-stats',
+ target: '#tab-btn-stats',
+ title: "Statistics",
+ content: "Finally, check your travel statistics here.",
+ position: 'top',
+ action: 'wait-click-tab', // Specifically asked to guide click
+ tab: 'stats',
+ check: () => activeTab === 'stats'
+ },
+ {
+ id: 'stats-content',
+ target: '#stats-view-content',
+ title: "Your Achievements",
+ content: "View your total distance, cost, and most frequented lines here.",
+ position: 'center', // It's a large area, maybe just highlight without blocking? Or center modal over it? Let's try target.
+ action: 'next'
+ },
+ {
+ id: 'finish-login',
+ target: null,
+ title: "Login & User Guide",
+ content: "That's it! We'll now open the Login screen where you can read the full User Guide.",
+ position: 'center',
+ action: 'finish'
+ },
+ {
+ id: 'login-guide',
+ target: '#login-readme-container',
+ title: "Read the Guide",
+ content: "Please take a moment to read the User Guide / Agreement here.",
+ position: 'left',
+ action: 'end'
+ }
+ ];
+
+ // Initialization check
+ useEffect(() => {
+ const skipped = localStorage.getItem('rail_tutorial_skipped');
+ if (skipped === 'true' || user) {
+ setStep(-2); // Skipped
+ return;
+ }
+ setStep(0);
+ setIsVisible(true);
+ }, [user]);
+
+ // Step Transition Logic & Rect Calculation
+ useEffect(() => {
+ if (step < 0 || step >= STEPS.length) return;
+
+ const currentStep = STEPS[step];
+
+ // 1. Target Resolution
+ const updateRect = () => {
+ if (!currentStep.target) {
+ setRect(null);
+ setTooltipStyle({
+ position: 'fixed',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ opacity: 1,
+ transition: 'all 0.5s cubic-bezier(0.25, 1, 0.5, 1)'
+ });
+ return;
+ }
+ const el = document.querySelector(currentStep.target);
+ if (el) {
+ const r = el.getBoundingClientRect();
+
+ // Add padding to highlight box
+ const highlight = {
+ top: r.top - 5,
+ left: r.left - 5,
+ width: r.width + 10,
+ height: r.height + 10
+ };
+ setRect(highlight);
+ } else {
+ // Element not found? Retry in a bit (maybe animation or mounting)
+ setRect(null);
+ }
+ };
+
+ // Initial update
+ updateRect();
+ // Poll for rect changes (animations)
+ const interval = setInterval(updateRect, 100);
+
+ // 2. Auto-actions (Switch Tab)
+ if (currentStep.action === 'switch-tab' && currentStep.tab && activeTab !== currentStep.tab) {
+ setActiveTab(currentStep.tab);
+ }
+
+ // 3. Wait Conditions
+ let checkInterval;
+ if (currentStep.check) {
+ checkInterval = setInterval(() => {
+ if (currentStep.check()) {
+ handleNext();
+ }
+ }, 200);
+ }
+
+ return () => {
+ clearInterval(interval);
+ if (checkInterval) clearInterval(checkInterval);
+ };
+ }, [step, activeTab, isTripEditing, isLoginOpen, pinMode]);
+
+ // Strict Positioning Logic
+ const calculatePosition = () => {
+ if (step < 0 || step >= STEPS.length || !rect || !tooltipRef.current) return;
+
+ const currentStep = STEPS[step];
+ const PADDING = 20;
+
+ // Measure ACTUAL tooltip dimensions
+ const CARD_W = tooltipRef.current.offsetWidth || 384;
+ const CARD_H = tooltipRef.current.offsetHeight || 250;
+
+ // Viewport Dimensions
+ const winH = window.visualViewport ? window.visualViewport.height : window.innerHeight;
+ const winW = window.visualViewport ? window.visualViewport.width : window.innerWidth;
+
+ // Center of the target element
+ const targetCenterX = rect.left + rect.width / 2;
+ const targetCenterY = rect.top + rect.height / 2;
+
+ let top = 0;
+ let left = 0;
+ let pos = currentStep.position;
+
+ // Helper to check collision
+ const checkBounds = (t, l) => {
+ return (t >= PADDING && l >= PADDING && (t + CARD_H) <= (winH - PADDING) && (l + CARD_W) <= (winW - PADDING));
+ };
+
+ // 1. Determine Initial Position & Flip if needed
+ const getCoords = (p) => {
+ let t, l;
+ if (p === 'bottom') {
+ t = rect.top + rect.height + 20;
+ l = targetCenterX - (CARD_W / 2);
+ } else if (p === 'top') {
+ t = rect.top - CARD_H - 20;
+ l = targetCenterX - (CARD_W / 2);
+ } else if (p === 'right') {
+ t = targetCenterY - (CARD_H / 2);
+ l = rect.left + rect.width + 20;
+ } else if (p === 'left') {
+ t = targetCenterY - (CARD_H / 2);
+ l = rect.left - CARD_W - 20;
+ } else { // center
+ t = winH / 2 - CARD_H / 2;
+ l = winW / 2 - CARD_W / 2;
+ }
+ return { t, l };
+ };
+
+ let coords = getCoords(pos);
+
+ // Smart Flip
+ if (!checkBounds(coords.t, coords.l) && pos !== 'center') {
+ const opposites = { 'top': 'bottom', 'bottom': 'top', 'left': 'right', 'right': 'left' };
+ const altPos = opposites[pos];
+ if (altPos) {
+ const altCoords = getCoords(altPos);
+ // If alternative is better (or valid), take it
+ // Simple heuristic: check if alt fits vertically for top/bottom swap
+ if (pos === 'top' || pos === 'bottom') {
+ if (altCoords.t >= PADDING && (altCoords.t + CARD_H) <= (winH - PADDING)) {
+ coords = altCoords;
+ pos = altPos;
+ }
+ }
+ // Check if alt fits horizontally for left/right swap
+ if (pos === 'left' || pos === 'right') {
+ if (altCoords.l >= PADDING && (altCoords.l + CARD_W) <= (winW - PADDING)) {
+ coords = altCoords;
+ pos = altPos;
+ }
+ }
+ }
+ }
+
+ // 2. Strict Clamping (The "Aggressive" Part)
+ // Ensure card never goes off screen, even if it de-centers from target
+ top = Math.max(PADDING, Math.min(coords.t, winH - CARD_H - PADDING));
+ left = Math.max(PADDING, Math.min(coords.l, winW - CARD_W - PADDING));
+
+ setTooltipStyle({
+ position: 'fixed',
+ top: `${top}px`,
+ left: `${left}px`,
+ transform: 'none',
+ opacity: 1,
+ transition: 'all 0.4s cubic-bezier(0.25, 1, 0.5, 1)' // Smooth transitions
+ });
+ };
+
+ useLayoutEffect(() => {
+ calculatePosition();
+ }, [rect, step]);
+
+ // ResizeObserver to handle content size changes
+ useEffect(() => {
+ if (!tooltipRef.current) return;
+ const observer = new ResizeObserver(() => {
+ calculatePosition();
+ });
+ observer.observe(tooltipRef.current);
+ return () => observer.disconnect();
+ }, [rect, step]);
+
+ const handleNext = () => {
+ if (step === STEPS.length - 2) { // Finish step
+ // Open Login Modal
+ setIsLoginOpen(true);
+ // Wait a bit for modal to open then next
+ setTimeout(() => setStep(s => s + 1), 500);
+ } else if (step >= STEPS.length - 1) {
+ // End
+ setStep(-2);
+ setIsVisible(false);
+ } else {
+ setStep(s => s + 1);
+ }
+ };
+
+ const handleSkip = (dontShowAgain) => {
+ if (dontShowAgain) {
+ localStorage.setItem('rail_tutorial_skipped', 'true');
+ }
+ setStep(-2);
+ setIsVisible(false);
+ };
+
+ if (!isVisible || step < 0) return null;
+
+ const currentStep = STEPS[step];
+ const isInteractionStep = currentStep.action === 'wait-interaction' || currentStep.action === 'wait-click-tab';
+
+ // Render
+ return createPortal(
+
+ {/* Mask / Spotlight */}
+ {rect ? (
+
+ ) : (
+ // Full overlay if no target (Welcome step)
+
+ )}
+
+ {/* Tooltip / Card */}
+
+
+
+
{currentStep.title}
+ {step + 1} / {STEPS.length}
+
+
+ {currentStep.content}
+
+
+
+
+ {step === 0 && (
+
+ )}
+ {step > 0 && (
+
+ )}
+
+
+ {!isInteractionStep && (
+
+ )}
+ {isInteractionStep && (
+
+ Please interact to continue...
+
+ )}
+
+
+
+
+ , document.body);
+};
+
+export default Tutorial;
diff --git a/verify_tutorial_logic.js b/verify_tutorial_logic.js
new file mode 100644
index 0000000..861833c
--- /dev/null
+++ b/verify_tutorial_logic.js
@@ -0,0 +1,40 @@
+
+const fs = require('fs');
+const path = require('path');
+
+const tutorialPath = path.join('src', 'components', 'Tutorial.jsx');
+
+console.log("Verifying Tutorial Logic...");
+
+if (!fs.existsSync(tutorialPath)) {
+ console.error("Tutorial.jsx not found");
+ process.exit(1);
+}
+
+const content = fs.readFileSync(tutorialPath, 'utf-8');
+
+// 1. Check for visualViewport usage
+if (content.includes('window.visualViewport.height')) {
+ console.log("[OK] visualViewport usage found");
+} else {
+ console.error("[FAIL] visualViewport usage missing");
+ process.exit(1);
+}
+
+// 2. Check for ResizeObserver usage
+if (content.includes('new ResizeObserver')) {
+ console.log("[OK] ResizeObserver usage found");
+} else {
+ console.error("[FAIL] ResizeObserver usage missing");
+ process.exit(1);
+}
+
+// 3. Check for map-pins interactive mode
+if (content.includes('id: \'map-pins\'') && content.includes('action: \'wait-interaction\'') && content.includes('pinMode')) {
+ console.log("[OK] Map pins interactive mode configured");
+} else {
+ console.error("[FAIL] Map pins configuration incorrect");
+ process.exit(1);
+}
+
+console.log("Verification Successful.");
diff --git a/verify_tutorial_refinement.js b/verify_tutorial_refinement.js
new file mode 100644
index 0000000..c62b23c
--- /dev/null
+++ b/verify_tutorial_refinement.js
@@ -0,0 +1,40 @@
+
+const fs = require('fs');
+const path = require('path');
+
+const tutorialPath = path.join('src', 'components', 'Tutorial.jsx');
+
+console.log("Verifying Tutorial Refinements...");
+
+if (!fs.existsSync(tutorialPath)) {
+ console.error("Tutorial.jsx not found");
+ process.exit(1);
+}
+
+const content = fs.readFileSync(tutorialPath, 'utf-8');
+
+// 1. Check for tooltipStyle usage
+if (content.includes('style={tooltipStyle}')) {
+ console.log("[OK] Dynamic style usage found");
+} else {
+ console.error("[FAIL] Dynamic style usage missing");
+ process.exit(1);
+}
+
+// 2. Check for Math.max clamping logic
+if (content.includes('Math.max(PADDING, Math.min')) {
+ console.log("[OK] Strict Clamping logic found");
+} else {
+ console.error("[FAIL] Strict Clamping logic missing");
+ process.exit(1);
+}
+
+// 3. Check for Smart Flip logic
+if (content.includes('const opposites = { \'top\': \'bottom\'')) {
+ console.log("[OK] Smart Flip logic found");
+} else {
+ console.error("[FAIL] Smart Flip logic missing");
+ process.exit(1);
+}
+
+console.log("Verification Successful.");
diff --git a/verify_tutorial_structure.js b/verify_tutorial_structure.js
new file mode 100644
index 0000000..ebf3adb
--- /dev/null
+++ b/verify_tutorial_structure.js
@@ -0,0 +1,53 @@
+
+const fs = require('fs');
+const path = require('path');
+
+const railRoundPath = path.join('src', 'RailRound.jsx');
+const loginModalPath = path.join('src', 'components', 'LoginModal.jsx');
+const tutorialPath = path.join('src', 'components', 'Tutorial.jsx');
+
+function checkFileExists(p) {
+ if (fs.existsSync(p)) {
+ console.log(`[OK] ${p} exists.`);
+ } else {
+ console.error(`[FAIL] ${p} does not exist.`);
+ process.exit(1);
+ }
+}
+
+function checkContent(p, searchString) {
+ const content = fs.readFileSync(p, 'utf-8');
+ if (content.includes(searchString)) {
+ console.log(`[OK] Found "${searchString}" in ${p}`);
+ } else {
+ console.error(`[FAIL] Could not find "${searchString}" in ${p}`);
+ process.exit(1);
+ }
+}
+
+console.log("Verifying Tutorial Implementation...");
+
+// 1. Check Files
+checkFileExists(railRoundPath);
+checkFileExists(loginModalPath);
+checkFileExists(tutorialPath);
+
+// 2. Check RailRound IDs and Import
+checkContent(railRoundPath, 'import Tutorial from \'./components/Tutorial\'');
+checkContent(railRoundPath, '