diff --git a/STATUS.md b/STATUS.md index 8c98217..96ce198 100644 --- a/STATUS.md +++ b/STATUS.md @@ -70,7 +70,7 @@ header stacks instead of overlapping the title). | `npm run typecheck` (root + workspaces) | clean, no errors | | `npm test` (`vitest run`) | **528 tests pass across 97 files** (5 Postgres-gated skipped) | -The 416 figure is the live `vitest run` count measured 2026-07-19; the suite has +The 528 figure is the live `vitest run` count measured 2026-07-19; the suite has grown from the D18 go-live cluster through the D20–D33 work (tickets, module fields, renewals, cockpit, audit viewer, SMS gateway, module directory/portals/search, and the D31/D32 plaintext-credential change with its own migration + review). Test files diff --git a/apps/hq-web/src/app.css b/apps/hq-web/src/app.css index a1f0995..6c7ed05 100644 --- a/apps/hq-web/src/app.css +++ b/apps/hq-web/src/app.css @@ -275,9 +275,10 @@ button.dash-row:hover { background: var(--accent-soft); } own horizontal-scroll container from `overflow-x: auto` in Chromium, so at desktop widths a wide table (e.g. Tickets' 11 columns) was overflowing past the viewport and getting silently clipped by `.hq-content`'s overflow-x: hidden below, unreachable at - any width. The wrapper scrolls at every width now, not just <901px. */ + any width. The wrapper scrolls at every width now, not just <901px. + (`.wf-table-wrap` itself is styled in @sims/ui — packages/ui/src/tokens.css — next to + the other table.wf rules, since it's part of that component, not this app's shell.) */ .hq-content { overflow-x: hidden; } -.wf-table-wrap { overflow-x: auto; } /* Relationship-pulse ribbon (Client 360 overview). */ .pulse { diff --git a/apps/hq-web/src/pages/ClientDetail.tsx b/apps/hq-web/src/pages/ClientDetail.tsx index 1400c96..cd255ed 100644 --- a/apps/hq-web/src/pages/ClientDetail.tsx +++ b/apps/hq-web/src/pages/ClientDetail.tsx @@ -52,6 +52,9 @@ export function ClientDetail() { const id = useParams()['id'] ?? '' const nav = useNavigate() const [sp, setSp] = useSearchParams() + const rawTab = sp.get('tab') ?? 'overview' + const tab: TabKey = (TAB_KEYS as readonly string[]).includes(rawTab) ? (rawTab as TabKey) : 'overview' + const setTab = (k: string) => setSp(k === 'overview' ? {} : { tab: k }, { replace: true }) const client = useData(() => getClient(id), [id]) const modules = useData(getModules, []) const cms = useData(() => getClientModules(id), [id]) @@ -66,7 +69,9 @@ export function ClientDetail() { const [ticketPage, setTicketPage] = useState(1) const tickets = useData(() => getTickets({ clientId: id, page: ticketPage, pageSize: 50 }), [id, ticketPage]) // Same classification list the Tickets desk uses, so the Type column reads identically here. - const ticketTypes = useData(getTicketTypes, []) + // Only needed for that column, so only fetch it once the tickets tab is actually open — + // NewTicketDialog fetches its own copy for the new-ticket type picker, independent of this. + const ticketTypes = useData(() => (tab === 'tickets' ? getTicketTypes() : Promise.resolve(TICKET_TYPE_FALLBACK)), [tab === 'tickets']) const ticketTypeOptions = ticketTypes.data ?? TICKET_TYPE_FALLBACK // Undefined = closed. `moduleCode` pre-fills the module when the ticket is raised from // a module block ("Raise ticket for this module") rather than the Tickets tab button. @@ -99,10 +104,6 @@ export function ClientDetail() { const [linkingDoc, setLinkingDoc] = useState(false) const [docLinkError, setDocLinkError] = useState() - const rawTab = sp.get('tab') ?? 'overview' - const tab: TabKey = (TAB_KEYS as readonly string[]).includes(rawTab) ? (rawTab as TabKey) : 'overview' - const setTab = (k: string) => setSp(k === 'overview' ? {} : { tab: k }, { replace: true }) - const c = client.data if (client.error !== undefined) return if (c === undefined) return
diff --git a/apps/hq-web/src/pages/Dashboard.tsx b/apps/hq-web/src/pages/Dashboard.tsx index e39fbf9..8f7d94e 100644 --- a/apps/hq-web/src/pages/Dashboard.tsx +++ b/apps/hq-web/src/pages/Dashboard.tsx @@ -146,11 +146,15 @@ export function Dashboard() { {stalled.error !== undefined ? @@ -185,7 +189,7 @@ export function Dashboard() { * scrollable list, or an empty-state line when there's nothing to show. */ function Card(props: { title: string; count?: number; link?: { to: string; label?: string } - empty: string; isEmpty: boolean; children: ReactNode + empty: string; isEmpty: boolean; loading?: boolean; children: ReactNode }) { return (
@@ -194,7 +198,9 @@ function Card(props: { {props.count !== undefined && {props.count}} {props.link !== undefined && {props.link.label ?? 'All'} →} - {props.isEmpty ?
{props.empty}
:
{props.children}
} + {props.loading === true + ?
+ : props.isEmpty ?
{props.empty}
:
{props.children}
}
) } diff --git a/packages/ui/src/tokens.css b/packages/ui/src/tokens.css index 76bab4d..63499c8 100644 --- a/packages/ui/src/tokens.css +++ b/packages/ui/src/tokens.css @@ -194,6 +194,22 @@ input.wf:focus, select.wf:focus, textarea.wf:focus { .wf-toolbar .spacer { flex: 1; } /* ================= tables ================= */ +/* Horizontal-scroll wrapper (emitted by DataTable) so a wide table scrolls sideways + inside its own box instead of silently losing its rightmost columns off-page at + any width (rule 8 — no silent caps). Bounded to a fraction of the viewport height + and given a matching `overflow-y` so it is a real, self-contained scroll box: + `overflow-x: auto` alone would still leave `overflow-y` at its initial `visible`, + but per the CSS overflow spec a `visible`/non-`visible` pairing computes the + `visible` side to `auto` too — so without this the wrapper silently becomes an + unbounded vertical scroll container (height: auto, scrollTop always 0) and steals + `table.wf th`'s sticky-positioning context from `.hq-content` (the page's real + scroller) for nothing in return: the header can never offset and just scrolls out + of view with the rest of the page. Giving the wrapper an actual bound restores a + working scrollport, so the sticky header sticks again — now against this box + rather than the page. A short table never reaches the bound (renders exactly as + before, no inner scrollbar); a long one scrolls inside it, same pattern already + used for `.dash-list`. */ +.wf-table-wrap { overflow: auto; max-height: 70vh; } table.wf { border-collapse: separate; border-spacing: 0; width: 100%; background: var(--bg-raised); border: 1px solid var(--border);