diff options
Diffstat (limited to 'src/frontend')
| -rw-r--r-- | src/frontend/app/i18n/locales/en-GB.json | 11 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/es-ES.json | 11 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/gl-ES.json | 11 | ||||
| -rw-r--r-- | src/frontend/app/routes/timetable-$id.css | 75 | ||||
| -rw-r--r-- | src/frontend/app/routes/timetable-$id.tsx | 275 | ||||
| -rw-r--r-- | src/frontend/package-lock.json | 21 |
6 files changed, 319 insertions, 85 deletions
diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json index 12c8121..992a311 100644 --- a/src/frontend/app/i18n/locales/en-GB.json +++ b/src/frontend/app/i18n/locales/en-GB.json @@ -9,9 +9,9 @@ "data_source_middle": "under license", "settings": "Settings", "theme": "Mode:", - "theme_light": "Light", - "theme_dark": "Dark", - "theme_system": "System", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_system": "System", "table_style": "Table style:", "table_style_regular": "Show in order", "table_style_grouped": "Group by line", @@ -79,7 +79,10 @@ "loadError": "Error loading timetables", "errorDetail": "Theoretical timetables are updated daily. Please try again later.", "showPast": "Show all", - "hidePast": "Hide past" + "hidePast": "Hide past", + "goToNow": "Go to now", + "scrollUp": "Scroll up", + "scrollDown": "Scroll down" }, "map": { "popup_title": "Stop", diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json index e929382..1df399d 100644 --- a/src/frontend/app/i18n/locales/es-ES.json +++ b/src/frontend/app/i18n/locales/es-ES.json @@ -9,9 +9,9 @@ "data_source_middle": "bajo licencia", "settings": "Ajustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Oscuro", - "theme_system": "Sistema", + "theme_light": "Claro", + "theme_dark": "Oscuro", + "theme_system": "Sistema", "table_style": "Estilo de tabla:", "table_style_regular": "Mostrar por orden", "table_style_grouped": "Agrupar por línea", @@ -79,7 +79,10 @@ "loadError": "Error al cargar los horarios", "errorDetail": "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.", "showPast": "Mostrar todos", - "hidePast": "Ocultar pasados" + "hidePast": "Ocultar pasados", + "goToNow": "Ir a ahora", + "scrollUp": "Subir", + "scrollDown": "Bajar" }, "map": { "popup_title": "Parada", diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json index ed1ef53..5a04aff 100644 --- a/src/frontend/app/i18n/locales/gl-ES.json +++ b/src/frontend/app/i18n/locales/gl-ES.json @@ -9,9 +9,9 @@ "data_source_middle": "baixo licenza", "settings": "Axustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Escuro", - "theme_system": "Sistema", + "theme_light": "Claro", + "theme_dark": "Escuro", + "theme_system": "Sistema", "table_style": "Estilo de táboa:", "table_style_regular": "Mostrar por orde", "table_style_grouped": "Agrupar por liña", @@ -79,7 +79,10 @@ "loadError": "Erro ao cargar os horarios", "errorDetail": "Os horarios teóricos actualízanse diariamente. Inténtao máis tarde.", "showPast": "Mostrar todos", - "hidePast": "Ocultar pasados" + "hidePast": "Ocultar pasados", + "goToNow": "Ir a agora", + "scrollUp": "Subir", + "scrollDown": "Baixar" }, "map": { "popup_title": "Parada", diff --git a/src/frontend/app/routes/timetable-$id.css b/src/frontend/app/routes/timetable-$id.css index c834633..b4e231a 100644 --- a/src/frontend/app/routes/timetable-$id.css +++ b/src/frontend/app/routes/timetable-$id.css @@ -38,6 +38,10 @@ .timetable-full-content { margin-top: 1rem; + overflow-y: auto; + max-height: calc(100vh - 250px); + position: relative; + padding-bottom: 80px; /* Space for FAB */ } .error-message { @@ -149,3 +153,74 @@ padding: 1rem; } } + +/* Floating Action Button */ +.fab-container { + position: fixed; + bottom: 80px; + right: 20px; + display: flex; + flex-direction: column; + gap: 12px; + z-index: 1000; +} + +.fab { + width: 56px; + height: 56px; + border-radius: 50%; + border: none; + background-color: var(--button-background-color, #007bff); + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; + animation: fadeIn 0.3s ease; +} + +.fab:hover { + background-color: var(--button-hover-background-color, #0069d9); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); + transform: scale(1.05); +} + +.fab:active { + transform: scale(0.95); +} + +.fab-icon { + width: 24px; + height: 24px; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Adjust FAB position on mobile */ +@media (max-width: 768px) { + .fab-container { + bottom: 70px; + right: 16px; + } + + .fab { + width: 48px; + height: 48px; + } + + .fab-icon { + width: 20px; + height: 20px; + } +} diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx index 5ecebc8..c8f0125 100644 --- a/src/frontend/app/routes/timetable-$id.tsx +++ b/src/frontend/app/routes/timetable-$id.tsx @@ -1,7 +1,14 @@ import { useEffect, useState, useRef } from "react"; import { useParams, Link } from "react-router"; import StopDataProvider from "../data/StopDataProvider"; -import { ArrowLeft, Eye, EyeOff } from "lucide-react"; +import { + ArrowLeft, + Eye, + EyeOff, + ChevronUp, + ChevronDown, + Clock, +} from "lucide-react"; import { type ScheduledTable } from "~/components/SchedulesTable"; import { TimetableSkeleton } from "~/components/TimetableSkeleton"; import { ErrorDisplay } from "~/components/ErrorDisplay"; @@ -12,12 +19,15 @@ import { useApp } from "~/AppContext"; import "./timetable-$id.css"; interface ErrorInfo { - type: 'network' | 'server' | 'unknown'; + type: "network" | "server" | "unknown"; status?: number; message?: string; } -const loadTimetableData = async (region: RegionId, stopId: string): Promise<ScheduledTable[]> => { +const loadTimetableData = async ( + region: RegionId, + stopId: string, +): Promise<ScheduledTable[]> => { const regionConfig = getRegionConfig(region); // Check if timetable is available for this region @@ -26,14 +36,17 @@ const loadTimetableData = async (region: RegionId, stopId: string): Promise<Sche } // Add delay to see skeletons in action (remove in production) - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); - const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format - const resp = await fetch(`${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, { - headers: { - Accept: "application/json", + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format + const resp = await fetch( + `${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, + { + headers: { + Accept: "application/json", + }, }, - }); + ); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); @@ -44,22 +57,26 @@ const loadTimetableData = async (region: RegionId, stopId: string): Promise<Sche // Utility function to compare times const timeToMinutes = (time: string): number => { - const [hours, minutes] = time.split(':').map(Number); + const [hours, minutes] = time.split(":").map(Number); return hours * 60 + minutes; }; // Filter past entries (keep only a few recent past ones) -const filterTimetableData = (data: ScheduledTable[], currentTime: string, showPast: boolean = false): ScheduledTable[] => { +const filterTimetableData = ( + data: ScheduledTable[], + currentTime: string, + showPast: boolean = false, +): ScheduledTable[] => { if (showPast) return data; const currentMinutes = timeToMinutes(currentTime); - const sortedData = [...data].sort((a, b) => - timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time) + const sortedData = [...data].sort( + (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time), ); // Find the current position - const currentIndex = sortedData.findIndex(entry => - timeToMinutes(entry.calling_time) >= currentMinutes + const currentIndex = sortedData.findIndex( + (entry) => timeToMinutes(entry.calling_time) >= currentMinutes, ); if (currentIndex === -1) { @@ -74,11 +91,11 @@ const filterTimetableData = (data: ScheduledTable[], currentTime: string, showPa // Utility function to parse service ID and get the turn number const parseServiceId = (serviceId: string): string => { - const parts = serviceId.split('_'); - if (parts.length === 0) return ''; + const parts = serviceId.split("_"); + if (parts.length === 0) return ""; const lastPart = parts[parts.length - 1]; - if (lastPart.length < 6) return ''; + if (lastPart.length < 6) return ""; const last6 = lastPart.slice(-6); const lineCode = last6.slice(0, 3); @@ -92,20 +109,40 @@ const parseServiceId = (serviceId: string): string => { let displayLine: string; switch (lineNumber) { - case 1: displayLine = "C1"; break; - case 3: displayLine = "C3"; break; - case 30: displayLine = "N1"; break; - case 33: displayLine = "N4"; break; - case 8: displayLine = "A"; break; - case 101: displayLine = "H"; break; - case 150: displayLine = "REF"; break; - case 500: displayLine = "TUR"; break; - default: displayLine = `L${lineNumber}`; + case 1: + displayLine = "C1"; + break; + case 3: + displayLine = "C3"; + break; + case 30: + displayLine = "N1"; + break; + case 33: + displayLine = "N4"; + break; + case 8: + displayLine = "A"; + break; + case 101: + displayLine = "H"; + break; + case 150: + displayLine = "REF"; + break; + case 500: + displayLine = "TUR"; + break; + default: + displayLine = `L${lineNumber}`; } return `${displayLine}-${turnNumber}`; }; +// Scroll threshold for showing FAB buttons (in pixels) +const SCROLL_THRESHOLD = 100; + export default function Timetable() { const { t } = useTranslation(); const { region } = useApp(); @@ -116,37 +153,48 @@ export default function Timetable() { const [loading, setLoading] = useState(true); const [error, setError] = useState<ErrorInfo | null>(null); const [showPastEntries, setShowPastEntries] = useState(false); + const [showScrollTop, setShowScrollTop] = useState(false); + const [showScrollBottom, setShowScrollBottom] = useState(false); + const [showGoToNow, setShowGoToNow] = useState(false); const nextEntryRef = useRef<HTMLDivElement>(null); + const containerRef = useRef<HTMLDivElement>(null); const regionConfig = getRegionConfig(region); const currentTime = new Date().toTimeString().slice(0, 8); // HH:MM:SS - const filteredData = filterTimetableData(timetableData, currentTime, showPastEntries); + const filteredData = filterTimetableData( + timetableData, + currentTime, + showPastEntries, + ); const parseError = (error: any): ErrorInfo => { if (!navigator.onLine) { - return { type: 'network', message: 'No internet connection' }; + return { type: "network", message: "No internet connection" }; } - if (error.message?.includes('Failed to fetch') || error.message?.includes('NetworkError')) { - return { type: 'network' }; + if ( + error.message?.includes("Failed to fetch") || + error.message?.includes("NetworkError") + ) { + return { type: "network" }; } - if (error.message?.includes('HTTP')) { + if (error.message?.includes("HTTP")) { const statusMatch = error.message.match(/HTTP (\d+):/); const status = statusMatch ? parseInt(statusMatch[1]) : undefined; - return { type: 'server', status }; + return { type: "server", status }; } - return { type: 'unknown', message: error.message }; + return { type: "unknown", message: error.message }; }; const loadData = async () => { // Check if timetable is available for this region if (!regionConfig.timetableEndpoint) { setError({ - type: 'server', + type: "server", status: 501, - message: 'Timetable not available for this region' + message: "Timetable not available for this region", }); setLoading(false); return; @@ -163,24 +211,25 @@ export default function Timetable() { // Scroll to next entry after a short delay to allow rendering setTimeout(() => { const currentMinutes = timeToMinutes(currentTime); - const sortedData = [...timetableBody].sort((a, b) => - timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time) + const sortedData = [...timetableBody].sort( + (a, b) => + timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time), ); - const nextIndex = sortedData.findIndex(entry => - timeToMinutes(entry.calling_time) >= currentMinutes + const nextIndex = sortedData.findIndex( + (entry) => timeToMinutes(entry.calling_time) >= currentMinutes, ); if (nextIndex !== -1 && nextEntryRef.current) { nextEntryRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'center' + behavior: "smooth", + block: "center", }); } }, 500); } } catch (err) { - console.error('Error loading timetable data:', err); + console.error("Error loading timetable data:", err); setError(parseError(err)); setTimetableData([]); } finally { @@ -193,6 +242,74 @@ export default function Timetable() { setCustomName(StopDataProvider.getCustomName(region, stopIdNum)); }, [params.id, region]); + // Handle scroll events to update FAB visibility + useEffect(() => { + const handleScroll = () => { + if ( + !containerRef.current || + loading || + error || + timetableData.length === 0 + ) { + return; + } + + const container = containerRef.current; + const scrollTop = container.scrollTop; + const scrollHeight = container.scrollHeight; + const clientHeight = container.clientHeight; + const scrollBottom = scrollHeight - scrollTop - clientHeight; + + // Threshold for showing scroll buttons (in pixels) + const threshold = 100; + + // Show scroll top button when scrolled down + setShowScrollTop(scrollTop > threshold); + + // Show scroll bottom button when not at bottom + setShowScrollBottom(scrollBottom > threshold); + + // Check if next entry (current time) is visible + if (nextEntryRef.current) { + const rect = nextEntryRef.current.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + const isNextVisible = + rect.top >= containerRect.top && rect.bottom <= containerRect.bottom; + + setShowGoToNow(!isNextVisible); + } + }; + + const container = containerRef.current; + if (container) { + container.addEventListener("scroll", handleScroll); + // Initial check + handleScroll(); + + return () => { + container.removeEventListener("scroll", handleScroll); + }; + } + }, [loading, error, timetableData]); + + const scrollToTop = () => { + containerRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + }; + + const scrollToBottom = () => { + containerRef.current?.scrollTo({ + top: containerRef.current.scrollHeight, + behavior: "smooth", + }); + }; + + const scrollToNow = () => { + nextEntryRef.current?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }; + if (loading) { return ( <div className="page-container"> @@ -242,16 +359,24 @@ export default function Timetable() { </div> ) : timetableData.length === 0 ? ( <div className="error-message"> - <p>{t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")}</p> + <p> + {t( + "timetable.noDataAvailable", + "No hay datos de horarios disponibles para hoy", + )} + </p> <p className="error-detail"> - {t("timetable.errorDetail", "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.")} + {t( + "timetable.errorDetail", + "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.", + )} </p> </div> ) : ( - <div className="timetable-full-content"> + <div className="timetable-full-content" ref={containerRef}> <div className="timetable-controls"> <button - className={`past-toggle ${showPastEntries ? 'active' : ''}`} + className={`past-toggle ${showPastEntries ? "active" : ""}`} onClick={() => setShowPastEntries(!showPastEntries)} > {showPastEntries ? ( @@ -274,6 +399,42 @@ export default function Timetable() { currentTime={currentTime} nextEntryRef={nextEntryRef} /> + + {/* Floating Action Button */} + {(showGoToNow || showScrollTop || showScrollBottom) && ( + <div className="fab-container"> + {showGoToNow && !showScrollTop && !showScrollBottom && ( + <button + className="fab fab-now" + onClick={scrollToNow} + title={t("timetable.goToNow", "Ir a ahora")} + aria-label={t("timetable.goToNow", "Ir a ahora")} + > + <Clock className="fab-icon" /> + </button> + )} + {showScrollTop && ( + <button + className="fab fab-up" + onClick={scrollToTop} + title={t("timetable.scrollUp", "Subir")} + aria-label={t("timetable.scrollUp", "Subir")} + > + <ChevronUp className="fab-icon" /> + </button> + )} + {showScrollBottom && !showScrollTop && ( + <button + className="fab fab-down" + onClick={scrollToBottom} + title={t("timetable.scrollDown", "Bajar")} + aria-label={t("timetable.scrollDown", "Bajar")} + > + <ChevronDown className="fab-icon" /> + </button> + )} + </div> + )} </div> )} </div> @@ -301,7 +462,11 @@ const TimetableTableWithScroll: React.FC<{ {data.map((entry, index) => { const entryMinutes = timeToMinutes(entry.calling_time); const isPast = entryMinutes < nowMinutes; - const isNext = !isPast && (index === 0 || timeToMinutes(data[index - 1]?.calling_time || '00:00:00') < nowMinutes); + const isNext = + !isPast && + (index === 0 || + timeToMinutes(data[index - 1]?.calling_time || "00:00:00") < + nowMinutes); return ( <div @@ -312,8 +477,8 @@ const TimetableTableWithScroll: React.FC<{ background: isPast ? "var(--surface-past, #f3f3f3)" : isNext - ? "var(--surface-next, #e8f5e8)" - : "var(--surface-future, #fff)" + ? "var(--surface-next, #e8f5e8)" + : "var(--surface-future, #fff)", }} > <div className="card-header"> @@ -325,7 +490,9 @@ const TimetableTableWithScroll: React.FC<{ {entry.route && entry.route.trim() ? ( <strong>{entry.route}</strong> ) : ( - <strong>{t("timetable.noDestination", "Línea")} {entry.line}</strong> + <strong> + {t("timetable.noDestination", "Línea")} {entry.line} + </strong> )} </div> @@ -341,7 +508,7 @@ const TimetableTableWithScroll: React.FC<{ <div className="card-body"> {!isPast && entry.next_streets.length > 0 && ( <div className="route-streets"> - {entry.next_streets.join(' — ')} + {entry.next_streets.join(" — ")} </div> )} </div> @@ -351,7 +518,9 @@ const TimetableTableWithScroll: React.FC<{ </div> {data.length === 0 && ( - <p className="no-data">{t("timetable.noData", "No hay datos de horarios disponibles")}</p> + <p className="no-data"> + {t("timetable.noData", "No hay datos de horarios disponibles")} + </p> )} </div> ); diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index e8bdee3..6548e8a 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -83,7 +83,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1601,7 +1600,6 @@ "integrity": "sha512-sww8oDNqz8SgaXEQ3maqTuMlibCMpmWvLE0s5zyEyOQb1G99clYMcXceQ2HNU2jtXJkp+P5XI1CngpGpngyTnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.5", @@ -1983,7 +1981,6 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1994,7 +1991,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2065,7 +2061,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -2372,7 +2367,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2613,7 +2607,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3178,7 +3171,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3386,7 +3378,6 @@ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4281,7 +4272,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -4391,8 +4381,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/leaflet.markercluster": { "version": "1.5.3", @@ -4472,7 +4461,6 @@ "integrity": "sha512-XY2KHMsrVwSxGmudI94BXvP6v++9KxFs6/MEm9yPaF83H1YYDE8MPe2kc+2yAuPV7c1iA6Y5tmBsPcNj4J/ywg==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5097,7 +5085,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5314,7 +5301,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5324,7 +5310,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5456,7 +5441,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -6311,7 +6295,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6506,7 +6489,6 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6782,7 +6764,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } |
