654 lines
14 KiB
Markdown
654 lines
14 KiB
Markdown
# 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`.
|