Files
headquarter/openspec/specs/web-ui-spacing-typography-rework.md
T

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.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

--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;
}
.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);
}
.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

.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.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.

.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.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.