feat(web/ui): consolidate dialogs, cards, and breakpoints (Pass 3)

- Unify modal/dialog system; .modal-* are now aliases of .dialog-*
- Migrate WorkspacesPage, start-tool-modal, merge-dialog, workspace-tools-panel to .dialog-*
- Add .card-sm/.card-md/.card-lg/.card-elevated/.card-borderless modifiers
- Apply card utilities across workspaces, projects, ssh-keys, workspace-detail, git-history
- Remove duplicated card-like background/border/padding from page CSS
- Remove 860px breakpoint; standardize on 767px/768px mobile split
- Add docs/development/ui-review-checklist.md
- Archive web-ui-spacing-typography-rework OpenSpec change
This commit is contained in:
Developer
2026-06-16 15:27:45 +00:00
parent 4f4939406f
commit 6e419f815d
25 changed files with 752 additions and 202 deletions
@@ -1,457 +0,0 @@
# OpenSpec Spec: Web UI Component Cleanup — Inline Styles, Forms, Badges
## Change
`web-ui-spacing-typography-rework` — Pass 2: Component Cleanup
## Parent Proposal
`openspec/proposals/web-ui-spacing-typography-rework.md`
## Parent Spec
`openspec/specs/web-ui-spacing-typography-rework.md`
## Status
spec
---
## 1. Scope of This Spec
This spec covers **Pass 2 (Component Cleanup)** only:
- Refactor static inline styles in the highest-volume editor/sidebar components into token-based utility classes.
- Replace hardcoded `280px` sidebars with the `--sidebar-width` token.
- Unify `.form-group` and `.form-field` into a single pattern.
- Consolidate status badges/chips into a single `.status-badge` family.
- Add the missing utility classes needed by the refactor (`.stack-*`, `.row-*`, `.card-*`, `.form-section`, `.form-row`, `.form-help`, `.text-error`, `.muted`, `.ghost-button`, `.secondary-button`, `.dialog-actions`).
Out of scope for Pass 2:
- Token additions (Pass 1, already done).
- Missing primitive classes like `.btn` or `.alert` (Pass 1, already done).
- Modal/dialog unification (Pass 3).
- Card consolidation across page CSS files (Pass 3).
- Breakpoint standardization beyond sidebar width (Pass 3).
---
## 2. Components to Refactor
The following components have high inline-style counts and must be refactored to use utility classes. **Only static layout styles should move to CSS**; dynamic values (e.g. drag-over background, computed widths) may remain inline.
### 2.1 `apps/web/src/components/features/tool/manifest-editor.tsx` (~70 inline styles)
Typical patterns:
- `display: "flex", gap: "..."``.row`, `.row-sm`, `.row-md`, `.stack`, `.stack-sm`, `.stack-md`
- `padding: "..."``.p-2`, `.p-3`, `.p-4`
- `marginBottom: "..."``.mb-2`, `.mb-3`, `.mb-4`
- `background: "var(--panel)"``.card`, `.card-sm`, `.card-md`
- `borderRadius: "0.375rem"` / `10px``.rounded-md`, `.rounded-lg`
- `border: "1px solid var(--border)"``.card` already provides it
- `fontSize: "0.875rem"``.text-sm`
- `fontFamily: "monospace"``.font-mono`
- `flex: 1``.flex-1`
- `overflow: "auto"``.overflow-auto`
- `textAlign: "center"``.text-center`
- `justifyContent: "space-between"``.justify-between`
- `alignItems: "center"``.items-center`
- `cursor: "pointer"``.cursor-pointer`
Required new utilities:
- `.row`, `.row-sm` (gap: `--space-2`), `.row-md` (gap: `--space-3`), `.row-lg` (gap: `--space-4`)
- `.stack`, `.stack-sm`, `.stack-md`, `.stack-lg`, `.stack-xl`
- `.items-start`, `.items-center`, `.items-end`
- `.justify-between`, `.justify-center`, `.justify-end`
- `.flex-1`, `.flex-none`
- `.text-center`, `.text-left`, `.text-right`
- `.font-mono`
- `.cursor-pointer`
- `.overflow-auto`, `.overflow-hidden`
- `.p-1` through `.p-5`, `.px-*`, `.py-*`
- `.m-0`, `.mb-1` through `.mb-6`, `.mt-*`, `.ml-*`, `.mr-*`
- `.w-full`, `.min-w-0`, `.max-w-prose`, `.max-w-form`
- `.rounded-sm`, `.rounded-md`, `.rounded-lg`, `.rounded-xl`, `.rounded-full`
### 2.2 `apps/web/src/components/features/config-profiles/ConfigProfileEditorPanel.tsx` (~51 inline styles)
Same utility patterns as above. In addition:
- Replace inline success-banner styles with `.alert-success` (add if missing).
- Replace inline drag item styles with `.drag-item`, `.drag-item-active` utilities.
- Replace inline sticky dialog-actions styles with the existing `.dialog-actions` class plus a `.sticky-footer` modifier if needed.
### 2.3 `apps/web/src/components/features/tool-workshop/ToolTypeEditorPanel.tsx` (~20 inline styles)
Same patterns as ConfigProfileEditorPanel. The empty-state and form header styles should use shared utilities.
### 2.4 `apps/web/src/components/features/tool-workshop/ToolTypeListSidebar.tsx` (~12 inline styles)
- Replace `style={{ width: "280px", minWidth: "280px" }}` with `style={{ width: "var(--sidebar-width)", minWidth: "var(--sidebar-width)" }}` or a `.sidebar` class.
- Replace list-item inline spacing with `.sidebar-item` utilities.
### 2.5 `apps/web/src/components/features/config-profiles/ConfigProfileListSidebar.tsx` (~14 inline styles)
- Same sidebar width change as ToolTypeListSidebar.
- Replace inline badge styles with `.badge`/`.badge-secondary`.
- Replace inline delete button styles with `.ghost-button.small` or `.btn-icon`.
### 2.6 `apps/web/src/components/features/git/git-mount-editor.tsx` (~31 inline styles)
Same stack/row/card/gap/padding/margin utilities as manifest-editor.
---
## 3. Sidebar Width Tokenization
Both sidebars currently hardcode `280px`. Change to use the token introduced in Pass 1.
### Option A: Inline style with CSS variable
```tsx
<div style={{ width: "var(--sidebar-width)", minWidth: "var(--sidebar-width)" }}>
```
### Option B: Utility class
```css
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: var(--panel);
}
```
Use **Option A** if the surrounding layout already has custom styles; use **Option B** if the sidebars can share more structure. The spec requires at minimum that `280px` is replaced by `var(--sidebar-width)`.
---
## 4. Form Pattern Unification
### Current state
- `.form-group` exists in `global.css` and is widely used.
- `.form-field` exists with nearly identical rules and is used in newer components.
### Target state
- Keep **`.form-group`** as the canonical class.
- Deprecate `.form-field` by making it an alias:
```css
.form-field {
/* alias for backward compatibility */
composes: form-group; /* not supported in plain CSS; instead duplicate the rule or remove .form-field usage */
}
```
Because plain CSS does not support `composes`, do one of:
1. **Preferred**: keep both selectors in the same rule block:
```css
.form-group,
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
```
2. Then migrate callers from `.form-field` to `.form-group` in a follow-up cleanup task (not this pass).
### Add missing form utilities
```css
.form-section {
padding: var(--space-4);
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.form-section > h4:first-child,
.form-section > h3:first-child {
margin-top: 0;
}
.form-row {
display: flex;
align-items: flex-start;
gap: var(--space-3);
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.form-help {
font-size: var(--font-size-sm);
color: var(--muted);
line-height: var(--line-height-normal);
}
.text-error {
color: var(--danger);
font-size: var(--font-size-sm);
}
```
---
## 5. Status Badge Consolidation
### Current state
Status colors are duplicated across `.status-badge` (utilities.css), `.instance-chip` (global.css), and inline styles.
### Target state
Create a single `.status-badge` family in `global.css`:
```css
.status-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;
text-transform: capitalize;
line-height: var(--line-height-tight);
background: var(--bg);
color: var(--muted);
border: 1px solid var(--border);
}
.status-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-badge--running,
.status-badge--success {
background: var(--success-light);
color: var(--success);
border-color: color-mix(in srgb, var(--success) 30%, transparent);
}
.status-badge--error,
.status-badge--failed {
background: var(--danger-light);
color: var(--danger);
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
}
.status-badge--warning {
background: var(--warning-light);
color: var(--warning);
border-color: color-mix(in srgb, var(--warning) 30%, transparent);
}
.status-badge--info,
.status-badge--pending,
.status-badge--idle {
background: var(--info-light);
color: var(--info);
border-color: color-mix(in srgb, var(--info) 30%, transparent);
}
```
Then update callers to use `.status-badge` + `.status-badge--<variant>` instead of `.instance-chip` or inline status colors. Keep `.instance-chip` as a deprecated alias for one pass if it is widely referenced; do not delete it yet.
---
## 6. Required New Utility Classes
Add to `apps/web/src/styles/utilities.css` unless they fit better in `global.css`.
### 6.1 Layout utilities
```css
.row {
display: flex;
align-items: center;
gap: var(--space-3);
}
.row-sm { gap: var(--space-2); }
.row-md { gap: var(--space-3); }
.row-lg { gap: var(--space-4); }
.row.items-start { align-items: flex-start; }
.row.items-center { align-items: center; }
.row.items-end { align-items: flex-end; }
.stack {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.stack-sm { gap: var(--space-2); }
.stack-md { gap: var(--space-3); }
.stack-lg { gap: var(--space-4); }
.stack-xl { gap: var(--space-5); }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.flex-1 { flex: 1; }
.flex-none { flex: none; }
.w-full { width: 100%; }
.min-w-0 { min-width: 0; }
.max-w-prose { max-width: 65ch; }
.max-w-form { max-width: 800px; }
```
### 6.2 Spacing utilities
```css
.m-0 { margin: 0; }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-5 { margin-bottom: var(--space-5); }
.mb-6 { margin-bottom: var(--space-6); }
.ml-auto { margin-left: auto; }
.mr-auto { margin-right: auto; }
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
.p-5 { padding: var(--space-5); }
.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); }
.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); }
.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); }
.py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); }
.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); }
```
### 6.3 Typography utilities
```css
.text-xs { font-size: var(--font-size-xs); }
.text-sm { font-size: var(--font-size-sm); }
.text-base { font-size: var(--font-size-base); }
.text-lg { font-size: var(--font-size-lg); }
.text-xl { font-size: var(--font-size-xl); }
.font-mono { font-family: "IBM Plex Mono", monospace; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
```
### 6.4 Visual utilities
```css
.cursor-pointer { cursor: pointer; }
.overflow-auto { overflow: auto; }
.overflow-hidden { overflow: hidden; }
.rounded-sm { border-radius: var(--radius-sm); }
.rounded-md { border-radius: var(--radius-md); }
.rounded-lg { border-radius: var(--radius-lg); }
.rounded-xl { border-radius: var(--radius-xl); }
.rounded-full { border-radius: var(--radius-full); }
```
### 6.5 Card utilities
```css
.card-sm {
padding: var(--space-3);
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.card-md {
padding: var(--space-4);
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.card-lg {
padding: var(--space-5);
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
```
Ensure existing `.card` rules remain backward-compatible.
---
## 7. Button Class Alignment
The codebase uses many ad-hoc button class names: `.primary-button`, `.secondary-button`, `.ghost-button`, `.button-secondary`, `.delete-btn`.
For Pass 2, align the most common ones to the new `.btn` primitives where feasible:
- `.primary-button` → keep as alias to `.btn.btn-primary`
- `.secondary-button` / `.button-secondary` → keep as alias to `.btn.btn-secondary`
- `.ghost-button` → define explicitly or alias to `.btn.btn-secondary`
- `.btn-icon` → add a modifier `.btn.btn-icon` for icon-only buttons
Because renaming every caller is risky, the worker should:
1. Add the aliases in CSS so both old and new class names work.
2. Update only the refactored components to use the new `.btn*` classes.
3. Leave other callers untouched to keep the diff focused.
---
## 8. Acceptance Criteria
- [ ] All six target components have significantly fewer inline style blocks.
- [ ] No hardcoded `280px` sidebar widths remain; `var(--sidebar-width)` is used.
- [ ] New layout/spacing/typography/visual utilities from §6 are present.
- [ ] `.form-group` and `.form-field` share a single rule block.
- [ ] `.status-badge` family exists and is used by at least one caller.
- [ ] Button aliases (`.primary-button`, `.secondary-button`, `.ghost-button`) map to `.btn` primitives.
- [ ] `npm run typecheck` passes in `apps/web`.
- [ ] `npm run lint` passes in `apps/web`.
- [ ] No visual regressions in Tool Workshop and Config Profiles pages.
---
## 9. Verification Plan
1. Run `cd apps/web && npm run typecheck`.
2. Run `cd apps/web && npm run lint`.
3. Count inline `style={{` blocks in the six target components before/after; expect ≥50% reduction overall.
4. Search for `280px` in `apps/web/src`; expect zero matches (except in comments/docs).
5. Open Tool Workshop and Config Profiles in browser/dev server and verify layout.
6. Check that `.status-badge` renders correctly in at least one place.
---
## 10. Next Phase
After this spec is approved, create **tasks** for Pass 2 implementation, then delegate to `sdd-apply`.
@@ -1,653 +0,0 @@
# 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`.