feat: Implement separate home and battery screens
This commit is contained in:
336
src/App.tsx
336
src/App.tsx
@@ -3,16 +3,14 @@ import type { ChargingSession } from "./types";
|
|||||||
import { fetchSessions } from "./api";
|
import { fetchSessions } from "./api";
|
||||||
|
|
||||||
type LoadState = "idle" | "loading" | "success" | "error";
|
type LoadState = "idle" | "loading" | "success" | "error";
|
||||||
|
type Screen = "home" | "battery";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
const [screen, setScreen] = useState<Screen>("home");
|
||||||
const [sessions, setSessions] = useState<ChargingSession[]>([]);
|
const [sessions, setSessions] = useState<ChargingSession[]>([]);
|
||||||
const [loadState, setLoadState] = useState<LoadState>("idle");
|
const [loadState, setLoadState] = useState<LoadState>("idle");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const [activeTab] = useState<"home" | "battery" | "trips" | "settings">(
|
|
||||||
"battery"
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoadState("loading");
|
setLoadState("loading");
|
||||||
@@ -21,7 +19,8 @@ const App: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const { sessions } = await fetchSessions(30);
|
const { sessions } = await fetchSessions(30);
|
||||||
const sorted = [...sessions].sort(
|
const sorted = [...sessions].sort(
|
||||||
(a, b) => new Date(b.endedAt).getTime() - new Date(a.endedAt).getTime()
|
(a, b) =>
|
||||||
|
new Date(b.endedAt).getTime() - new Date(a.endedAt).getTime()
|
||||||
);
|
);
|
||||||
setSessions(sorted);
|
setSessions(sorted);
|
||||||
setLoadState("success");
|
setLoadState("success");
|
||||||
@@ -43,6 +42,159 @@ const App: React.FC = () => {
|
|||||||
return latestSession.sohEnd - previousSession.sohEnd;
|
return latestSession.sohEnd - previousSession.sohEnd;
|
||||||
}, [latestSession, previousSession]);
|
}, [latestSession, previousSession]);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`screen ${
|
||||||
|
screen === "battery" ? "screen-battery-enter" : "screen-home-enter"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<StatusBar now={now} />
|
||||||
|
{screen === "home" ? (
|
||||||
|
<HomeScreen onOpenBattery={() => setScreen("battery")} />
|
||||||
|
) : (
|
||||||
|
<BatteryScreen
|
||||||
|
sessions={sessions}
|
||||||
|
loadState={loadState}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
latestSession={latestSession}
|
||||||
|
sohChange={sohChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{screen === "battery" && (
|
||||||
|
<BottomNav current={screen} onChange={setScreen} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- STATUS BAR ---------- */
|
||||||
|
|
||||||
|
interface StatusBarProps {
|
||||||
|
now: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBar: React.FC<StatusBarProps> = ({ now }) => (
|
||||||
|
<div className="status-bar">
|
||||||
|
<div className="status-left">
|
||||||
|
<span className="status-time">
|
||||||
|
{now.toLocaleTimeString(undefined, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="status-temp">-2°C</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-center">
|
||||||
|
<span className="status-car">
|
||||||
|
🚗
|
||||||
|
<span className="status-car-range">320 km</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-right">
|
||||||
|
<span className="status-icon">📶</span>
|
||||||
|
<span className="status-icon">LTE</span>
|
||||||
|
<span className="status-icon">🔊</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------- HOME SCREEN ---------- */
|
||||||
|
|
||||||
|
interface HomeScreenProps {
|
||||||
|
onOpenBattery: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HomeScreen: React.FC<HomeScreenProps> = ({ onOpenBattery }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="top-bar">
|
||||||
|
<div className="top-bar-left">
|
||||||
|
<span className="brand">Car Menu</span>
|
||||||
|
<span className="subtitle">Select a function</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* HOME CONTENT: VW-style icon grid */}
|
||||||
|
<main className="home-menu-content">
|
||||||
|
<div className="home-menu-grid">
|
||||||
|
{/* Row 1 */}
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">📻</span>
|
||||||
|
<span className="home-menu-label">Radio</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">🎵</span>
|
||||||
|
<span className="home-menu-label">Media</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">🔗</span>
|
||||||
|
<span className="home-menu-label">App Connect</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">❄️</span>
|
||||||
|
<span className="home-menu-label">Climate</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">📞</span>
|
||||||
|
<span className="home-menu-label">Devices</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="home-menu-tile home-menu-tile-battery"
|
||||||
|
onClick={onOpenBattery}
|
||||||
|
>
|
||||||
|
<span className="home-menu-icon">🔋</span>
|
||||||
|
<span className="home-menu-label">Battery</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">🚗</span>
|
||||||
|
<span className="home-menu-label">Car</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">🖼️</span>
|
||||||
|
<span className="home-menu-label">Images</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">🔊</span>
|
||||||
|
<span className="home-menu-label">Sound</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="home-menu-tile">
|
||||||
|
<span className="home-menu-icon">⚙️</span>
|
||||||
|
<span className="home-menu-label">Settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- BATTERY SCREEN (existing screen) ---------- */
|
||||||
|
|
||||||
|
interface BatteryScreenProps {
|
||||||
|
sessions: ChargingSession[];
|
||||||
|
loadState: LoadState;
|
||||||
|
errorMessage: string | null;
|
||||||
|
latestSession: ChargingSession | null;
|
||||||
|
sohChange: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BatteryScreen: React.FC<BatteryScreenProps> = ({
|
||||||
|
sessions,
|
||||||
|
loadState,
|
||||||
|
errorMessage,
|
||||||
|
latestSession,
|
||||||
|
sohChange,
|
||||||
|
}) => {
|
||||||
const sohTrendPoints = useMemo(() => {
|
const sohTrendPoints = useMemo(() => {
|
||||||
return sessions
|
return sessions
|
||||||
.slice()
|
.slice()
|
||||||
@@ -56,73 +208,15 @@ const App: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
}, [sessions]);
|
}, [sessions]);
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="screen">
|
<>
|
||||||
{/* STATUS BAR – topmost, like Android Auto / CarPlay */}
|
|
||||||
<div className="status-bar">
|
|
||||||
<div className="status-left">
|
|
||||||
<span className="status-time">
|
|
||||||
{now.toLocaleTimeString(undefined, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="status-temp">-2°C</span>
|
|
||||||
</div>
|
|
||||||
<div className="status-center">
|
|
||||||
<span className="status-car">
|
|
||||||
🚗
|
|
||||||
<span className="status-car-range">320 km</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="status-right">
|
|
||||||
<span className="status-icon">📶</span>
|
|
||||||
<span className="status-icon">LTE</span>
|
|
||||||
<span className="status-icon">🔊</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* APP HEADER */}
|
|
||||||
<header className="top-bar">
|
<header className="top-bar">
|
||||||
<div className="top-bar-left">
|
<div className="top-bar-left">
|
||||||
<span className="brand">Battery Health</span>
|
<span className="brand">Battery Health</span>
|
||||||
<span className="subtitle">Last 30 days</span>
|
<span className="subtitle">Last 30 days</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="top-bar-right">
|
|
||||||
<span className="status-dot" />
|
|
||||||
<span className="status-label">
|
|
||||||
{loadState === "loading"
|
|
||||||
? "Syncing…"
|
|
||||||
: loadState === "error"
|
|
||||||
? "Offline"
|
|
||||||
: "Connected"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* QUICK CONTROL STRIP – static toggles to feel “car-like” */}
|
|
||||||
<div className="quick-strip">
|
|
||||||
<button className="quick-btn quick-btn-active">
|
|
||||||
<span className="quick-icon">❄️</span>
|
|
||||||
<span className="quick-label">AC Auto</span>
|
|
||||||
</button>
|
|
||||||
<button className="quick-btn">
|
|
||||||
<span className="quick-icon">🔥</span>
|
|
||||||
<span className="quick-label">Seat</span>
|
|
||||||
</button>
|
|
||||||
<button className="quick-btn">
|
|
||||||
<span className="quick-icon">🌙</span>
|
|
||||||
<span className="quick-label">Night</span>
|
|
||||||
</button>
|
|
||||||
<button className="quick-btn">
|
|
||||||
<span className="quick-icon">⚡</span>
|
|
||||||
<span className="quick-label">Eco</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* MAIN CONTENT */}
|
|
||||||
{loadState === "loading" && (
|
{loadState === "loading" && (
|
||||||
<div className="center-message">Loading charging history…</div>
|
<div className="center-message">Loading charging history…</div>
|
||||||
)}
|
)}
|
||||||
@@ -156,14 +250,16 @@ const App: React.FC = () => {
|
|||||||
<span className="value">
|
<span className="value">
|
||||||
{latestSession ? latestSession.sohEnd.toFixed(1) : "--"}%
|
{latestSession ? latestSession.sohEnd.toFixed(1) : "--"}%
|
||||||
</span>
|
</span>
|
||||||
<span className="secondary">
|
{latestSession && (
|
||||||
Session ended{" "}
|
<span className="secondary">
|
||||||
{new Date(latestSession!.endedAt).toLocaleString(undefined, {
|
Session ended{" "}
|
||||||
weekday: "short",
|
{new Date(latestSession.endedAt).toLocaleString(undefined, {
|
||||||
hour: "2-digit",
|
weekday: "short",
|
||||||
minute: "2-digit",
|
hour: "2-digit",
|
||||||
})}
|
minute: "2-digit",
|
||||||
</span>
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="soh-change">
|
<div className="soh-change">
|
||||||
@@ -254,9 +350,7 @@ const App: React.FC = () => {
|
|||||||
{sessions.map((session, index) => {
|
{sessions.map((session, index) => {
|
||||||
const prev = sessions[index + 1];
|
const prev = sessions[index + 1];
|
||||||
const delta =
|
const delta =
|
||||||
prev != null
|
prev != null ? session.sohEnd - prev.sohEnd : undefined;
|
||||||
? session.sohEnd - prev.sohEnd
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionRow
|
<SessionRow
|
||||||
@@ -270,47 +364,52 @@ const App: React.FC = () => {
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
{/* BOTTOM NAV – static icons / labels */}
|
|
||||||
<nav className="bottom-nav">
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
"nav-button " + (activeTab === "home" ? "nav-button-active" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="nav-icon">🏠</span>
|
|
||||||
<span className="nav-label">Home</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
"nav-button " + (activeTab === "battery" ? "nav-button-active" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="nav-icon">🔋</span>
|
|
||||||
<span className="nav-label">Battery</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
"nav-button " + (activeTab === "trips" ? "nav-button-active" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="nav-icon">🗺️</span>
|
|
||||||
<span className="nav-label">Trips</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
"nav-button " +
|
|
||||||
(activeTab === "settings" ? "nav-button-active" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="nav-icon">⚙️</span>
|
|
||||||
<span className="nav-label">Settings</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ---------- BOTTOM NAV ---------- */
|
||||||
|
|
||||||
|
interface BottomNavProps {
|
||||||
|
current: Screen;
|
||||||
|
onChange: (screen: Screen) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomNav: React.FC<BottomNavProps> = ({ current, onChange }) => {
|
||||||
|
return (
|
||||||
|
<nav className="bottom-nav">
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"nav-button " + (current === "home" ? "nav-button-active" : "")
|
||||||
|
}
|
||||||
|
onClick={() => onChange("home")}
|
||||||
|
>
|
||||||
|
<span className="nav-icon">🏠</span>
|
||||||
|
<span className="nav-label">Home</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"nav-button " + (current === "battery" ? "nav-button-active" : "")
|
||||||
|
}
|
||||||
|
onClick={() => onChange("battery")}
|
||||||
|
>
|
||||||
|
<span className="nav-icon">🚗</span>
|
||||||
|
<span className="nav-label">Car</span>
|
||||||
|
</button>
|
||||||
|
<button className="nav-button">
|
||||||
|
<span className="nav-icon">🗺️</span>
|
||||||
|
<span className="nav-label">Navigation</span>
|
||||||
|
</button>
|
||||||
|
<button className="nav-button">
|
||||||
|
<span className="nav-icon">⚙️</span>
|
||||||
|
<span className="nav-label">Settings</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- CHART + SESSION ROW (unchanged) ---------- */
|
||||||
|
|
||||||
interface TrendPoint {
|
interface TrendPoint {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
@@ -332,10 +431,7 @@ const SimpleTrendChart: React.FC<SimpleTrendChartProps> = ({ points }) => {
|
|||||||
|
|
||||||
const norm = (v: number) => {
|
const norm = (v: number) => {
|
||||||
if (max === min) return 50;
|
if (max === min) return 50;
|
||||||
return (
|
return padding + ((max - v) / (max - min)) * (100 - padding * 2);
|
||||||
padding +
|
|
||||||
((max - v) / (max - min)) * (100 - padding * 2)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepX = points.length > 1 ? 100 / (points.length - 1) : 50;
|
const stepX = points.length > 1 ? 100 / (points.length - 1) : 50;
|
||||||
@@ -350,7 +446,11 @@ const SimpleTrendChart: React.FC<SimpleTrendChartProps> = ({ points }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-wrapper">
|
<div className="chart-wrapper">
|
||||||
<svg viewBox="0 0 100 100" className="chart-svg" preserveAspectRatio="none">
|
<svg
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
className="chart-svg"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
<path d={pathD} className="chart-line" fill="none" />
|
<path d={pathD} className="chart-line" fill="none" />
|
||||||
{points.map((p, i) => {
|
{points.map((p, i) => {
|
||||||
const x = i * stepX;
|
const x = i * stepX;
|
||||||
|
|||||||
169
src/styles.css
169
src/styles.css
@@ -19,7 +19,6 @@ html,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* MAIN SCREEN LAYOUT */
|
/* MAIN SCREEN LAYOUT */
|
||||||
|
|
||||||
.screen {
|
.screen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -30,7 +29,6 @@ html,
|
|||||||
padding: 4px 16px 8px;
|
padding: 4px 16px 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
/* whole screen scrolls */
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
@@ -40,7 +38,6 @@ html,
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
/* small tweak */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-bar-left {
|
.top-bar-left {
|
||||||
@@ -89,19 +86,16 @@ html,
|
|||||||
color: #ff453a;
|
color: #ff453a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CONTENT AREA – just layout, NO own scrolling now */
|
/* CONTENT AREA */
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
/* small extra space above nav */
|
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PANELS */
|
/* PANELS */
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: rgba(9, 11, 22, 0.9);
|
background: rgba(9, 11, 22, 0.9);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -133,7 +127,6 @@ html,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* CURRENT SOH SECTION */
|
/* CURRENT SOH SECTION */
|
||||||
|
|
||||||
.current-soh-row {
|
.current-soh-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -215,7 +208,6 @@ html,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Chart */
|
/* Chart */
|
||||||
|
|
||||||
.panel-chart {
|
.panel-chart {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -262,7 +254,6 @@ html,
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IMPORTANT: remove its own scrolling, let whole screen handle it */
|
|
||||||
.session-list {
|
.session-list {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -307,7 +298,6 @@ html,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* STATUS BAR */
|
/* STATUS BAR */
|
||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -354,41 +344,7 @@ html,
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* QUICK STRIP */
|
|
||||||
|
|
||||||
.quick-strip {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-btn {
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: rgba(21, 24, 40, 0.9);
|
|
||||||
color: #e7e9f5;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-btn-active {
|
|
||||||
background: linear-gradient(120deg, #2bcbff, #5e72eb);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-icon {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-label {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BOTTOM NAV – fixed overlay inside the car screen */
|
/* BOTTOM NAV – fixed overlay inside the car screen */
|
||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -460,3 +416,126 @@ html,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VW-style HOME MENU GRID – fills the screen under the header */
|
||||||
|
|
||||||
|
.home-menu-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-grid {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-tile {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: radial-gradient(circle at top, #272b37, #141722 60%, #05060a);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 10px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||||
|
color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Battery tile – interactive, glow on hover */
|
||||||
|
.home-menu-tile-battery {
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.25s ease,
|
||||||
|
box-shadow 0.25s ease,
|
||||||
|
background 0.25s ease,
|
||||||
|
filter 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-tile-battery:hover {
|
||||||
|
transform: translateY(-3px) scale(1.04);
|
||||||
|
box-shadow:
|
||||||
|
0 0 25px rgba(80, 220, 255, 0.8),
|
||||||
|
0 8px 18px rgba(0, 0, 0, 0.8);
|
||||||
|
background: radial-gradient(circle at top, #2f3f6a, #111423 65%, #05060a);
|
||||||
|
filter: drop-shadow(0 0 12px rgba(80, 220, 255, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-menu-tile-battery:active {
|
||||||
|
transform: translateY(-1px) scale(0.99);
|
||||||
|
box-shadow:
|
||||||
|
0 0 14px rgba(80, 220, 255, 0.7),
|
||||||
|
0 4px 10px rgba(0, 0, 0, 0.8);
|
||||||
|
filter: drop-shadow(0 0 8px rgba(80, 220, 255, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screen enter animation */
|
||||||
|
.screen-battery-enter {
|
||||||
|
animation: battery-screen-in 300ms ease-out;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes battery-screen-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.98);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-home-enter {
|
||||||
|
animation: home-screen-in 200ms ease-out;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes home-screen-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.98);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user