14 KiB
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.csswith missing primitives. - Add missing primitive component styles to
apps/web/src/styles/global.cssandapps/web/src/styles/utilities.css. - Fix mobile navigation / FAB / bottom-sheet collisions via a shared
--mobile-nav-heighttoken. - 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
--space-7: 2.5rem;
--space-9: 5rem;
--space-12: 6rem;
2.2 Border Radius
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 16px;
--radius-full: 999px;
2.3 Line Height
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
2.4 Shadow / Elevation
--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
--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.
.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
.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.
.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
.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
.hint {
font-size: var(--font-size-sm);
color: var(--muted);
line-height: var(--line-height-normal);
}
3.6 Error Message
.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
.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
.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
.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.
.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).
.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
.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-navinutilities.css.mobile-edit-actionsinutilities.css.shell-content.mobileinutilities.css.start-tool-fabinglobal.css.mobile-bottom-sheet/.mobile-action-sheetif present
5.2 Start Tool FAB
.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
.shell-content.mobile {
padding-bottom: calc(var(--mobile-nav-height) + var(--space-3));
}
5.4 Mobile Edit Actions
.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.
.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.tsxlines 82, 95- Any other
0.7rem/11px/10pxinline 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.
.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.
.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.csscontains 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-visiblerings. - Inline
0.7remfont sizes are replaced with token-based classes. npm run typecheckpasses inapps/web.npm run lintpasses inapps/web.- No visual changes to desktop pages beyond improved spacing/consistency.
11. Verification Plan
- Run
cd apps/web && npm run typecheck. - Run
cd apps/web && npm run lint. - Search for each undefined class from the audit and confirm it now has a rule.
- Open the mobile detail view in a narrow viewport and verify layout.
- Open the SSH keys page and verify signing/verification sections.
- 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.