updates on the responsiveness
This commit is contained in:
1115
package-lock.json
generated
1115
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import AppToaster from '@/components/third-party/AppToaster';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { queryClientInstance } from '@/lib/query-client';
|
||||
@@ -176,7 +175,6 @@ function App() {
|
||||
<ScrollToTop />
|
||||
<AuthenticatedApp />
|
||||
</Router>
|
||||
<Toaster />
|
||||
{/* Every toast in the app (OpenToast, the design system's `toast`,
|
||||
direct react-hot-toast calls) renders here: top-right, one card
|
||||
design. Without a mounted renderer every toast fires into
|
||||
|
||||
@@ -486,7 +486,7 @@ export default function Bookings() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 lg:flex-none">
|
||||
<ListToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
|
||||
@@ -43,6 +43,32 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Size container ─────────────────────────────────────────────────────────
|
||||
Every max-width breakpoint in this file is a CONTAINER query against this
|
||||
wrapper, not a viewport media query. The MileTruth dock narrows the page
|
||||
without changing the viewport: 1366px screen, panel open, ~920px for this
|
||||
page — and the old @media rules still applied their 1366px layout, so the
|
||||
header row overflowed and clipped the date picker and clock.
|
||||
|
||||
Side effect, accepted: container queries need layout containment, which
|
||||
makes this wrapper the containing block for position:fixed descendants.
|
||||
The centred order card and the rider-position modal therefore centre on
|
||||
the Control X area rather than the whole window.
|
||||
max-height rules stay @media — height isn't what the dock takes. */
|
||||
.dispatch-shell {
|
||||
container: dispatch / inline-size;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dispatch-shell.embedded {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.dispatch-container #hdr {
|
||||
height: 56px;
|
||||
@@ -5753,7 +5779,7 @@
|
||||
────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Large laptop — subtle sidebar reduction */
|
||||
@media (max-width: 1280px) {
|
||||
@container dispatch (max-width: 1280px) {
|
||||
.dispatch-container #sidebar {
|
||||
width: 360px;
|
||||
flex-basis: 360px;
|
||||
@@ -5769,7 +5795,7 @@
|
||||
}
|
||||
|
||||
/* Compact laptop (common 1366×768 screens) */
|
||||
@media (max-width: 1180px) {
|
||||
@container dispatch (max-width: 1180px) {
|
||||
.dispatch-container #sidebar {
|
||||
width: 320px;
|
||||
flex-basis: 320px;
|
||||
@@ -5816,7 +5842,7 @@
|
||||
}
|
||||
|
||||
/* Small laptop / 1024px */
|
||||
@media (max-width: 1080px) {
|
||||
@container dispatch (max-width: 1080px) {
|
||||
.dispatch-container #sidebar {
|
||||
width: 290px;
|
||||
flex-basis: 290px;
|
||||
@@ -5830,12 +5856,9 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Header — hide decorative city pill, tighten spacing */
|
||||
.dispatch-container .logo-city,
|
||||
.dispatch-container .logo-city-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The hub selector used to be hidden here to save width. It is the only way
|
||||
to switch hub, so it stays; the header wraps onto a second line instead
|
||||
(see the #hdr rule under "Header wrap"). */
|
||||
.dispatch-container .logo-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -5962,7 +5985,7 @@
|
||||
}
|
||||
|
||||
/* Very small laptop / tablet landscape — 960px */
|
||||
@media (max-width: 960px) {
|
||||
@container dispatch (max-width: 960px) {
|
||||
.dispatch-container #sidebar {
|
||||
width: 250px;
|
||||
flex-basis: 250px;
|
||||
@@ -6578,7 +6601,7 @@
|
||||
}
|
||||
|
||||
/* Mobile — collapse the sidebar above the main panel, single-column stats */
|
||||
@media (max-width: 600px) {
|
||||
@container dispatch (max-width: 600px) {
|
||||
.dispatch-container .rider-info-mode {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -6601,7 +6624,7 @@
|
||||
}
|
||||
|
||||
/* ── Laptop Responsive Tuning (max-width: 1366px) ── */
|
||||
@media (max-width: 1366px) {
|
||||
@container dispatch (max-width: 1366px) {
|
||||
|
||||
/* Header adjustments */
|
||||
.dispatch-container #hdr {
|
||||
@@ -7247,7 +7270,7 @@
|
||||
color: var(--accent, #000000);
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
@container dispatch (max-width: 1366px) {
|
||||
.dispatch-container .compare-delta.is-collapsible.is-collapsed {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
@@ -7995,7 +8018,7 @@
|
||||
stacks on top, then the unified map, then the data panel scrolls
|
||||
below. The map gets a generous min-height so the layer-switcher
|
||||
overlay and timeline stay usable on tablets. */
|
||||
@media (max-width: 1100px) {
|
||||
@container dispatch (max-width: 1100px) {
|
||||
.dispatch-container #body.compare-mode {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(360px, 1fr) auto;
|
||||
@@ -9240,7 +9263,7 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@container dispatch (max-width: 720px) {
|
||||
.dispatch-container .da-picker-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -9615,7 +9638,7 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@container dispatch (max-width: 720px) {
|
||||
.dispatch-container .da-metric-grid-3 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -10162,7 +10185,7 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@container dispatch (max-width: 900px) {
|
||||
.dispatch-container .da-hero-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -10232,13 +10255,13 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
@container dispatch (max-width: 1100px) {
|
||||
.dispatch-container .da-health-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@container dispatch (max-width: 600px) {
|
||||
.dispatch-container .da-health-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -10588,7 +10611,7 @@
|
||||
the rider/batch sidebar flows below it full-width and scrolls. Desktop
|
||||
(≥ md) is untouched; every rule here is gated inside this media query.
|
||||
========================================================================= */
|
||||
@media (max-width: 768px) {
|
||||
@container dispatch (max-width: 768px) {
|
||||
|
||||
/* The standalone page un-does MainCard's 24px padding via negative margin
|
||||
+ 100vw-ish sizing; keep that, just allow the inner body to grow with
|
||||
@@ -10763,7 +10786,7 @@
|
||||
}
|
||||
|
||||
/* Date picker chip responsive utilities */
|
||||
@media (max-width: 1280px) {
|
||||
@container dispatch (max-width: 1280px) {
|
||||
.dispatch-container .date-chip-full {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -10779,7 +10802,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@container dispatch (max-width: 768px) {
|
||||
.dispatch-container .date-chip-nav {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -10859,3 +10882,113 @@
|
||||
line-height: 1.45;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ── Header: one line, compacting in steps ─────────────────────────────────
|
||||
The header is title+hub+Live+Batch | date | clock. It used to WRAP below
|
||||
1180px, which on a 1366 laptop with MileTruth open (≈920px here) left the
|
||||
clock alone on a second line under the title — it looked misaligned, and
|
||||
the row missed fitting by 3px. Instead of wrapping, each step below drops
|
||||
something the operator can do without, so the row stays on ONE line down
|
||||
to tablet width. Only phones (≤640px) get two lines, laid out on purpose.
|
||||
|
||||
Kept LAST in the file: earlier blocks (1366px etc.) pin #hdr's height and
|
||||
padding, and a rule of equal specificity placed before them would lose. */
|
||||
|
||||
/* ≤1180: drop the "BATCH:" caption (the picker still names the batch) and
|
||||
tighten gaps. At 920px this leaves ~50px to spare. */
|
||||
@container dispatch (max-width: 1180px) {
|
||||
.dispatch-container #hdr {
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.dispatch-container .logo {
|
||||
gap: 8px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.dispatch-container #dispatch-top-tabs {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dispatch-container .dx-batch-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-container .hdr-stats {
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.dispatch-container #clock {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ≤900 (tablet, or small laptop with the assistant open): the page title
|
||||
text goes (the D logo stays), Live becomes icon-only, the TODAY pill goes,
|
||||
and a long hub name truncates instead of pushing the row. */
|
||||
@container dispatch (max-width: 900px) {
|
||||
.dispatch-container .logo-name,
|
||||
.dispatch-container .dtt-label,
|
||||
.dispatch-container .date-chip-today-pill {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-container .dtt-tab {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.dispatch-container .logo-city-text {
|
||||
max-width: 96px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ≤640 (phones): one line is not physically possible, so two deliberate
|
||||
lines — [logo · hub · Live · Batch] then [date ········ clock], with the
|
||||
clock pinned right on the same line as the date rather than dropping
|
||||
under the logo on its own. */
|
||||
@container dispatch (max-width: 640px) {
|
||||
.dispatch-container #hdr {
|
||||
height: auto;
|
||||
min-height: 48px;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.dispatch-container .logo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dispatch-container .hdr-stats {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dispatch-container #clock {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ≤480 (phones): the title group itself can't fit hub + Live + Batch picker
|
||||
side by side (at 414px the picker was cut off), so let it wrap inside its
|
||||
own line too. */
|
||||
@container dispatch (max-width: 480px) {
|
||||
.dispatch-container .logo,
|
||||
.dispatch-container #dispatch-top-tabs {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
MdRestaurant,
|
||||
MdPublic,
|
||||
MdInventory2,
|
||||
MdTrendingUp,
|
||||
MdTrendingDown,
|
||||
MdAccountBalanceWallet,
|
||||
MdStraighten,
|
||||
MdLocationOn,
|
||||
@@ -361,6 +359,27 @@ function MapAutoResize({ trigger }) {
|
||||
const timers = [0, 60, 120, 200, 300, 380].map((t) => setTimeout(() => map.invalidateSize({ animate: false }), t));
|
||||
return () => timers.forEach(clearTimeout);
|
||||
}, [trigger, map]);
|
||||
|
||||
// The pulse above only runs for the layout changes it is told about. The map
|
||||
// box also changes size when the MileTruth panel opens or closes, when the
|
||||
// header wraps or unwraps, and when the window moves between breakpoints —
|
||||
// none of which fire a window resize. The map then kept drawing tiles for
|
||||
// its OLD width: dragging it showed a grey band on the right with no tiles.
|
||||
// Watching the container itself covers every cause at once.
|
||||
useEffect(() => {
|
||||
const el = map.getContainer();
|
||||
if (!el || typeof ResizeObserver === 'undefined') return undefined;
|
||||
let frame = 0;
|
||||
const observer = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => map.invalidateSize({ animate: false }));
|
||||
});
|
||||
observer.observe(el);
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3592,6 +3611,12 @@ const Dispatch = ({
|
||||
};
|
||||
|
||||
return (
|
||||
// .dispatch-shell is the size container Dispatch.css's breakpoints query.
|
||||
// They used to be viewport media queries, which can't see the MileTruth
|
||||
// dock: on a 1366px screen with the panel open this page gets ~920px but
|
||||
// every rule still behaved as if it had 1366px, so the header never wrapped
|
||||
// and the date picker and clock were clipped off the right edge.
|
||||
<div className={`dispatch-shell${embedded ? ' embedded' : ''}`}>
|
||||
<div className={`dispatch-container${embedded ? ' embedded' : ''}${compareOpen ? ' compare-open' : ''}`}>
|
||||
{!embedded && (
|
||||
<div id="hdr">
|
||||
@@ -3642,7 +3667,7 @@ const Dispatch = ({
|
||||
<span className="dtt-icon">
|
||||
<MdMap />
|
||||
</span>
|
||||
Live
|
||||
<span className="dtt-label">Live</span>
|
||||
</button>
|
||||
{shouldFetchLive && viewMode !== 'rider-info' && (
|
||||
<div className="relative inline-flex items-center ml-1" ref={batchMenuRef}>
|
||||
@@ -3652,7 +3677,7 @@ const Dispatch = ({
|
||||
className="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200/90 rounded-lg px-3 py-1.5 text-xs font-semibold text-slate-800 shadow-2xs transition-all cursor-pointer select-none"
|
||||
>
|
||||
<MdAccessTime className="text-slate-400 w-3.5 h-3.5 shrink-0" />
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Batch:</span>
|
||||
<span className="dx-batch-label text-[10px] font-bold text-slate-400 uppercase tracking-wider">Batch:</span>
|
||||
<span className="font-bold text-slate-900 text-xs">
|
||||
{BATCHES.find((b) => b.id === selectedBatch)?.label || 'All Batches'}
|
||||
</span>
|
||||
@@ -3726,40 +3751,16 @@ const Dispatch = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header right-cluster: profit/loss chip, total-orders pill, date picker.
|
||||
Sits to the LEFT of the running clock so the operator sees fleet
|
||||
health + current wave size + selected date together in one row. */}
|
||||
{/* Header right-cluster: date picker (plus a "Failed to load" notice
|
||||
when the live feed errors). The revenue chip and the
|
||||
Loading / "N orders" pill that used to sit here were removed on
|
||||
request — the sidebar tiles already show the order count, and the
|
||||
header now has room to stay on one line on more screens. The
|
||||
error notice stays: without it a failed load looks exactly like a
|
||||
day with no orders. */}
|
||||
<div className="hdr-stats">
|
||||
{(() => {
|
||||
const isLoss = activeStats.profit < 0;
|
||||
const amount = Math.abs(activeStats.profit);
|
||||
return (
|
||||
<span
|
||||
className={`strat-stat ${isLoss ? 'strat-stat-loss' : 'strat-stat-profit'}`}
|
||||
title={`${isLoss ? 'Loss' : 'Revenue'} (${activeStats.label})`}
|
||||
>
|
||||
<span className="strat-stat-icon">{isLoss ? <MdTrendingDown /> : <MdTrendingUp />}</span>
|
||||
<span className="strat-stat-label">{isLoss ? 'Loss' : 'Revenue'}</span>
|
||||
<span className="strat-stat-value">
|
||||
{isLoss ? '-' : ''}₹{amount.toFixed(0)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
|
||||
{shouldFetchLive && (
|
||||
<>
|
||||
{liveIsFetching && (
|
||||
<span className="live-status">
|
||||
<span className="live-dot" /> Loading {liveRows.length ? `· ${liveRows.length} loaded` : ''}
|
||||
</span>
|
||||
)}
|
||||
{!liveIsFetching && !liveIsError && (
|
||||
<span className="live-status live-status-ready">
|
||||
<span className="live-dot ready" /> {filteredLiveRows.length} orders
|
||||
<span className="live-status-sub"> / {liveRows.length} total</span>
|
||||
</span>
|
||||
)}
|
||||
{liveIsError && (
|
||||
<span className="live-status live-status-error">
|
||||
<span className="live-dot error" /> Failed to load
|
||||
@@ -7191,6 +7192,7 @@ const Dispatch = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2247,6 +2247,9 @@ export default function Home() {
|
||||
|
||||
{/* BOTTOM APP LAUNCHER & SUITES */}
|
||||
<footer className="w-full pt-4 pb-2 border-t border-slate-100">
|
||||
{/* Tiles wrap inside each group: on a phone a group column is ~160px and
|
||||
three 72px tiles didn't fit, so the row ran past the screen and
|
||||
the whole page scrolled sideways. */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8 max-w-5xl mx-auto">
|
||||
|
||||
{/* 1. OPERATIONS */}
|
||||
@@ -2254,7 +2257,7 @@ export default function Home() {
|
||||
<div className="text-[11px] font-bold uppercase tracking-wider text-slate-400 mb-3 px-1">
|
||||
OPERATIONS
|
||||
</div>
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
<div className="flex flex-wrap items-start gap-2.5 sm:gap-3">
|
||||
{/* Orders Tile */}
|
||||
<Link
|
||||
to="/doormile/orders"
|
||||
@@ -2314,7 +2317,7 @@ export default function Home() {
|
||||
<div className="text-[11px] font-bold uppercase tracking-wider text-slate-400 mb-3 px-1">
|
||||
FLEET
|
||||
</div>
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
<div className="flex flex-wrap items-start gap-2.5 sm:gap-3">
|
||||
{/* Staff Directory */}
|
||||
<Link
|
||||
to="/doormile/milers"
|
||||
@@ -2383,7 +2386,7 @@ export default function Home() {
|
||||
<div className="text-[11px] font-bold uppercase tracking-wider text-slate-400 mb-3 px-1">
|
||||
CLIENTS & REVENUE
|
||||
</div>
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
<div className="flex flex-wrap items-start gap-2.5 sm:gap-3">
|
||||
{/* Bookings.
|
||||
Labelled "Invoices", which this console does not have — the
|
||||
page is the customer-app booking list. Billing lives on the
|
||||
@@ -2442,7 +2445,7 @@ export default function Home() {
|
||||
<div className="text-[11px] font-bold uppercase tracking-wider text-slate-400 mb-3 px-1">
|
||||
REPORTS & AI
|
||||
</div>
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
<div className="flex flex-wrap items-start gap-2.5 sm:gap-3">
|
||||
{/* Orders Summary.
|
||||
Called "AI Reports", but nothing about the page it opens is
|
||||
AI — it is the daily volume and status aggregation, computed
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Settings() {
|
||||
{/* Unified Toolbar: Segmented Tabs on Left, Tab-Specific Action on Right */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
||||
{/* Pill Segmented Tabs */}
|
||||
<div className="inline-flex items-center gap-1 p-1 bg-slate-100/90 rounded-2xl border border-slate-200/80 shadow-2xs">
|
||||
<div className="no-scrollbar inline-flex max-w-full items-center gap-1 overflow-x-auto p-1 bg-slate-100/90 rounded-2xl border border-slate-200/80 shadow-2xs">
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
@@ -81,7 +81,7 @@ export default function Settings() {
|
||||
type="button"
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-xl text-xs sm:text-sm font-medium transition-all whitespace-nowrap cursor-pointer select-none',
|
||||
'flex shrink-0 items-center gap-2 px-3 sm:px-4 py-2 rounded-xl text-xs sm:text-sm font-medium transition-all whitespace-nowrap cursor-pointer select-none',
|
||||
isActive
|
||||
? 'bg-white text-slate-900 font-semibold shadow-xs border border-slate-200/80'
|
||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-200/50'
|
||||
|
||||
Reference in New Issue
Block a user