From 607693a04a13dd278948b7f2ae4a36286983878d Mon Sep 17 00:00:00 2001 From: Yashas Date: Thu, 10 Sep 2026 13:46:27 +0530 Subject: [PATCH] console: make the phone layout work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opened at 390px the masthead wrapped inside its own 56px bar and clipped — "ZURICH kotak", "Lead Desk / Lead to policy" and the user's name all on three lines each — and the nine-item rail stacked above the content, so every page began with a screenful of menu. The masthead now keeps one line: the mark, the app name, the person, with the secondary lines dropped below 900px and the app name below 560px, where it cannot fit beside the user menu without truncating. The rail becomes a drawer behind a button, over the page rather than above it, closing on navigation and on a tap outside. Desktop is untouched: the button is display:none, the rail is static, and the drawer rules live entirely inside the media query. Also: "Ops admin", not "OPS admin" — the acronym rule over-fired on a word. Verified at 390x844 and 1440x900: no horizontal scroll, nothing clipped in the masthead, the drawer closes on navigation, and the desktop rail keeps its transform and its hidden button. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 9 ++++++ src/layout/Shell.css | 70 ++++++++++++++++++++++++++++++++++------- src/layout/Shell.jsx | 25 +++++++++++++-- src/layout/UserMenu.jsx | 3 +- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 38ffbfb..e3a62ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,15 @@ Workflow config is seeded outside this repo, from `sm2/custom-apps/zurich-kotak/ - `src/components/ActivityForm.jsx` — renders the **live** activity schema and submits it back. `src/components/FileField.jsx` handles `file`/`ocr` fields; `src/components/Timeline.jsx` renders the audit trail as a story. +- `src/api/thread.js` — the WhatsApp conversation, assembled once from the audit rows + and shared by the timeline entry and the dialog. `turns` is every message deduped + (one submission is recorded up to three times); `episodes` are its contiguous bursts, + each carrying the workflow steps that ran before it; `byRow` says which steps sent a + message from inside their own trigger. The trail renders ONE entry per burst and never + a turn — a burst rather than the whole thread, because the payment exchange is the same + conversation resumed twenty minutes and four steps later, and one entry for all of it + would date those messages to the moment of the first. `THREAD_FIELDS` is why no step + quotes `customer_reply` / `customer_answer` itself: the thread owns them. Routing: `/stage/:stageUid`, `/lead/:instanceId`, `/add`; unauthenticated renders `Login` for every path. diff --git a/src/layout/Shell.css b/src/layout/Shell.css index 0e5cb10..1385309 100644 --- a/src/layout/Shell.css +++ b/src/layout/Shell.css @@ -19,24 +19,40 @@ color: #fff; } -.mast__logo { display: flex; align-items: center; gap: 10px; } +.mast__burger { + display: none; /* a phone-only control; see the media block */ + width: 34px; height: 34px; flex: none; + place-items: center; margin-left: -6px; + border: 0; border-radius: 8px; background: transparent; color: #fff; cursor: pointer; +} +.mast__burger svg { width: 20px; height: 20px; } +.mast__burger:hover { background: rgba(255, 255, 255, .12); } +.mast__burger:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255, 255, 255, .28); } + +.mast__logo { display: flex; align-items: center; gap: 10px; min-width: 0; } .mast__z { width: 30px; height: 30px; border-radius: 50%; background: #fff; color: var(--zk-navy); font-weight: 700; font-size: 17px; display: grid; place-items: center; } -.mast__word b { display: block; font-weight: 700; font-size: 17px; letter-spacing: .06em; line-height: 1.15; } +.mast__word b { display: block; white-space: nowrap; font-weight: 700; font-size: 17px; letter-spacing: .06em; line-height: 1.15; } .mast__word b span { font-weight: 500; letter-spacing: 0; } -.mast__word small { display: block; font-size: 11.5px; opacity: .75; margin-top: -1px; letter-spacing: .02em; } +.mast__word small { display: block; white-space: nowrap; font-size: 11.5px; opacity: .75; margin-top: -1px; letter-spacing: .02em; } .mast__sep { width: 1px; height: 26px; background: rgba(255, 255, 255, .22); } -.mast__app { font-size: 17px; font-weight: 600; } +.mast__app { font-size: 17px; font-weight: 600; white-space: nowrap; } .mast__app span { font-weight: 400; opacity: .7; margin-left: 8px; font-size: 15px; } .mast__user { margin-left: auto; } +.shell__scrim { + display: none; + position: fixed; inset: 56px 0 0 0; z-index: 35; + border: 0; padding: 0; background: rgba(11, 42, 91, .35); cursor: default; +} + /* ── Frame ────────────────────────────────────────────────────────────────── */ .shell__body { display: flex; flex: 1; min-height: 0; align-items: stretch; } @@ -133,13 +149,45 @@ } @media (max-width: 900px) { - .shell__body { flex-direction: column; } + /* ── PHONE ────────────────────────────────────────────────────────────── + The masthead keeps one line and one line only: the mark, the app name, + the person. Everything secondary goes, because a 56px bar that wraps is + a bar that clips, which is what it did. + + The rail becomes a drawer over the page rather than a block above it — + nine queues stacked at the top meant scrolling past the whole menu on + every screen before reaching a single lead. */ + .shell__top { gap: 12px; padding: 0 14px; } + .mast__burger { display: grid; } + .mast__sep, + .mast__word small, + .mast__app span { display: none; } + .mast__word b { font-size: 15.5px; } + .mast__app { font-size: 15.5px; } + + .shell__body { flex-direction: row; } .shell__nav { - position: static; width: 100%; min-height: 0; max-height: none; - border-right: 0; border-bottom: 1px solid var(--zk-line); - flex-direction: row; flex-wrap: wrap; padding: 10px 12px; + position: fixed; top: 56px; left: 0; bottom: 0; z-index: 40; + width: 268px; max-width: 82vw; min-height: 0; max-height: none; + border-right: 1px solid var(--zk-line); + transform: translateX(-100%); + transition: transform .22s var(--ease); + overflow-y: auto; overscroll-behavior: contain; } - .nav__h { display: none; } - .nav__group { flex-direction: row; flex-wrap: wrap; } - .shell__main { padding: 20px 16px 48px; } + .shell__nav.is-open { transform: none; box-shadow: 0 12px 40px rgba(11, 42, 91, .22); } + .shell__scrim { display: block; } + .shell__main { padding: 20px 16px 48px; min-width: 0; flex: 1; } +} + +@media (max-width: 560px) { + /* Below this the app name and the wordmark cannot both fit beside the user + menu without one of them truncating. The mark wins — it is the brand. */ + .mast__app { display: none; } + .um__id { display: none; } + .um__caret { display: none; } + .um__trigger { padding: 4px; } +} + +@media (prefers-reduced-motion: reduce) { + .shell__nav { transition: none; } } diff --git a/src/layout/Shell.jsx b/src/layout/Shell.jsx index 5da293b..9e1fb8b 100644 --- a/src/layout/Shell.jsx +++ b/src/layout/Shell.jsx @@ -1,4 +1,5 @@ -import { NavLink, Outlet } from 'react-router-dom' +import { useEffect, useState } from 'react' +import { NavLink, Outlet, useLocation } from 'react-router-dom' import { NAV_GROUPS, STAGES } from '../api/config.js' import { rolesOf, visibleStages } from '../api/permissions.js' import { useStageCounts } from '../api/portfolio.jsx' @@ -30,6 +31,12 @@ export default function Shell() { const roles = rolesOf(user) const stages = visibleStages(roles) const { counts, ready } = useStageCounts() + // On a phone the rail becomes a drawer. It is closed on every navigation, + // because a menu that stays open over the page you just chose is a menu you + // have to dismiss twice. Desktop never reads this — the rail is always there. + const [navOpen, setNavOpen] = useState(false) + const { pathname } = useLocation() + useEffect(() => { setNavOpen(false) }, [pathname]) const count = (s) => counts[s.uid] || { total: 0, waiting: 0, working: 0, urgent: 0 } @@ -58,6 +65,17 @@ export default function Shell() { return (
+
@@ -71,7 +89,10 @@ export default function Shell() {
-