SPB Git

spb/airiskindex Public

The most methodologically rigorous, fully transparent AI job-exposure index.

TypeScript 88% Python 6.1% SQL 2.7% CSS 1.2% JavaScript 0.9% Shell 0.8%

fix(web): mobile layout — Tailwind content missed components/, truncate min-w-0 overflow, drawer portal

- tailwind.config content now scans components/ and lib/ (classes unique to
  those files were never generated: drawer fixed/inset/animation, search
  dropdown styling)
- min-w-0 on truncate flex children + ranking grid children: page no longer
  overflows horizontally, mobile browsers no longer zoom out
- drawer rendered via portal to <body> (blurred sticky header is a containing
  block for fixed descendants)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 6 days ago (Aug 5, 2026) parent 4754708

Showing 5 changed files with +27 and −17

modified apps/web/app/occupations/[code]/page.tsx +1 −1
@@ -373,7 +373,7 @@ export default async function OccupationPage({
373 373 href={`/occupations/${row.occupationCode}`}
374 374 className="flex items-baseline gap-3 px-5 py-3 hover:bg-[var(--wash)]"
375 375 >
376 <span className="truncate text-sm">{row.occupation.title}</span>
376 + <span className="min-w-0 truncate text-sm">{row.occupation.title}</span>
377 377 <span className="ml-auto text-sm font-semibold tabular-nums">
378 378 {row.substitution.toFixed(0)}
379 379 </span>
modified apps/web/app/occupations/page.tsx +1 −1
@@ -68,7 +68,7 @@ export default async function OccupationsPage(): Promise<JSX.Element> {
68 68 href={`/occupations/${occupation.code}`}
69 69 className="flex items-baseline gap-2 py-2 hover:bg-[var(--wash)]"
70 70 >
71 <span className="truncate text-sm">{occupation.title}</span>
71 + <span className="min-w-0 truncate text-sm">{occupation.title}</span>
72 72 {score !== undefined && (
73 73 <span className="ml-auto shrink-0 rounded-full bg-[var(--seq-track)] px-2 py-0.5 text-xs font-semibold tabular-nums">
74 74 {score.toFixed(0)}
modified apps/web/app/page.tsx +4 −4
@@ -89,7 +89,7 @@ function RankList({ rows, startRank }: { rows: RankRow[]; startRank?: number }):
89 89 {startRank + index}
90 90 </span>
91 91 )}
92 <span className="truncate font-medium">{row.occupation.title}</span>
92 + <span className="min-w-0 truncate font-medium">{row.occupation.title}</span>
93 93 <span className="ml-auto pl-3 text-sm font-semibold tabular-nums">
94 94 {row.substitution.toFixed(0)}
95 95 </span>
@@ -171,11 +171,11 @@ export default async function HomePage(): Promise<JSX.Element> {
171 171 from rater disagreement — a wide band is a claim we hold loosely.
172 172 </p>
173 173 <div className="mt-6 grid gap-6 lg:grid-cols-2">
174 <div>
174 + <div className="min-w-0">
175 175 <h3 className="mb-2 text-sm font-medium text-[var(--ink-2)]">Most exposed</h3>
176 176 <RankList rows={top as RankRow[]} startRank={1} />
177 177 </div>
178 <div>
178 + <div className="min-w-0">
179 179 <h3 className="mb-2 text-sm font-medium text-[var(--ink-2)]">Least exposed</h3>
180 180 <RankList rows={bottom as RankRow[]} />
181 181 </div>
@@ -196,7 +196,7 @@ export default async function HomePage(): Promise<JSX.Element> {
196 196 className="block border-b border-[var(--grid)] px-5 py-3 last:border-b-0 hover:bg-[var(--wash)]"
197 197 >
198 198 <div className="flex items-baseline gap-3">
199 <span className="truncate text-sm">
199 + <span className="min-w-0 truncate text-sm">
200 200 {SOC_MAJOR_GROUPS[group.prefix] ?? group.prefix}
201 201 </span>
202 202 <span className="ml-auto text-xs text-[var(--muted)]">
modified apps/web/components/mobile-menu.tsx +15 −10
@@ -2,6 +2,7 @@
2 2
3 3 import Link from "next/link";
4 4 import { useEffect, useState } from "react";
5 +import { createPortal } from "react-dom";
5 6
6 7 export function MobileMenu({ items }: { items: ReadonlyArray<readonly [string, string]> }): JSX.Element {
7 8 const [open, setOpen] = useState(false);
@@ -40,14 +41,17 @@ export function MobileMenu({ items }: { items: ReadonlyArray<readonly [string, s
40 41 )}
41 42 </button>
42 43
43 {open && (
44 <>
45 <button
46 type="button"
47 aria-label="Close menu"
48 onClick={() => setOpen(false)}
49 className="fixed inset-0 z-40 cursor-default bg-black/25 backdrop-blur-[2px]"
50 />
44 + {/* Portal to <body>: the blurred sticky header is a containing block for
45 + fixed descendants, which would trap and stretch the drawer. */}
46 + {open &&
47 + createPortal(
48 + <>
49 + <button
50 + type="button"
51 + aria-label="Close menu"
52 + onClick={() => setOpen(false)}
53 + className="fixed inset-0 z-40 cursor-default bg-black/25 backdrop-blur-[2px]"
54 + />
51 55 {/* Drawer sliding in from the right */}
52 56 <nav className="fixed right-0 top-0 z-50 flex h-dvh w-72 max-w-[85vw] animate-[drawer-in_180ms_ease-out] flex-col border-l border-[var(--border)] bg-[var(--surface-1)] shadow-2xl">
53 57 <div className="flex items-center justify-between border-b border-[var(--grid)] px-5 py-3.5">
@@ -85,8 +89,9 @@ export function MobileMenu({ items }: { items: ReadonlyArray<readonly [string, s
85 89 </a>
86 90 </p>
87 91 </nav>
88 </>
89 )}
92 + </>,
93 + document.body,
94 + )}
90 95 </div>
91 96 );
92 97 }
modified apps/web/tailwind.config.ts +6 −1
@@ -1,7 +1,12 @@
1 1 import type { Config } from "tailwindcss";
2 2
3 3 export default {
4 content: ["./app/**/*.{ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"],
4 + content: [
5 + "./app/**/*.{ts,tsx}",
6 + "./components/**/*.{ts,tsx}",
7 + "./lib/**/*.{ts,tsx}",
8 + "../../packages/ui/src/**/*.{ts,tsx}",
9 + ],
5 10 theme: {
6 11 extend: {},
7 12 },
8 13