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