|
| 1 | +# Story 4.3 Supplement: Docker Tabs UI Contract |
| 2 | + |
| 3 | +**Epic**: Epic 4 - Docker Operations Layer |
| 4 | +**Status**: Implemented |
| 5 | +**Parent**: Story 4.3 Docker Workspace Replan |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Purpose |
| 10 | + |
| 11 | +This document records finalized UI conventions for all Docker sub-tabs inside the Docker workspace. It is the implementation reference for building and maintaining any Docker tab — existing or new. |
| 12 | + |
| 13 | +Containers-specific decisions are called out explicitly. Everything else applies to all tabs. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Scope |
| 18 | + |
| 19 | +Covers: |
| 20 | +- panel shell layout and outer chrome (`DockerPanel`) |
| 21 | +- shared section header conventions |
| 22 | +- Containers tab: table structure, columns, interaction patterns |
| 23 | +- React patterns that apply across all tabs |
| 24 | + |
| 25 | +Does not cover: |
| 26 | +- Docker backend routes or API contracts (Epic 4 domain stories) |
| 27 | +- monitor telemetry contracts (Story 28.6, 28.7) |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +This document records the finalized UI conventions for the `Containers` tab inside the Docker workspace. It serves as the implementation reference so the same conventions can be applied consistently when extending other Docker sub-tabs. |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## Panel Shell (`DockerPanel`) |
| 36 | + |
| 37 | +`DockerPanel` owns all outer chrome. Individual tab components are controlled — they render content only, no outer borders, headers, or toolbars of their own. |
| 38 | + |
| 39 | +``` |
| 40 | +DockerPanel |
| 41 | + ├── Left nav (tab switching, collapsible) |
| 42 | + └── Content panel (rounded-xl border overflow-hidden) |
| 43 | + ├── Section header (title + per-tab toolbar) |
| 44 | + └── <Tab component> (controlled, no chrome) |
| 45 | +``` |
| 46 | + |
| 47 | +**Rule: `overflow-hidden` on the content panel.** The content panel uses `rounded-xl border`. It must also carry `overflow-hidden` so that child backgrounds do not paint over the rounded corners, which would make the bottom corners appear flat/cut off. |
| 48 | + |
| 49 | +**Rule: controlled tab components.** Any tab component that has toolbar state (filters, pagination, column visibility) must receive that state as props from `DockerPanel`. The tab itself does not own or render the toolbar. |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Section Header (all tabs) |
| 54 | + |
| 55 | +Lives in `DockerPanel`, not in individual tab components. |
| 56 | + |
| 57 | +Layout: `flex flex-col gap-2` with inner row `flex-wrap items-center justify-between`. Title left, toolbar right. Wraps naturally in narrow panels — no breakpoint-driven `flex-row` switch. |
| 58 | + |
| 59 | +**Rule**: Do not use `xl:flex-row` or similar breakpoints to switch between stacked and side-by-side. `flex-wrap justify-between` is sufficient. |
| 60 | + |
| 61 | +Tab description text is **not shown** in the section header. The tab name is self-explanatory. |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Left Nav (collapsible) |
| 66 | + |
| 67 | +The collapse/expand toggle button uses `right-0` when collapsed and `right-3` when expanded. |
| 68 | + |
| 69 | +**Rule**: At collapsed width (`w-14` = 56px), placing the toggle at `right-3` (12px from edge) causes it to overlap the centered tab icons. Use `right-0` when collapsed. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Containers Table |
| 74 | + |
| 75 | +### Column Order |
| 76 | + |
| 77 | +| # | Column | Always visible | |
| 78 | +|---|--------|----------------| |
| 79 | +| 1 | Name | ✓ | |
| 80 | +| 2 | Runtime | ✓ | |
| 81 | +| 3 | Quick | ✓ | |
| 82 | +| 4 | Lifecycle | optional | |
| 83 | +| 5 | Ports | optional | |
| 84 | +| 6+ | CPU / Mem / Net / Compose | optional | |
| 85 | +| last | Actions | ✓ | |
| 86 | + |
| 87 | +**Rule**: Quick column is fixed at position 3, immediately after Runtime. It must always be visible without horizontal scrolling. |
| 88 | + |
| 89 | +### Name Column |
| 90 | + |
| 91 | +- Content: container name (bold, `text-sm`) above image tag (`font-mono text-[11px] text-muted-foreground`) |
| 92 | +- Left edge aligns with the section header title (`pl-4`, 16px from panel edge) |
| 93 | +- Clicking the name row opens the inline detail expansion |
| 94 | + |
| 95 | +**Rule**: Name column header and cell content share the same left offset (`pl-4`). Do not use negative margin tricks on the sort button to compensate for component padding — use a native `<button>` element with `px-0` instead. |
| 96 | + |
| 97 | +### Runtime Column |
| 98 | + |
| 99 | +Shows state badge + telemetry freshness badge together. |
| 100 | + |
| 101 | +- Running: `emerald` badge |
| 102 | +- Exited: muted badge |
| 103 | +- Paused: `amber` badge |
| 104 | +- No telemetry / Stale: ghost dashed badge alongside the state badge |
| 105 | + |
| 106 | +### Quick Column (`w-[112px]`) |
| 107 | + |
| 108 | +Three icon-only buttons: Logs (`FileText`), Monitor (`Activity`), Exec (`TerminalSquare`). |
| 109 | + |
| 110 | +- Exec button is disabled when container state is not `running` |
| 111 | +- All buttons use `onClick` with `event.stopPropagation()` + `event.preventDefault()` |
| 112 | + |
| 113 | +### Actions Column (`w-[52px]`) |
| 114 | + |
| 115 | +`DropdownMenu` trigger with `MoreVertical` icon. |
| 116 | + |
| 117 | +**Critical rule**: All `DropdownMenuItem` handlers must use `onSelect` (not `onClick`) with `window.setTimeout(() => handler(), 0)` to defer state changes until after Radix menu cleanup. Using `onClick` on menu items causes React error #185 (maximum update depth exceeded) because synchronous state changes during menu close animation create a render loop. |
| 118 | + |
| 119 | +```tsx |
| 120 | +<DropdownMenuItem |
| 121 | + onSelect={event => { |
| 122 | + event.stopPropagation() |
| 123 | + window.setTimeout(() => setStatsContainer(c), 0) |
| 124 | + }} |
| 125 | +> |
| 126 | +``` |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## Derived Data — useMemo Rule |
| 131 | + |
| 132 | +Filter chains (`filtered`, `stateFiltered`, `nameFiltered`) must be wrapped in `useMemo`. Plain `.filter()` calls in the render body produce new array references on every render, which causes downstream `useMemo` and `useEffect` hooks to run on every render, eventually triggering React error #185 via the `onSummaryChange` callback. |
| 133 | + |
| 134 | +```tsx |
| 135 | +const filtered = useMemo( |
| 136 | + () => containers.filter(c => c.Names?.toLowerCase().includes(query)), |
| 137 | + [containers, query] |
| 138 | +) |
| 139 | +``` |
| 140 | + |
| 141 | +--- |
| 142 | + |
| 143 | +## Sort Button |
| 144 | + |
| 145 | +The `SortHead` component must use a native `<button>` element, not the shadcn `Button` component. The shadcn `size="sm"` prop injects `has-[>svg]:px-2.5` which overrides `px-0` when an SVG child is present, shifting the header text to the right and breaking alignment with cell content. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## Row Styling |
| 150 | + |
| 151 | +- Running rows: subtle `bg-emerald-500/[0.015]` tint |
| 152 | +- Expanded row: `bg-muted/35` |
| 153 | +- Hover: `hover:bg-muted/30` |
| 154 | +- State dot / inline badge in the Name cell: **not used** (state is shown in the Runtime column only) |
0 commit comments