# OpenSpec Spec: Web UI Foundations — Spacing, Typography & Visual Rhythm ## Change `web-ui-spacing-typography-rework` — Pass 1: Foundations ## Parent Proposal `openspec/proposals/web-ui-spacing-typography-rework.md` ## Status spec --- ## 1. Scope of This Spec This spec covers **Pass 1 (Foundations)** only: - Expand `apps/web/src/styles/tokens.css` with missing primitives. - Add missing primitive component styles to `apps/web/src/styles/global.css` and `apps/web/src/styles/utilities.css`. - Fix mobile navigation / FAB / bottom-sheet collisions via a shared `--mobile-nav-height` token. - Replace critical inline font-size violations (`0.7rem`) with token values. - Add visible focus states to interactive controls. - Provide full CSS for the currently unstyled mobile detail view. Out of scope for Pass 1: - Refactoring inline styles in editor components (Pass 2). - Modal/dialog unification (Pass 3). - Card/badge consolidation across page CSS files (Pass 3). - Breakpoint standardization beyond the safe-area/mobile-nav work (Pass 3). --- ## 2. Token Additions Add to `apps/web/src/styles/tokens.css` **without renaming or removing existing tokens**. ### 2.1 Spacing ```css --space-7: 2.5rem; --space-9: 5rem; --space-12: 6rem; ``` ### 2.2 Border Radius ```css --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px; --radius-xl: 16px; --radius-full: 999px; ``` ### 2.3 Line Height ```css --line-height-tight: 1.25; --line-height-normal: 1.5; --line-height-relaxed: 1.75; ``` ### 2.4 Shadow / Elevation ```css --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); --shadow-xl: 0 12px 40px rgba(0, 0, 0, 0.18); ``` ### 2.5 Layout & Touch ```css --touch-target: 44px; --sidebar-width: 280px; --mobile-nav-height: calc(64px + env(safe-area-inset-bottom, 0px)); ``` ### 2.6 Token Documentation Header Add a short comment block at the top of `tokens.css` describing the naming convention and scales, so future contributors know which token to reach for. --- ## 3. Primitive Component Styles Add the following classes to `apps/web/src/styles/global.css` unless noted otherwise. ### 3.1 Buttons Unify the existing ad-hoc button usage. The codebase already references `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-sm` in many places. ```css .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-height: var(--touch-target); padding: var(--space-2) var(--space-4); border: 1px solid transparent; border-radius: var(--radius-md); font-size: var(--font-size-sm); font-weight: 500; line-height: var(--line-height-tight); background: var(--panel); color: var(--ink); cursor: pointer; transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease; } .btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; } .btn-primary { background: var(--brand); color: var(--primary-fg); border-color: var(--brand); } .btn-primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); } .btn-secondary { background: var(--panel); color: var(--ink); border-color: var(--border); } .btn-secondary:hover { background: var(--bg); } .btn-sm { min-height: calc(var(--touch-target) - 8px); padding: var(--space-1) var(--space-3); font-size: var(--font-size-xs); } .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; } ``` ### 3.2 Link Button ```css .link-button { display: inline-flex; align-items: center; gap: var(--space-1); padding: 0; background: transparent; border: none; color: var(--brand); font-size: inherit; font-weight: 500; cursor: pointer; text-decoration: none; } .link-button:hover { text-decoration: underline; } .link-button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-sm); } ``` ### 3.3 Form Inputs Add a baseline `.form-input` class and a `.form-textarea` modifier. ```css .form-input, .form-textarea { width: 100%; min-height: var(--touch-target); padding: var(--space-2) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--panel); color: var(--ink); font-size: var(--font-size-base); line-height: var(--line-height-normal); } .form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 20%, transparent); } .form-input::placeholder, .form-textarea::placeholder { color: var(--muted); } .form-textarea { min-height: 6rem; resize: vertical; } ``` ### 3.4 Checkbox Label ```css .checkbox-label { display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--touch-target); font-size: var(--font-size-sm); color: var(--ink); cursor: pointer; } .checkbox-label input[type="checkbox"] { width: 1.125rem; height: 1.125rem; accent-color: var(--brand); } ``` ### 3.5 Hint Text ```css .hint { font-size: var(--font-size-sm); color: var(--muted); line-height: var(--line-height-normal); } ``` ### 3.6 Error Message ```css .form-error, .error-message { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: var(--danger-light); color: var(--danger); font-size: var(--font-size-sm); line-height: var(--line-height-normal); } ``` ### 3.7 Alert Banner ```css .alert { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--panel); color: var(--ink); font-size: var(--font-size-sm); line-height: var(--line-height-normal); } .alert-error { background: var(--danger-light); border-color: color-mix(in srgb, var(--danger) 30%, transparent); color: var(--danger); } .alert-warning { background: var(--warning-light); border-color: color-mix(in srgb, var(--warning) 30%, transparent); color: var(--warning); } .alert-info { background: var(--info-light); border-color: color-mix(in srgb, var(--info) 30%, transparent); color: var(--info); } ``` ### 3.8 Loading State ```css .loading-state { display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-4); color: var(--muted); font-size: var(--font-size-sm); } ``` ### 3.9 Badges ```css .badge { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--space-2); border-radius: var(--radius-full); font-size: var(--font-size-xs); font-weight: 600; line-height: var(--line-height-tight); background: var(--bg); color: var(--muted); border: 1px solid var(--border); } .badge-success { background: var(--success-light); color: var(--success); border-color: color-mix(in srgb, var(--success) 30%, transparent); } .badge-secondary { background: var(--info-light); color: var(--info); border-color: color-mix(in srgb, var(--info) 30%, transparent); } ``` ### 3.10 Text Utilities Add `.text-muted` as an alias for existing muted text patterns. The codebase references `.text-muted` in `mobile-detail-view.tsx`. ```css .text-muted { color: var(--muted); } ``` --- ## 4. Mobile Detail View Styles Add to `apps/web/src/styles/utilities.css` (these classes are referenced only in mobile components and fit the utilities file). ```css .mobile-detail-view { display: flex; flex-direction: column; min-height: 100%; background: var(--bg); } .mobile-detail-header { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); background: var(--panel); border-bottom: 1px solid var(--border); } .mobile-detail-header-content { flex: 1; min-width: 0; } .mobile-detail-title { margin: 0; font-size: var(--font-size-lg); line-height: var(--line-height-tight); } .mobile-detail-subtitle { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--font-size-sm); line-height: var(--line-height-normal); } .mobile-detail-actions { display: flex; gap: var(--space-2); } .mobile-detail-fields { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3); } .mobile-detail-field { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-3); background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md); } .mobile-detail-field-label { font-size: var(--font-size-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); } .mobile-detail-field-value { font-size: var(--font-size-base); color: var(--ink); line-height: var(--line-height-normal); word-break: break-word; } .mobile-detail-code { overflow: auto; max-width: 100%; font-size: var(--font-size-sm); line-height: var(--line-height-normal); } ``` ### 4.1 Mobile List Search ```css .mobile-list-search { padding: var(--space-3); border-bottom: 1px solid var(--border); background: var(--panel); } .mobile-list-search-input { width: 100%; min-height: var(--touch-target); padding: var(--space-2) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg); color: var(--ink); font-size: var(--font-size-base); } .mobile-list-search-input:focus { outline: none; border-color: var(--brand); } ``` --- ## 5. Mobile Nav / FAB / Bottom-Sheet Collisions ### 5.1 Replace Magic Numbers Wherever the mobile nav height is hardcoded as `64px`, switch to `var(--mobile-nav-height)`. Affected selectors (verify current line numbers before editing): - `.mobile-nav` in `utilities.css` - `.mobile-edit-actions` in `utilities.css` - `.shell-content.mobile` in `utilities.css` - `.start-tool-fab` in `global.css` - `.mobile-bottom-sheet` / `.mobile-action-sheet` if present ### 5.2 Start Tool FAB ```css .start-tool-fab { position: fixed; bottom: calc(var(--mobile-nav-height) + var(--space-3)); right: var(--space-4); z-index: 50; } ``` ### 5.3 Shell Content Padding ```css .shell-content.mobile { padding-bottom: calc(var(--mobile-nav-height) + var(--space-3)); } ``` ### 5.4 Mobile Edit Actions ```css .mobile-edit-actions { position: fixed; bottom: var(--mobile-nav-height); /* existing left/right rules preserved */ } ``` --- ## 6. Focus States Add visible focus rings to the following selectors in `global.css`/`utilities.css`. ```css .nav-item:focus-visible, .mobile-nav-item:focus-visible, .tab:focus-visible, .tree-entry:focus-visible, .btn:focus-visible, .form-input:focus-visible, .form-textarea:focus-visible, .link-button:focus-visible, .checkbox-label input:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; } ``` For `.nav-item-active` and `.tab.active`, use `--primary-fg` or a high-contrast outline color if `--brand` is insufficient. --- ## 7. Inline Font-Size Violations Replace inline `style={{ fontSize: "0.7rem" }}` and similar hardcoded values with token-based classes. Known locations (verify before editing): - `ConfigProfileListSidebar.tsx` lines 82, 95 - Any other `0.7rem`/`11px`/`10px` inline styles found during implementation Use `.text-xs` or add a `.text-xs` utility mapped to `--font-size-xs` if it does not already exist. If a component truly needs smaller than `--font-size-xs`, escalate instead of adding a new smaller token. --- ## 8. SSH Key Page Styles Add missing styles for the signing/verification sections in `SSHKeyList.tsx`. ```css .key-signing, .key-verification { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3); background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md); margin-top: var(--space-3); } .signature-result, .verify-result { padding: var(--space-3); border-radius: var(--radius-md); background: var(--bg); border: 1px solid var(--border); font-family: "IBM Plex Mono", monospace; font-size: var(--font-size-sm); word-break: break-all; } ``` --- ## 9. Profile Page Avatar Styles Add fallback styles for the avatar section used in `ProfilePage.tsx`. ```css .profile-avatar-section { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); padding: var(--space-4); background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md); } .avatar-preview, .avatar-placeholder { width: 6rem; height: 6rem; border-radius: var(--radius-full); overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--bg); border: 2px solid var(--border); } .avatar-image { width: 100%; height: 100%; object-fit: cover; } .avatar-placeholder { font-size: var(--font-size-2xl); font-weight: 600; color: var(--muted); } ``` --- ## 10. Acceptance Criteria - [ ] `tokens.css` contains all new tokens from §2 and a documentation header. - [ ] All class names listed in §3–§9 have working CSS rules. - [ ] No referenced class in the audited report remains undefined (verified by searching for the class names and confirming a rule exists). - [ ] Mobile detail view renders with proper padding, label/value hierarchy, and bounded code blocks. - [ ] Mobile nav, FAB, edit actions, and shell content no longer collide on devices with safe-area insets. - [ ] All interactive controls have visible `:focus-visible` rings. - [ ] Inline `0.7rem` font sizes are replaced with token-based classes. - [ ] `npm run typecheck` passes in `apps/web`. - [ ] `npm run lint` passes in `apps/web`. - [ ] No visual changes to desktop pages beyond improved spacing/consistency. --- ## 11. Verification Plan 1. Run `cd apps/web && npm run typecheck`. 2. Run `cd apps/web && npm run lint`. 3. Search for each undefined class from the audit and confirm it now has a rule. 4. Open the mobile detail view in a narrow viewport and verify layout. 5. Open the SSH keys page and verify signing/verification sections. 6. Open the profile page and verify avatar styling. --- ## 12. Next Phase After this spec is approved, create **tasks** for Pass 1 implementation, then delegate to `sdd-apply`.