Skip to content

P3 Design & UX Principles

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/p3-design-principles/design-and-ux.md
DescriptionNot 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 border only 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

ADR-018 · ADR-031

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.

StateCorrect treatment
loadingShape-matched skeleton (card shell for cards, table shell for tables)
refreshingKeep existing content visible; subtle chrome spinner
staleStale-data badge with age; content remains visible
partialAlert noting which sources failed; show what succeeded
emptyConfirmed empty state — only after a successful query returns zero items
errorExplicit error treatment with a retry action
forbiddenDistinct 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

ADR-005 · ADR-008

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

ADR-015 · ADR-003 · ADR-004

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 shapeHook
Plain stringuseUrlString
Boolean flaguseUrlBoolean
Multi-select setuseUrlSet
Sort field + directionuseUrlSort
Constrained enum or tabuseUrlEnum

Never hand-roll split(",") / join(","), custom enum validation, or paired sort params. Invalid values must normalize to valid defaults — no leaked bogus URL state.

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 !data or data?.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 useUrlState helpers.
  • 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.