Skip to content

Visual foundations

Visual language for Portal UI. Token values are defined only in src/ui-kit/styles/globals.css. This page defines intent, usage, and dimension adaptation. Stack: Tailwind CSS v4 + shadcn/ui (@freeanima/ui-kit). DaisyUI is not used.

Each section uses the dimension adaptation template.

Agent bans (raw CSS vars, DaisyUI tokens) → .agent/rules/frontend-ui.md.

Intent: Semantic surfaces and text, not ad-hoc hex. Dark mode via root .dark. Accent variants on dark via data-color-theme (neutral default, ocean / forest / sunset / violet) from local preference (portal-sdk/color-theme).

Usage: Tailwind semantic classes (bg-background, text-muted-foreground, bg-primary, border-border, …). Prefer @apply with those utilities over raw var(--background) outside globals.css.

LensAdaptation
InvariantSemantic token names; light/dark pairs; destructive for irreversible risk
LayoutN/A (same palette); chrome may use sidebar-* tokens
InteractionHover/active tints (hover:bg-muted, bg-primary/20 selection) only when pointer can hover; touch must not rely on hover-only color cues
ShellAccent theme is local client preference, not Shell kind; do not ship a separate palette per shell
ForbiddenDaisyUI data-theme / --color-base-*; choosing colors from getShellKind()

Pending alignment: Feature-local one-off colors that bypass semantic tokens.

Intent: Clear hierarchy — page/panel title, body, secondary, meta/label.

Usage (current practice): Panel titles often text-lg font-semibold or text-sm font-semibold; body text-sm; secondary/meta text-xs text-muted-foreground; truncate long titles with truncate.

LensAdaptation
InvariantHierarchy roles (title / body / secondary / meta)
LayoutCompact may hide or shorten secondary lines; do not invent a second type scale per breakpoint
InteractionEnlarge hit targets with spacing/min-height, not by breaking the type scale
ShellN/A
ForbiddenDifferent font stacks per shell

Intent: Consistent padding and control heights; touch-friendly targets without sparse pointer UIs.

Usage: Control heights h-8 / h-9 / h-10; list rows often min-h-11 + px-1 py-1; panel headers p-3.

LensAdaptation
InvariantSpacing scale via Tailwind; shared row/control heights where possible
LayoutCompact chrome must clear bottom nav (--app-bottom-nav-h); denser chrome, not denser unsafe hits
Interactiontouch: minimum ~44px hit target (min-h-11); pointer: may use denser icon buttons if hover/menu still reachable
ShellSafe-area insets via shell/host (shared-safe-area.css); not a second density system
ForbiddengetShellKind() to pick padding

Intent: Soft but consistent corners from --radius (and sm/md/lg/xl derivatives in @theme).

Usage: Controls/menus often rounded-md / rounded-sm; rows/Dialogs rounded-lg; pills/switches rounded-full; compact sheets may use larger top radius (rounded-t-2xl).

LensAdaptation
InvariantRadius tokens; map control vs surface roles
LayoutSheet/drawer presentation may use larger top radius; Dialog stays rounded-lg
InteractionN/A
ShellN/A
ForbiddenOne-off rounded-[13px] sprawl (pending alignment where present)

Intent: Separate regions with border-border; prefer list separators or muted backgrounds over card-in-card stacks.

LensAdaptation
Invariantborder-border / border-input for controls
LayoutExpanded multi-column uses column borders; compact drawers use edge borders + shadow
InteractionN/A
ShellN/A
ForbiddenDecorative multi-border chrome without information structure

Intent: Elevation signals overlay/stacking, not decoration.

Usage: Controls shadow-xs; overlays/drawers shadow-lg sparingly.

LensAdaptation
InvariantPrefer semantic elevation steps; avoid stacked glow
LayoutDrawer/Sheet/Dialog use elevation; list rows usually flat
InteractionN/A
ShellN/A
ForbiddenGlow-as-brand; multi-layer shadows for idle chrome

Intent: Lucide icons; default size aligned with controls (size-4 common).

LensAdaptation
InvariantIcon set and default size
LayoutN/A
InteractionEnlarge button hit box, not the global icon size, for touch
ShellN/A

Intent: Transitions for open/close and state change (Radix/shadcn animate). Motion clarifies hierarchy; it is not ornament.

LensAdaptation
InvariantPrefer existing animate utilities
LayoutSheet slide vs Dialog zoom follows presentation
InteractionDo not depend on hover transitions for essential feedback on touch
ShellN/A
ForbiddenMeaningless glow pulses

Intent: Visible focus-visible rings (ring / ring-ring). Dialogs/Sheets default to blocking auto-focus on open; confirm actions must not receive default focus.

LensAdaptation
InvariantFocus rings; no stealth focus traps on destructive confirms
LayoutN/A
InteractionKeyboard users need visible focus; Enter-to-send is interaction capability, not layout
ShellExternal keyboard does not flip touch→pointer strategy
ForbiddenautoFocus on destructive confirm buttons

Intent: disabled:opacity-50 + pointer-events-none on controls; completed/softened content may use opacity-60.

LensAdaptation
InvariantDisabled affordance pattern
Layout / Interaction / ShellN/A

Intent: Global thin scrollbar styles (ui-kit scrollbar CSS). Safe-area CSS variables for notched devices; layout chrome clears bottom nav height.

LensAdaptation
InvariantShared scrollbar look
LayoutCompact fixed layers respect --app-bottom-nav-h
InteractionPull-to-refresh ignores starts near left edge (drawer conflict) — see patterns / page refresh
ShellSafe-area and keyboard inset host differences
ForbiddenTheme colors inside safe-area-only CSS files

Intent: Stable stacking for menus, sheets, toasts. Prefer shared conventions over magic numbers.

LensAdaptation
InvariantOverlay above chrome; do not cover bottom nav by raising z-index
LayoutSheet/Dialog/Drawer stacking
InteractionContext menu / ActionSheet above content
ShellN/A
ForbiddenAd-hoc z-[9999] (pending alignment where present, e.g. some pickers)