From 46cce8f44e84ab1a969f3a7e6280687e688bea13 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:49:14 +0000 Subject: [PATCH] Responsive UX/UI system: tokens, hooks, drawer nav, workspace gate, a11y primitives Adds a CSS-first responsive foundation with breakpoint tokens, fluid typography/spacing via clamp(), and matchMedia-driven hooks. Portal chrome swaps to an off-canvas drawer below md; workspace (IDE) shows a friendly mobile gate below md with links to portal routes. Details in docs/ux-responsive-strategy.md. Co-Authored-By: Nakamoto, S --- docs/ux-responsive-strategy.md | 291 ++++++++++++++++ index.html | 13 +- src/App.tsx | 38 ++- src/Portal.tsx | 8 +- src/components/a11y/SkipToContent.tsx | 16 + src/components/a11y/VisuallyHidden.tsx | 11 + src/components/portal/PortalLayout.tsx | 127 +++++-- src/hooks/useBreakpoint.ts | 55 ++++ src/hooks/useMediaQuery.ts | 37 +++ src/hooks/useOrientation.ts | 10 + src/hooks/useReducedMotion.ts | 12 + src/main.tsx | 6 + src/styles/a11y.css | 71 ++++ src/styles/responsive.css | 438 +++++++++++++++++++++++++ src/styles/tokens.css | 132 ++++++++ 15 files changed, 1242 insertions(+), 23 deletions(-) create mode 100644 docs/ux-responsive-strategy.md create mode 100644 src/components/a11y/SkipToContent.tsx create mode 100644 src/components/a11y/VisuallyHidden.tsx create mode 100644 src/hooks/useBreakpoint.ts create mode 100644 src/hooks/useMediaQuery.ts create mode 100644 src/hooks/useOrientation.ts create mode 100644 src/hooks/useReducedMotion.ts create mode 100644 src/styles/a11y.css create mode 100644 src/styles/responsive.css create mode 100644 src/styles/tokens.css diff --git a/docs/ux-responsive-strategy.md b/docs/ux-responsive-strategy.md new file mode 100644 index 0000000..82aea3b --- /dev/null +++ b/docs/ux-responsive-strategy.md @@ -0,0 +1,291 @@ +# Responsive UX/UI strategy + +This document describes the responsive design system added to the +CurrenciCombo portal. It is intended to be the source of truth for +future responsive work. + +## Goals + +The portal ships to a mixed audience that includes treasury operators +at desks (≥ 1440 px displays), branch-office users on 13" laptops, +tablets in landscape, and mobile phones for on-call auditors. The +system has to adapt across all of those without: + +- horizontal scroll at any viewport ≥ 320 px, +- layout shift on resize (no CLS), +- unusable tap targets on touch devices, +- losing keyboard navigation on desktop, or +- degrading desktop performance. + +## Architecture + +The responsive system is CSS-first and token-driven. There is **one** +runtime hook (`useBreakpoint`) used by the handful of components that +have to swap structure (nav, workspace gate) — everything else is +pure CSS. + +### Layered stylesheet order + +`src/main.tsx` imports styles in this order: + +1. `src/styles/tokens.css` — design tokens (no visual side effects). +2. `src/index.css` — the original desktop baseline stylesheet. +3. `src/styles/responsive.css` — media-query overrides. +4. `src/styles/a11y.css` — focus ring, skip-to-content, sr-only, + touch-target enforcement. + +Order matters: overrides must appear after the baseline so that rules +at equal specificity win. Nothing in `responsive.css` or `a11y.css` +changes behavior at `≥ lg` — every rule is wrapped in a +`@media (max-width: …)` or `@media (pointer: coarse)` query. + +### Tokens + +All sizing values that change with viewport live in +`src/styles/tokens.css`: + +| Category | Tokens | +| ------------- | -------------------------------------------- | +| Breakpoints | `--bp-xs 0`, `--bp-sm 480`, `--bp-md 768`, `--bp-lg 1024`, `--bp-xl 1440` (px) | +| Fluid type | `--fs-2xs` through `--fs-4xl`, all `clamp()` | +| Fluid spacing | `--space-0` through `--space-12`, all `clamp()` | +| Line height | `--lh-tight`, `--lh-snug`, `--lh-normal`, `--lh-relaxed` | +| Motion | `--motion-fast 120ms`, `--motion-base 200ms`, `--motion-slow 320ms`, `--motion-ease` | +| Z-index | `--z-base`, `--z-sticky`, `--z-drawer-backdrop`, `--z-drawer`, `--z-dropdown`, `--z-modal`, `--z-toast`, `--z-tooltip`, `--z-focus` | +| Focus ring | `--focus-ring-color #60a5fa`, `--focus-ring-offset 2px`, `--focus-ring-width 2px` | +| Safe area | `--safe-top/right/bottom/left` via `env()` | +| Tap target | `--tap-min 44px` | + +### Fluid scaling formula + +Typography and spacing use `clamp(min, preferred, max)` where +`preferred` has a `vw` term so values interpolate continuously between +the smallest design viewport (320 px) and the largest (1440 px). This +is what the spec calls "avoid hard breakpoints where possible; prefer +fluid responsiveness" — between breakpoints, sizes grow continuously +rather than snapping. + +Example, `--fs-base: clamp(0.8125rem, 0.77rem + 0.23vw, 1rem)`: + +- at 320 px → 13 px (minimum), +- at 1024 px → ~15.1 px, +- at 1440 px → 16 px (maximum clamp). + +### Breakpoints + +| Name | CSS range | Semantic | +| ---- | ----------------------- | ---------------------- | +| xs | 0 – 479 px | Small phones, portrait | +| sm | 480 – 767 px | Larger phones | +| md | 768 – 1023 px | Tablets | +| lg | 1024 – 1439 px | Laptops | +| xl | ≥ 1440 px | External displays | + +Semantic aliases exposed by `useBreakpoint()`: + +- `isMobile` = `< md` (phones) +- `isTablet` = `md – lg` (tablets, portrait iPads) +- `isDesktop` = `≥ lg` (laptops, external monitors) + +### Hooks + +- **`useMediaQuery(query)`** — subscribes to a matchMedia `change` + event via `useSyncExternalStore`. No resize listener — re-renders + only when the query result actually flips. +- **`useBreakpoint()`** — returns `{ current, isXs/Sm/Md/Lg/Xl, + isMobile/Tablet/Desktop }`. Composed of four `useMediaQuery` calls + using the `--bp-*` values from the tokens. +- **`useReducedMotion()`** — wraps `(prefers-reduced-motion: reduce)` + so components that animate in JS can disable animations. +- **`useOrientation()`** — returns `'portrait'` or `'landscape'`. + +All four hooks are SSR-safe (return `false` / default in a non-DOM +environment). + +## Component inventory and strategy + +### Global primitives + +- **Skip-to-content link** — rendered at the top of the tree via + ``. Visually hidden until `:focus`, then slides in at + top-left. Target is `
` in + `PortalLayout`. +- **VisuallyHidden** — `sr-only` wrapper, for adding accessible text + without visual presence. + +### Portal chrome + +- **Topbar** (`.portal-topbar`): fixed 48 px height at all sizes. + Below `md`, the "Production" environment badge and the "Solace Bank + Group" logo text are hidden so the hamburger, logo mark and user + avatar fit on a phone without wrapping. +- **Hamburger toggle** (`.portal-menu-toggle`): new `