Feedback & States
This category covers the reusable feedback and state components that surface transient
messages, empty collections, hover hints, and progressive-complexity nudges. They live
under $ui/ (and the sibling alerts folder) in
packages/praxrr-app/src/lib/client/ui/ and are wired to Svelte stores rather than
one-off local state.
Single toast/alert card rendered inside AlertContainer. It shows a type-colored icon
plus message, dismisses on click or Enter/Space, and animates in (fly from top) and out
(fade).
Import:
<script> import Alert from '$lib/client/alerts/Alert.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
id |
string |
— | Yes | Alert id used to remove it from alertStore on dismiss. |
type |
AlertType ('success' | 'error' | 'warning' | 'info') |
— | Yes | Controls icon (CheckCircle/XCircle/AlertTriangle/Info) and the color scheme (emerald/red/amber/sky). |
message |
string |
— | Yes | Text shown next to the icon. |
Usage:
<script> import Alert from '$lib/client/alerts/Alert.svelte';</script>
<Alert id="abc-123" type="success" message="Profile saved" />Variants and notes:
- Four color/icon variants driven by
type: success (emerald + CheckCircle), error (red- XCircle), warning (amber + AlertTriangle), info (sky + Info). Full-size padded card
(
px-4 py-3,rounded-xl,shadow-lg, 18px icon).
- XCircle), warning (amber + AlertTriangle), info (sky + Info). Full-size padded card
(
- No callback props. Internally binds
on:clickandon:keydown(Enter/Space) to callalertStore.remove(id), withrole="button"andtabindex=0. - Svelte 4 style (
export let,on:click). Not meant to be instantiated directly in feature code — alerts are created viaalertStore.add(type, message, duration?)and rendered byAlertContainer, which iterates$alertStore. AlertTypeis imported from./store. Transitions:in:fly {y:-12,duration:200},out:fade {duration:150}.
AlertContainer
Section titled “AlertContainer”Fixed-position overlay that subscribes to alertStore and renders one Alert per active
toast. It reads position from alertSettingsStore and adapts layout for auth pages versus
the main app shell.
Import:
<script> import AlertContainer from '$lib/client/alerts/AlertContainer.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
| — | — | — | — | No props — fully driven by stores. |
Usage:
<script> import AlertContainer from '$lib/client/alerts/AlertContainer.svelte';</script>
<AlertContainer />Variants and notes:
- Mount once in the root layout so every route shares one toast overlay.
- Six position placements resolved from
$alertSettingsStore.positionviaAlertPosition(top-left|top-center|top-right|bottom-left|bottom-center|bottom-right); it falls back totop-center. - On non-auth pages it is hidden on mobile and offsets for the sidebar (
md:pl-80); on auth pages it renders without those offsets. - Uses a
pointer-events-nonewrapper withpointer-events-autoper alert so clicks pass through empty areas. Keyedeachblock over$alertStorebyalert.id.AlertPositionis imported from./settings;$app/storespageis used to detect/auth/pathnames.
MobileNavAlert
Section titled “MobileNavAlert”Compact single-line alert variant sized for the mobile navigation bar. It has the same
dismiss behavior as Alert but is smaller (text-xs, truncated message, 14px icon).
Import:
<script> import MobileNavAlert from '$lib/client/alerts/MobileNavAlert.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
id |
string |
— | Yes | Alert id used to remove it from alertStore on dismiss. |
type |
AlertType ('success' | 'error' | 'warning' | 'info') |
— | Yes | Controls icon and color scheme (emerald/red/amber/sky). |
message |
string |
— | Yes | Text shown; truncated with min-w-0 truncate when long. |
Usage:
<script> import MobileNavAlert from '$lib/client/alerts/MobileNavAlert.svelte';</script>
<MobileNavAlert id="abc-123" type="info" message="Syncing…" />Variants and notes:
- Same four
typevariants asAlertbut with a lighter compact style (px-2.5 py-1.5,rounded-lg,text-xs font-semibold, no shadow). Fade in/out only (in:fade150ms,out:fade100ms). - No callback props. Internal
on:clickandon:keydown(Enter/Space) callalertStore.remove(id), withrole="button"andtabindex=0. - Svelte 4 style. Differs from
Alertonly in sizing/typography and transition; text color uses the-800/-200shades versusAlert’s-900/-100, and it has no hover state. Intended for the mobile top bar whereAlertContaineris hidden.
EmptyState
Section titled “EmptyState”Centered full-height empty-state placeholder with a circular icon, title, description, and a single call-to-action link button. Use it when a list or collection has no items yet.
Import:
<script> import EmptyState from '$ui/state/EmptyState.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
icon |
ComponentType (svelte) |
— | Yes | Icon component rendered large (h-12 w-12) inside the rounded neutral circle. Typically a lucide-svelte icon. |
title |
string |
— | Yes | Heading text (text-2xl font-bold). |
description |
string |
— | Yes | Supporting paragraph text under the title. |
buttonText |
string |
— | Yes | Label for the action link/button. |
buttonHref |
string |
— | Yes | href the action button navigates to (rendered as an <a>). |
buttonIcon |
ComponentType (svelte) |
Plus |
No | Icon shown at 18px inside the action button; defaults to lucide-svelte Plus. |
Usage:
<script> import EmptyState from '$ui/state/EmptyState.svelte'; import { Database, Plus } from 'lucide-svelte';</script>
<EmptyState icon={Database} title="No databases yet" description="Link a PCD repository to get started." buttonText="Add database" buttonHref="/databases/new" buttonIcon={Plus}/>Variants and notes:
- Single layout; the only visual variation is via the
icon/buttonIconprops. Accent-colored CTA button (bg-accent-600 hover:bg-accent-700). - Svelte 4 style (
export let). RendersiconandbuttonIconvia<svelte:component this={...}>. - The action is always an anchor (
href), not a click handler — there is noonclickprop.min-h-[calc(100vh-4rem)]accounts for the app top bar.
Tooltip
Section titled “Tooltip”Hover tooltip that wraps any trigger content (default slot) and renders a fixed-position,
viewport-clamped label on mouseenter. It positions above or below the trigger and flips
if it would overflow.
Import:
<script> import Tooltip from '$ui/tooltip/Tooltip.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
text |
string |
'' |
No | Tooltip label. When empty, no tooltip renders (show() early-returns and the {#if text && visible} guard is false). |
position |
'top' | 'bottom' |
'bottom' |
No | Preferred side of the trigger. Auto-flips to the opposite side if there isn’t enough room within an 8px viewport padding. |
Usage:
<script> import Tooltip from '$ui/tooltip/Tooltip.svelte';</script>
<Tooltip text="Delete profile" position="top"> <button aria-label="Delete">🗑</button></Tooltip>Variants and notes:
- The default slot is the trigger element(s) the tooltip is attached to (wrapped in an
inline-flexdiv). - Two placements via
position(top/bottom), each with automatic flip-on-overflow. Fixedz-50bubble,pointer-events-none,rounded-xl, light/dark styled. - No callback props. Internally handles
on:mouseenter(show) andon:mouseleave(hide) on the wrapper. - Svelte 4 style. Computes position from the wrapper’s
getBoundingClientRect, renders, then awaitstick()and clamps horizontally withinPADDING=8pxof the viewport, flipping vertically if the preferred side overflows. Uses inline style plus a hardcodedborder-radius:0.75rem !importantoverride. The bubble ispointer-events-noneso it never intercepts hover.
ComplexityProgressionHint
Section titled “ComplexityProgressionHint”Contextual inline nudge that suggests advancing the current area’s complexity tier (beginner → intermediate → advanced) after the user has opened advanced options enough times. It offers Switch / Not now actions.
Import:
<script> import ComplexityProgressionHint from '$ui/complexity/ComplexityProgressionHint.svelte';</script>Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
| — | — | — | — | No exposed props; behavior comes entirely from the complexity-tier context. |
Usage:
<script> import ComplexityProgressionHint from '$ui/complexity/ComplexityProgressionHint.svelte'; // Requires an ancestor to have called setComplexityTierContext(...)</script>
<ComplexityProgressionHint />Variants and notes:
- Single amber advisory banner (
aria-live="polite"). It renders nothing unless the complexity-tier context exists, a next tier is available,advancedToggleCount >= 5, and the suggestion has not already been shown/dismissed for that tier. - Internal buttons: Switch (
acceptSuggestion) setscontext.tierto the suggested tier and callscontext.dismissSuggestion(suggestedTier); Not now (dismissSuggestion) callscontext.dismissSuggestion(suggestedTier). - Svelte 5 handler style (
onclick). Consumes theComplexityTierContextviagetComplexityTierContext()from./complexityTierContext— it returnsundefinedif no provider, in which case the hint never shows. - Subscribes to context stores (
tier,advancedToggleCount,lastSuggestedTier,suggestionDismissedAt) and unsubscribesonDestroy. Threshold constantADVANCED_TOGGLES_BEFORE_SUGGEST = 5.suggestedTieris derived: beginner → intermediate, intermediate → advanced, advanced → null (no hint at the top tier).ComplexityTiertype comes from$shared/complexity/tiers.ts.