aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-06 23:36:52 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-06 23:36:52 +0100
commit37d8eedd641bb04c086797010292bcb25240d56d (patch)
tree85486542fc59e4b08485eba5625c9f923ca71ac1 /src/frontend
parent0ac8ba208e0ad4d61cb82d6216c9cb34d43421a0 (diff)
Refactor styles and add alert color variables; implement scroll management for timetable
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/app/components/SchedulesTable.css55
-rw-r--r--src/frontend/app/components/StopAlert.css50
-rw-r--r--src/frontend/app/components/StopAlert.tsx9
-rw-r--r--src/frontend/app/components/StopItemSkeleton.tsx2
-rw-r--r--src/frontend/app/components/StopSheetSkeleton.tsx4
-rw-r--r--src/frontend/app/components/TimetableSkeleton.tsx2
-rw-r--r--src/frontend/app/root.css32
-rw-r--r--src/frontend/app/root.tsx12
-rw-r--r--src/frontend/app/routes/map.tsx21
-rw-r--r--src/frontend/app/routes/timetable-$id.css2
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx235
-rw-r--r--src/frontend/public/maps/spritesheet/sprite.json16
-rw-r--r--src/frontend/public/maps/spritesheet/sprite.pngbin1515 -> 2987 bytes
-rw-r--r--src/frontend/public/maps/spritesheet/sprite@2x.json16
-rw-r--r--src/frontend/public/maps/spritesheet/sprite@2x.pngbin2878 -> 7056 bytes
15 files changed, 295 insertions, 161 deletions
diff --git a/src/frontend/app/components/SchedulesTable.css b/src/frontend/app/components/SchedulesTable.css
index 8980fb4..6d2f201 100644
--- a/src/frontend/app/components/SchedulesTable.css
+++ b/src/frontend/app/components/SchedulesTable.css
@@ -7,7 +7,7 @@
margin-bottom: 1rem;
text-align: left;
font-size: 1.1rem;
- color: var(--text-primary, #333);
+ color: var(--text-primary);
}
.timetable-cards {
@@ -18,17 +18,36 @@
}
.timetable-card {
- background-color: var(--surface-future, #fff);
- border: 1px solid var(--card-border, #e0e0e0);
+ background-color: var(--surface-future);
+ border: 1px solid var(--card-border);
border-radius: 10px;
padding: 1.25rem;
transition: background-color 0.2s ease, border 0.2s ease;
}
+/* Next upcoming service: slight emphasis */
+.timetable-card.timetable-next {
+ background-color: var(--surface-next);
+ border-color: var(--card-border);
+ position: relative;
+}
+
+.timetable-card.timetable-next::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ background: var(--accent-next);
+}
+
.timetable-card.timetable-past {
- background-color: var(--surface-past, #f3f3f3);
- color: var(--text-secondary, #aaa);
- border: 1px solid #e0e0e0;
+ background-color: var(--surface-past);
+ color: var(--text-secondary);
+ border: 1px solid var(--card-border);
}
.card-header {
@@ -46,7 +65,7 @@
flex: 1;
text-align: left;
margin: 0 1rem;
- color: var(--text-primary, #333);
+ color: var(--text-primary);
}
.destination-info strong {
@@ -54,7 +73,7 @@
}
.timetable-card.timetable-past .destination-info {
- color: var(--text-secondary, #aaa);
+ color: var(--text-secondary);
}
.time-info {
@@ -68,11 +87,11 @@
font-weight: bold;
font-family: monospace;
font-size: 1.1rem;
- color: var(--text-primary, #333);
+ color: var(--text-primary);
}
.timetable-card.timetable-past .departure-time {
- color: var(--text-secondary, #aaa);
+ color: var(--text-secondary);
}
.card-body {
@@ -81,7 +100,7 @@
.route-streets {
font-size: 0.85rem;
- color: var(--text-secondary, #666);
+ color: var(--text-secondary);
line-height: 1.8;
word-break: break-word;
}
@@ -89,8 +108,8 @@
.service-id {
font-family: monospace;
font-size: 0.8rem;
- color: var(--text-secondary, #666);
- background: var(--service-background, #f0f0f0);
+ color: var(--text-secondary);
+ background: var(--service-background);
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-weight: 500;
@@ -99,18 +118,18 @@
}
.timetable-card.timetable-past .service-id {
- color: var(--text-secondary, #bbb);
- background: #e8e8e8;
+ color: var(--text-secondary);
+ background: var(--service-background-past);
}
.no-data {
text-align: center;
- color: var(--text-secondary, #666);
+ color: var(--text-secondary);
font-style: italic;
padding: 2rem;
- background: var(--card-background, #f8f9fa);
+ background: var(--card-background);
border-radius: 8px;
- border: 1px solid var(--card-border, #e0e0e0);
+ border: 1px solid var(--card-border);
}
/* Responsive design */
diff --git a/src/frontend/app/components/StopAlert.css b/src/frontend/app/components/StopAlert.css
index 0032d09..2ba3baa 100644
--- a/src/frontend/app/components/StopAlert.css
+++ b/src/frontend/app/components/StopAlert.css
@@ -1,3 +1,25 @@
+/* Alert color variables */
+:root {
+ --alert-info-bg: rgba(59, 130, 246, 0.1);
+ --alert-info-border: rgba(59, 130, 246, 0.5);
+ --alert-info-text: #1e40af;
+
+ --alert-error-bg: rgba(239, 68, 68, 0.1);
+ --alert-error-border: rgba(239, 68, 68, 0.5);
+ --alert-error-text: #991b1b;
+}
+
+/* Dark mode overrides use data-mode */
+[data-mode="dark"] {
+ --alert-info-bg: rgba(59, 130, 246, 0.15);
+ --alert-info-border: rgba(59, 130, 246, 0.4);
+ --alert-info-text: #93c5fd;
+
+ --alert-error-bg: rgba(239, 68, 68, 0.15);
+ --alert-error-border: rgba(239, 68, 68, 0.4);
+ --alert-error-text: #fca5a5;
+}
+
.stop-alert {
display: flex;
align-items: flex-start;
@@ -9,20 +31,20 @@
}
.stop-alert-info {
- background-color: rgba(59, 130, 246, 0.1);
- border-color: rgba(59, 130, 246, 0.3);
- color: #1e40af;
+ background-color: var(--alert-info-bg);
+ border-color: var(--alert-info-border);
+ color: var(--alert-info-text);
}
.stop-alert-error {
- background-color: rgba(239, 68, 68, 0.1);
- border-color: rgba(239, 68, 68, 0.3);
- color: #991b1b;
+ background-color: var(--alert-error-bg);
+ border-color: var(--alert-error-border);
+ color: var(--alert-error-text);
}
.stop-alert-compact {
padding: 0.5rem;
- margin: 0.5rem 0;
+ margin: 1.5rem 0 0.5rem 0;
font-size: 0.875rem;
}
@@ -73,17 +95,3 @@
font-size: 0.75rem;
}
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- .stop-alert-info {
- background-color: rgba(59, 130, 246, 0.15);
- border-color: rgba(59, 130, 246, 0.4);
- color: #93c5fd;
- }
-
- .stop-alert-error {
- background-color: rgba(239, 68, 68, 0.15);
- border-color: rgba(239, 68, 68, 0.4);
- color: #fca5a5;
- }
-}
diff --git a/src/frontend/app/components/StopAlert.tsx b/src/frontend/app/components/StopAlert.tsx
index 69ecc22..a96f93e 100644
--- a/src/frontend/app/components/StopAlert.tsx
+++ b/src/frontend/app/components/StopAlert.tsx
@@ -19,17 +19,10 @@ export const StopAlert: React.FC<StopAlertProps> = ({ stop, compact = false }) =
return (
<div className={`stop-alert ${isError ? 'stop-alert-error' : 'stop-alert-info'} ${compact ? 'stop-alert-compact' : ''}`}>
- <div className="stop-alert-icon">
- {isError ? <AlertCircle /> : <Info />}
- </div>
+ {isError ? <AlertCircle /> : <Info />}
<div className="stop-alert-content">
{stop.title && <div className="stop-alert-title">{stop.title}</div>}
{stop.message && <div className="stop-alert-message">{stop.message}</div>}
- {stop.alternateCodes && stop.alternateCodes.length > 0 && (
- <div className="stop-alert-alternate-codes">
- Alternative stops: {stop.alternateCodes.join(", ")}
- </div>
- )}
</div>
</div>
);
diff --git a/src/frontend/app/components/StopItemSkeleton.tsx b/src/frontend/app/components/StopItemSkeleton.tsx
index 72f7506..9133393 100644
--- a/src/frontend/app/components/StopItemSkeleton.tsx
+++ b/src/frontend/app/components/StopItemSkeleton.tsx
@@ -12,7 +12,7 @@ const StopItemSkeleton: React.FC<StopItemSkeletonProps> = ({
stopId
}) => {
return (
- <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
+ <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)">
<li className="list-item">
<div className="list-item-link">
<span>
diff --git a/src/frontend/app/components/StopSheetSkeleton.tsx b/src/frontend/app/components/StopSheetSkeleton.tsx
index 651efa5..6870af2 100644
--- a/src/frontend/app/components/StopSheetSkeleton.tsx
+++ b/src/frontend/app/components/StopSheetSkeleton.tsx
@@ -13,7 +13,7 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({
const { t } = useTranslation();
return (
- <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
+ <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)">
<div className="stop-sheet-estimates">
<h3 className="stop-sheet-subtitle">
{t("estimates.next_arrivals", "Next arrivals")}
@@ -53,7 +53,7 @@ export const StopSheetSkeleton: React.FC<StopSheetSkeletonProps> = ({
</div>
<div className="stop-sheet-view-all" style={{
- background: "#f0f0f0",
+ background: "var(--service-background)",
cursor: "not-allowed",
pointerEvents: "none"
}}>
diff --git a/src/frontend/app/components/TimetableSkeleton.tsx b/src/frontend/app/components/TimetableSkeleton.tsx
index 01956ee..79c7725 100644
--- a/src/frontend/app/components/TimetableSkeleton.tsx
+++ b/src/frontend/app/components/TimetableSkeleton.tsx
@@ -13,7 +13,7 @@ export const TimetableSkeleton: React.FC<TimetableSkeletonProps> = ({
const { t } = useTranslation();
return (
- <SkeletonTheme baseColor="#f0f0f0" highlightColor="#e0e0e0">
+ <SkeletonTheme baseColor="var(--skeleton-base)" highlightColor="var(--skeleton-highlight)">
<div className="timetable-container">
<div className="timetable-caption">
<Skeleton width="250px" height="1.1rem" />
diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css
index 202e6f1..fb955eb 100644
--- a/src/frontend/app/root.css
+++ b/src/frontend/app/root.css
@@ -10,6 +10,22 @@
--star-color: #ffcc00;
--message-background-color: #f8f9fa;
+ /* Skeletons */
+ --skeleton-base: #f0f0f0;
+ --skeleton-highlight: #e0e0e0;
+
+ /* Timetable component variables */
+ --text-primary: var(--text-color);
+ --text-secondary: #666666;
+ --surface-future: var(--background-color);
+ --surface-next: #eef6ff; /* slightly accented surface for next card */
+ --surface-past: hsl(0 0% 25% / 1);
+ --accent-next: #1e88e5; /* accent color for next card left border */
+ --card-border: var(--border-color);
+ --card-background: #f8f9fa;
+ --service-background: #f0f0f0;
+ --service-background-past: #e8e8e8;
+
color-scheme: light;
font-family: "Roboto Variable", Roboto, Arial, sans-serif;
}
@@ -26,6 +42,22 @@
--star-color: #ffcc00;
--message-background-color: #333333;
+ /* Skeletons (dark) */
+ --skeleton-base: #2a2a2a;
+ --skeleton-highlight: #3a3a3a;
+
+ /* Timetable component dark overrides */
+ --text-primary: var(--text-color);
+ --text-secondary: #bbbbbb;
+ --surface-future: #1e1e1e;
+ --surface-next: #17212b;
+ --surface-past: #1a1a1a;
+ --accent-next: #64b5f6;
+ --card-border: var(--border-color);
+ --card-background: #1e1e1e;
+ --service-background: #222222;
+ --service-background-past: #1f1f1f;
+
color-scheme: dark;
}
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 4815d2f..9bc69e4 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -76,6 +76,18 @@ export function Layout({ children }: { children: React.ReactNode }) {
<title>Busurbano</title>
<Meta />
+ {/* Set theme early to avoid flash of wrong theme (especially skeletons) */}
+ <script
+ dangerouslySetInnerHTML={{
+ __html: `(() => { try {
+ var saved = localStorage.getItem('theme');
+ var system = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
+ var resolved = (saved === 'light' || saved === 'dark') ? saved : system;
+ document.documentElement.setAttribute('data-theme', resolved);
+ document.documentElement.style.colorScheme = resolved;
+ } catch (e) {} })();`,
+ }}
+ />
<Links />
</head>
<body>
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index effd29b..f705617 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -32,7 +32,10 @@ const defaultStyle: StyleSpecification = {
export default function StopMap() {
const { t } = useTranslation();
const [stops, setStops] = useState<
- GeoJsonFeature<Point, { stopId: number; name: string; lines: string[] }>[]
+ GeoJsonFeature<
+ Point,
+ { stopId: number; name: string; lines: string[]; cancelled?: boolean }
+ >[]
>([]);
const [selectedStop, setSelectedStop] = useState<Stop | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
@@ -57,14 +60,19 @@ export default function StopMap() {
StopDataProvider.getStops(region).then((data) => {
const features: GeoJsonFeature<
Point,
- { stopId: number; name: string; lines: string[] }
+ { stopId: number; name: string; lines: string[]; cancelled?: boolean }
>[] = data.map((s) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.longitude as number, s.latitude as number],
},
- properties: { stopId: s.stopId, name: s.name.original, lines: s.lines },
+ properties: {
+ stopId: s.stopId,
+ name: s.name.original,
+ lines: s.lines,
+ cancelled: s.cancelled,
+ },
}));
setStops(features);
});
@@ -157,7 +165,12 @@ export default function StopMap() {
type="symbol"
source="stops-source"
layout={{
- "icon-image": `stop-${region}`,
+ "icon-image": [
+ "case",
+ ["boolean", ["get", "cancelled"], false],
+ `stop-${region}-cancelled`,
+ `stop-${region}`,
+ ],
"icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.7, 18, 1.0],
"icon-allow-overlap": true,
"icon-ignore-placement": true,
diff --git a/src/frontend/app/routes/timetable-$id.css b/src/frontend/app/routes/timetable-$id.css
index b4e231a..3815982 100644
--- a/src/frontend/app/routes/timetable-$id.css
+++ b/src/frontend/app/routes/timetable-$id.css
@@ -38,8 +38,6 @@
.timetable-full-content {
margin-top: 1rem;
- overflow-y: auto;
- max-height: calc(100vh - 250px);
position: relative;
padding-bottom: 80px; /* Space for FAB */
}
diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx
index c8f0125..df77372 100644
--- a/src/frontend/app/routes/timetable-$id.tsx
+++ b/src/frontend/app/routes/timetable-$id.tsx
@@ -153,9 +153,6 @@ 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);
@@ -242,73 +239,7 @@ 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",
- });
- };
+ // Scroll FABs moved to ScrollFabManager component
if (loading) {
return (
@@ -401,40 +332,13 @@ export default function Timetable() {
/>
{/* 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>
- )}
+ <ScrollFabManager
+ containerRef={containerRef}
+ nextEntryRef={nextEntryRef}
+ currentTime={currentTime}
+ data={filteredData}
+ disabled={loading || !!error || timetableData.length === 0}
+ />
</div>
)}
</div>
@@ -525,3 +429,126 @@ const TimetableTableWithScroll: React.FC<{
</div>
);
};
+
+// Component to manage scroll-based FAB visibility globally within timetable
+const ScrollFabManager: React.FC<{
+ containerRef: React.RefObject<HTMLDivElement | null>;
+ nextEntryRef: React.RefObject<HTMLDivElement | null>;
+ currentTime: string;
+ data: ScheduledTable[];
+ disabled?: boolean;
+}> = ({ containerRef, nextEntryRef, currentTime, data, disabled = false }) => {
+ const { t } = useTranslation();
+ const [showScrollTop, setShowScrollTop] = useState(false);
+ const [showScrollBottom, setShowScrollBottom] = useState(false);
+ const [showGoToNow, setShowGoToNow] = useState(false);
+
+ // Find the actual scrollable ancestor (.main-content) if our container isn't scrollable
+ const getScrollContainer = () => {
+ let el: HTMLElement | null = containerRef.current;
+ while (el) {
+ const style = getComputedStyle(el);
+ const hasScroll = el.scrollHeight > el.clientHeight + 8;
+ const overflowY = style.overflowY;
+ if (hasScroll && (overflowY === 'auto' || overflowY === 'scroll')) {
+ return el;
+ }
+ el = el.parentElement;
+ }
+ return null;
+ };
+
+ useEffect(() => {
+ if (disabled) return;
+ const scrollEl = getScrollContainer();
+ const useWindowScroll = !scrollEl;
+
+ const handleScroll = () => {
+ const scrollTop = useWindowScroll
+ ? (window.scrollY || document.documentElement.scrollTop || 0)
+ : scrollEl!.scrollTop;
+ const scrollHeight = useWindowScroll
+ ? document.documentElement.scrollHeight
+ : scrollEl!.scrollHeight;
+ const clientHeight = useWindowScroll ? window.innerHeight : scrollEl!.clientHeight;
+
+ const scrollBottom = scrollHeight - scrollTop - clientHeight;
+ const threshold = 80; // slightly smaller threshold for responsiveness
+ setShowScrollTop(scrollTop > threshold);
+ setShowScrollBottom(scrollBottom > threshold);
+
+ if (nextEntryRef.current) {
+ const rect = nextEntryRef.current.getBoundingClientRect();
+ const isNextVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
+ setShowGoToNow(!isNextVisible);
+ }
+ };
+
+ const target: any = useWindowScroll ? window : scrollEl!;
+ target.addEventListener('scroll', handleScroll, { passive: true });
+ window.addEventListener('resize', handleScroll);
+ handleScroll();
+ return () => {
+ target.removeEventListener('scroll', handleScroll);
+ window.removeEventListener('resize', handleScroll);
+ };
+ }, [containerRef, nextEntryRef, disabled, data, currentTime]);
+
+ const scrollToTop = () => {
+ const scrollEl = getScrollContainer();
+ if (!scrollEl) {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ } else {
+ scrollEl.scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ };
+ const scrollToBottom = () => {
+ const scrollEl = getScrollContainer();
+ if (!scrollEl) {
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' });
+ } else {
+ scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'smooth' });
+ }
+ };
+ const scrollToNow = () => {
+ nextEntryRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
+ };
+
+ if (disabled) return null;
+ if (!(showGoToNow || showScrollTop || showScrollBottom)) return null;
+
+ return (
+ <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>
+ );
+};
diff --git a/src/frontend/public/maps/spritesheet/sprite.json b/src/frontend/public/maps/spritesheet/sprite.json
index f6b564f..636efa5 100644
--- a/src/frontend/public/maps/spritesheet/sprite.json
+++ b/src/frontend/public/maps/spritesheet/sprite.json
@@ -14,5 +14,21 @@
"width": 32,
"height": 32,
"pixelRatio": 1
+ },
+ "stop-vigo-cancelled": {
+ "id": "stop-vigo-cancelled",
+ "x": 64,
+ "y": 0,
+ "width": 32,
+ "height": 32,
+ "pixelRatio": 1
+ },
+ "stop-santiago-cancelled": {
+ "id": "stop-santiago-cancelled",
+ "x": 96,
+ "y": 0,
+ "width": 32,
+ "height": 32,
+ "pixelRatio": 1
}
}
diff --git a/src/frontend/public/maps/spritesheet/sprite.png b/src/frontend/public/maps/spritesheet/sprite.png
index f9799e4..5c46a13 100644
--- a/src/frontend/public/maps/spritesheet/sprite.png
+++ b/src/frontend/public/maps/spritesheet/sprite.png
Binary files differ
diff --git a/src/frontend/public/maps/spritesheet/sprite@2x.json b/src/frontend/public/maps/spritesheet/sprite@2x.json
index b5bfb0b..f4a9adf 100644
--- a/src/frontend/public/maps/spritesheet/sprite@2x.json
+++ b/src/frontend/public/maps/spritesheet/sprite@2x.json
@@ -14,5 +14,21 @@
"width": 64,
"height": 64,
"pixelRatio": 2
+ },
+ "stop-vigo-cancelled": {
+ "id": "stop-vigo-cancelled",
+ "x": 128,
+ "y": 0,
+ "width": 64,
+ "height": 64,
+ "pixelRatio": 2
+ },
+ "stop-santiago-cancelled": {
+ "id": "stop-santiago-cancelled",
+ "x": 192,
+ "y": 0,
+ "width": 64,
+ "height": 64,
+ "pixelRatio": 2
}
}
diff --git a/src/frontend/public/maps/spritesheet/sprite@2x.png b/src/frontend/public/maps/spritesheet/sprite@2x.png
index e05a242..4101224 100644
--- a/src/frontend/public/maps/spritesheet/sprite@2x.png
+++ b/src/frontend/public/maps/spritesheet/sprite@2x.png
Binary files differ