P3 Design & UX Principles
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/p3-design-principles/design-and-ux.md |
| Description | Not specified |
Source Content
P3 Design & UX Principles
Consolidated design and UX principles for the Platform Three dashboard.
Extracted from the ADRs in ~/Code/platformthree.io/p3-mvp/docs/adr/.
1. Visual Restraint
No Borders on Borders
The principle: Border restraint is an established P3 convention. Borders are meaningful; they denote a boundary, a separation, or a decision point. They are not neutral decoration.
The anti-pattern: Adding a neutral border to every wrapper creates recursive framing — a bordered page shell around a bordered filter panel around a bordered content card. The eye reads all those lines as equally important. Nothing stands out; everything just looks dense.
Rules:
- A wrapper that is already framed by background/elevation MUST NOT add a border. Pick one signal.
- Use
borderonly to separate peers at the same visual level (e.g., table rows, dividers between list items). - Prefer
shadow,bg-muted, or spacing over borders to create grouping or hierarchy. - Section headers inside a card do not need a bottom border if the typographic weight already creates the break.
- When reviewing a component, ask: “What is this border telling the user?” If the answer is “nothing — it just looks contained,” remove it.
2. State Discipline
Empty ≠ Loading ≠ Error — never conflate them
The rule: Render an empty state only after the query succeeds and returns zero items. A "No results found" message before the first fetch resolves is a false negative — the product is lying to the user.
| State | Correct treatment |
|---|---|
loading | Shape-matched skeleton (card shell for cards, table shell for tables) |
refreshing | Keep existing content visible; subtle chrome spinner |
stale | Stale-data badge with age; content remains visible |
partial | Alert noting which sources failed; show what succeeded |
empty | Confirmed empty state — only after a successful query returns zero items |
error | Explicit error treatment with a retry action |
forbidden | Distinct forbidden state — never render as empty |
Practical consequence: Never use !data or data?.length === 0 as the loading guard for empty-state copy. Gate empty state on isSuccess && data.length === 0.
Page chrome stays stable while content loads
Titles, tabs, breadcrumbs, and sticky scope controls must render immediately. Only the content region beneath them skeletons. Do not full-page-spinner a surface that has already rendered its chrome.
Background refresh preserves the last good state
Once real data has rendered, a refetch must keep existing content visible. Do not replace a populated page with an empty skeleton on revalidation.
3. Card Grid Layout
→ ADR-011
Cards summarize; they are not navigation tiles
Aggregate cards must expose rollup information about their scope. A card that only has a title and a “View →” link is a placeholder, not a card.
Rules:
- Aggregate cards MUST surface at least a small set of rollup metrics so users can decide whether to drill in.
- Card grids MUST use a wrapping responsive grid, never a horizontal scroll rail.
- All cards in a row MUST stretch to equal height (flex-col + h-full on the card root).
- Primary actions SHOULD be pinned to the lower section to keep scan order consistent.
- Long text inside cards SHOULD be clamped so a verbose card cannot destabilize row height.
4. Slide-out Panels
Slideouts are contextual, not universal
Use SlideOutPanel when the user should remain anchored to their current page context. Do not use a slideout as a default container for everything.
Use a slideout when the task is:
- Contextual to a specific resource, namespace, or cluster.
- Short enough to complete within a narrow side panel.
- Better served by preserving the background view.
Use a dedicated route instead when the task is:
- A primary product destination.
- Multi-column, canvas-based, or comparison-heavy.
- Too dense for a standard panel width.
Inside slideouts:
- Tables MUST use
mobileCardLayout="stack"— never a side-by-side grid layout. - Headers MUST clearly identify the resource or task in focus.
- Tab state MUST be URL-backed.
- Kubernetes resources MUST include a Manifest tab.
- Footer actions SHOULD be used for stable primary actions (Save, Apply, Continue, Back).
5. Wizard Flows
→ ADR-004
One step = one decision
Wizards reduce cognitive load by gating choices. They are not an excuse to split a simple form across five screens.
When to use a wizard:
- The workflow has ordered steps with dependencies between them.
- Showing all controls at once would materially increase cognitive load.
- The user benefits from progress visibility.
When NOT to use a wizard:
- The form is short and independent.
- Users need frequent power-user editing across many fields.
Implementation rules:
- Steps MUST be defined as metadata, not scattered boolean branches.
- Step copy MUST describe the current decision, not the entire workflow.
- Validation MUST gate forward progress only for the current step’s required inputs — never future steps.
- Advanced options SHOULD appear in later steps, after core decisions are made.
- Footer actions (Back, Continue, Save, Cancel) MUST remain stable across all steps.
6. Toast Notifications
→ ADR-011
Toasts are for operations, not field validation
Use a toast when:
- A mutation succeeds and the user needs lightweight confirmation.
- A mutation fails and the failure is not tied to a specific input field.
- A background fetch fails silently.
- A follow-up action (Retry, Open resource, View details) shortens recovery time.
Never use a toast when:
- The error belongs to a specific field in the current form — use inline validation.
- The message must stay visible until the user resolves it — use a persistent banner.
Rules:
- Every toast MUST have a close button.
- Use semantic variants for meaning: success, warning, destructive, default — never pick a color for decoration.
- Auto-dismiss: success ~4s, warning ~6s, error defaults to manual dismissal.
- At most one primary action per toast. Labels must be explicit: “Retry”, “Open resource”, “View details”.
- Auto-dismiss for actionable toasts only when the action remains available elsewhere.
7. Navigation & URL State
The URL is the source of truth for permalinkable UI state
Every piece of visible state that should survive a page refresh, support deep linking, or participate in browser back/forward must live in the URL.
Use canonical hooks — never hand-roll:
| State shape | Hook |
|---|---|
| Plain string | useUrlString |
| Boolean flag | useUrlBoolean |
| Multi-select set | useUrlSet |
| Sort field + direction | useUrlSort |
| Constrained enum or tab | useUrlEnum |
Never hand-roll split(",") / join(","), custom enum validation, or paired sort params. Invalid values must normalize to valid defaults — no leaked bogus URL state.
Breadcrumb dropdowns must be searchable
Every breadcrumb dropdown (cluster, vCluster, application) must include a search input. It must auto-focus on open and reset on close. A “No matches” message must appear on zero results.
8. Creation Flows
→ ADR-003
Lead with intent, not infrastructure taxonomy
The first step of a creation flow must ask what the user wants to accomplish, not what Kubernetes resource type to create. Kubernetes-specific types, manifests, and advanced options belong in review or advanced sections — not step one.
Rules:
- New creation workflows are Storybook-first: build the Storybook story before wiring the production flow.
- Storybook must render the same component production uses — no parallel implementations.
- Separate purpose selection, scope/details, values/configuration, and review into distinct steps when that reduces cognitive load.
- Sensitive values in review steps should be summarized, not re-exposed.
9. Practical Review Checklist
Before shipping a UI change, confirm:
- No new border wraps a wrapper that is already visually contained by elevation or background color.
- Empty state is gated on
isSuccess && data.length === 0, not on!dataordata?.length === 0. - Loading state uses a shape-matched skeleton, not a spinner in place of a card or table.
- Error state is visually distinct from empty state.
- Page chrome (title, tabs, breadcrumbs) renders immediately and does not re-skeleton on background refresh.
- Toasts use semantic variants and are not used for field-level validation errors.
- Any multi-step form with dependent choices uses
PanelWizard. - All permalink-worthy UI state (filters, tabs, sort, panel open/closed) is URL-backed via
useUrlStatehelpers. - Aggregate cards surface rollup metrics, not just a title and link.
- Tables inside slideouts use
mobileCardLayout="stack". - Creation flows lead with user intent and defer Kubernetes specifics to later steps.