Skip to content
Pathly · Living style guide

Design system

Every specimen on this page renders the real component from src/components with its import path. Switch the theme to review both — dark is primary, light is complete.

Theme
Written spec: docs/02-design-system.md · Page contract: web/CONVENTIONS.mdEmail templates
01

Foundations

Design tokens live in src/app/globals.css as CSS variables and are exposed to Tailwind v4 via @theme inline. Never hard-code a colour, shadow or font in a component.

Colour

Every colour is a CSS variable mapped into Tailwind via @theme inline, so utilities and alpha modifiers (bg-accent/10) work in both themes. Dark is the primary theme; light is complete. Never use raw hex in components. Accent text on surfaces is always text-accent-text. Status colours always ship with an icon and a label.

src/app/globals.css

Theme tokens

Left swatch = light, right swatch = dark. Contrast is measured against surface.

Surfaces

  • backgroundvar(--background)
    bg-backgroundL #f7f8fa · D #0b0d10
    App canvas behind everything
  • surfacevar(--surface)
    bg-surfaceL #ffffff · D #111418
    Cards, popovers, dialogs, sidebar
  • surface-2var(--surface-2)
    bg-surface-2L #f2f4f7 · D #171b21
    Insets, hover rows, selected nav, secondary buttons
  • surface-3var(--surface-3)
    bg-surface-3L #eaedf1 · D #1e232b
    Pressed states, progress tracks, count pills

Borders

  • bordervar(--border)
    border-borderL #e5e7eb · D #252a32
    1px hairlines, dividers, card outlines (default for *)
  • border-strongvar(--border-strong)
    border-border-strongL #d3d7de · D #323946
    Hover borders, checkbox/radio outlines, dashed nodes

Text

  • fgvar(--fg)
    text-fgL #111318 · D #f5f7fa
    Primary text, titles, valuesL18.6AAAD17.2AAA
  • fg-2var(--fg-2)
    text-fg-2L #6b7280 · D #8b95a5
    Secondary text, descriptions, inactive navL4.8AAD6.1AA
  • fg-3var(--fg-3)
    text-fg-3L #6f7785 · D #78818f
    Muted meta, placeholders, overlines, icons (AA)L4.5AAD4.7AA

Brand

  • accentvar(--accent)
    bg-accent · bg-accent/10 · border-accent/30L #7c5cff · D #7c5cff
    Primary actions, progress, current node, focus ring
  • accent-hovervar(--accent-hover)
    hover:bg-accent-hoverL #6b4bf0 · D #8e72ff
    Primary button hover
  • accent-fgvar(--accent-fg)
    text-accent-fgL #ffffff · D #ffffff
    Text/icons on solid accent
  • accent-solidvar(--accent-solid)
    bg-accent-solid · hover:bg-accent-solid-hoverL #7453fa · D #7453fa
    Filled surfaces with white text: primary buttons, solid badges (AA)
  • accent-textvar(--accent-text)
    text-accent-textL #5f3df0 · D #a591ff
    Accent text & icons on surfaces (never text-accent)L6.2AAD7.1AAA

Status

  • successvar(--success)
    text-success · bg-success/10L #1a7f37 · D #3fb950
    Completed, approved, positive deltasL5.1AAD7.3AAA
  • warningvar(--warning)
    text-warning · bg-warning/10L #955800 · D #e0a33a
    Due soon, needs changes, streak, rescheduledL5.7AAD8.3AAA
  • dangervar(--danger)
    text-danger · bg-danger/10L #cf222e · D #f85149
    Errors, overdue, cancelled, live indicatorL5.4AAD5.5AA
  • infovar(--info)
    text-info · bg-info/10L #0969da · D #4c9aff
    In progress, informational, streak freezeL5.2AAD6.5AA
  • perfectvar(--perfect)
    text-perfect · bg-perfect/10L #8f6300 · D #f2c94c
    Perfect score, achievements, rank examsL5.3AAD11.6AAA
  • success-solidvar(--success-solid)
    bg-success-solidL #1a7f37 · D #238636
    Filled success buttons with white text (AA)
  • danger-solidvar(--danger-solid)
    bg-danger-solidL #cf222e · D #da3633
    Filled danger buttons with white text (AA)

Status tints

Tinted fills for badges, callouts and icon tiles: bg /10, border /25 (perfect /30).

Accentaccent/10
Successsuccess/10
Warningwarning/10
Dangerdanger/10
Infoinfo/10
Perfectperfect/10

Side by side

The same composition rendered in both themes (tokens re-scoped inline).

Light

Weekly goal+50 XP

420 / 500 XP · 2 days left

12

Muted meta text (fg-3)

Dark

Weekly goal+50 XP

420 / 500 XP · 2 days left

12

Muted meta text (fg-3)

Typography

Geist Sans for UI, Geist Mono for code and ids. Base is 14/20 with cv11 and ss01 features. Weights: 400 body, 500 labels & nav, 600 titles — never bold (700) in UI. Add tabular to every number that sits in a column or counts.

Scale

As used in the product. Prefer the component (PageHeader, SectionHeader, CardTitle) over raw classes.

  • Display28/36 → 34/40 · 600Countdown, celebration numbers
    02 : 14 : 09text-[28px] leading-9 font-semibold tracking-tight tabular sm:text-[34px] sm:leading-10
  • Page title22/28 → 24/32 · 600 · −0.02emPageHeader <h1>
    Programming Architecturetext-[22px] leading-7 font-semibold tracking-[-0.02em] sm:text-2xl sm:leading-8
  • Hero title20/28 → 22/28 · 600NextLessonCard, Continue learning
    Classes & Objectstext-xl leading-7 font-semibold tracking-tight sm:text-[22px]
  • Stat value24/28 · 600 · tabularStatCard value
    11,140tabular text-2xl leading-7 font-semibold tracking-tight
  • Dialog title16/24 · 600DialogTitle, SheetTitle
    Reschedule lessontext-base font-semibold tracking-tight
  • Section title15/20 · 600SectionHeader <h2>
    Upcoming lessonstext-[15px] font-semibold tracking-tight
  • Card title14/20 · 600CardTitle
    Rank progresstext-sm font-semibold tracking-tight
  • Body14/20 · 400Default body (set on <body>)
    Implement Idle, Patrol and Chase states.text-sm
  • Body small13/20 · 400–500Lists, table cells, nav items, descriptions
    Chapter 4 · Lesson 18 of 42text-[13px]
  • Meta12/16 · 400–500Timestamps, hints, field errors
    Updated 2h agotext-xs text-fg-2
  • Overline11/16 · 500 · uppercaseGroup labels, menu labels
    Learningtext-[11px] font-medium tracking-wide text-fg-3 uppercase
  • Micro10/14 · 500Chart axes, countdown units
    hours · min · sectext-[10px] font-medium tracking-wider text-fg-3 uppercase
  • Lesson prose16/28 · 400.prose-lesson reading surfaces
    A class is a blueprint for objects.prose-lesson
  • Code13/22 · Geist MonoCodeBlock, inline code, ids
    GetComponent<Rigidbody>()font-mono text-[13px] leading-[22px]

Families

Aa
Geist Sansfont-sans · var(--font-geist-sans)
Regular 400Medium 500Semibold 600
{ }
Geist Monofont-mono · var(--font-geist-mono)

Numbers

Use .tabular so digits align and counters don't jitter.

1,111 XP8,888 XP00:11:09
proportional
1,111 XP8,888 XP00:11:09
.tabular

Spacing

Tailwind's 4px scale. Stick to the steps below; odd values (gap-[7px]) are a smell.

Scale

  • 0.52pxIcon nudges, chart gaps
  • 14pxBadge gap, tight stacks
  • 1.56pxLabel → control, meta stacks
  • 28pxButton icon gap, inline groups
  • 2.510pxNav item padding, list icon gap
  • 312pxRow gap in lists, card header gap
  • 416pxGrid gap (gap-4), mobile page padding
  • 520pxCard padding (px-5), hero gaps
  • 624pxSection spacing (mt-6), sm page padding
  • 832pxLarge section spacing (mt-8), lg page padding
  • 1040pxDocs section rhythm
  • 1456pxEmpty state vertical padding

Rhythm rules

  • Card paddingpx-5 (20px) · header pt-4 pb-3 · hero p-5 sm:p-6
  • Grid gapgap-4 (16px) between cards and columns
  • Section spacingmt-4 between dashboard rows · mt-6 / mt-8 between sections
  • Page paddingpx-4 → sm:px-6 → lg:px-8 · top pt-5 → sm:pt-7
  • Page bottompb-24 on mobile (clears 64px bottom nav) → lg:pb-12
  • Stackslabel → control gap-1.5 · list rows py-3 · meta stacks gap-0.5

Radii

Radius grows with the size of the surface: controls 8px, cards 12px, dialogs 16px.

4pxKbd, tiny chipsrounded
6pxBadges, menu items, icon buttons (xs)rounded-md
8pxButtons, inputs, selects, icon tiles, nav itemsrounded-lg
12pxCards, popovers, menus, code blocksrounded-xl
16pxDialogs, bottom sheetsrounded-2xl
fullAvatars, pills, progress, switchesrounded-full

Borders

1px hairlines carry most of the structure — especially in dark mode where shadows are off. `*` defaults to border-border.

border-border · resting
hover:border-border-strong
border-dashed · available / cancelled
border-accent/50 + shadow-glow · emphasis
border-danger/25 bg-danger/5 · alert
Focused
focus-visible: ring-2 ring-accent/60 offset-2

Shadows

Three elevations only. Dark theme: cards are flat (border only), popovers get a deep shadow, glow is reserved for the one thing that needs attention now.

shadow-card

Resting cards. Dark theme relies on borders only.

shadow-card
L 0 1px 2px rgb(16 24 40 / 0.04)D none

shadow-pop

Popovers, menus, dialogs, sheets, toasts, tooltips

shadow-pop
L 0 12px 32px -8px rgb(16 24 40 / 0.16), 0 2px 6px rgb(16 24 40 / 0.06)D 0 16px 40px -8px rgb(0 0 0 / 0.6), 0 0 0 1px rgb(255 255 255 / 0.02)

shadow-glow

Current focus only: live / starting-soon lesson, current roadmap node

shadow-glow
L 0 0 0 4px rgb(124 92 255 / 0.14)D 0 0 0 4px rgb(124 92 255 / 0.18), 0 0 24px -4px rgb(124 92 255 / 0.45)

Motion

Fast and quiet: 150–250ms for UI, one expo-out curve cubic-bezier(0.16, 1, 0.3, 1) for things that enter. Only celebrations use longer motion. prefers-reduced-motion collapses every animation to 0.01ms globally.

src/app/globals.css

Durations & utilities

PreviewNameUtilityDurationUsage
transition (hover)transition-colors duration-150150msHover / focus colour changes on every control
fade-inanimate-fade-in160msOverlays, tooltips
scale-inanimate-scale-in160msPopovers, menus, dialogs
slide-in-rightanimate-slide-in-right240msRight sheet (create / edit)
slide-in-leftanimate-slide-in-left240msMobile navigation drawer
slide-upanimate-slide-up240msBottom sheet
riseanimate-rise600msCelebration screens, staggered hero content
progresstransition-[width] duration-700 ease-out700msProgress / ProgressRing fill
shimmer.skeleton / animate-shimmer1.6s loopSkeleton loading
pulse-softanimate-pulse-soft2s loopWaiting / syncing indicators

Iconography

lucide-react v1, stroke 2, inheriting currentColor. Components size icons for you ([&_svg]:size-4) — pass the icon as a child. Completed checks use stroke 2.5–3. lucide v1 has no brand marks: use GithubIcon from @/components/pathly/icons; Google Meet is represented by Video.

lucide-react

Common icons

BookOpen
Calendar
Video
Bell
Search
Plus
Check
X
Lock
Flame
Sparkles
Settings
Copy
Trash2
MoreHorizontal
ChevronDown
ArrowRight
Play
GithubIcon

Sizes

Badges12px
xs buttons, meta rows14px
Default: buttons, nav, menus16px
Icon buttons, bottom nav18px
Empty states, large tiles20px

Rules

  • Icons in text use text-fg-2 / text-fg-3; the label carries meaning.
  • Icon-only buttons need aria-label and a Tooltip.
  • Status = colour + icon + label, never colour alone.
  • No filled/duotone icon sets, no emoji.

Layout

Fixed 232px sidebar and sticky 60px header on desktop; below 1024px the sidebar becomes a left drawer and a 64px bottom nav appears. Every page renders inside <Page>.

import { Page, PageHeader, SectionHeader } from "@/components/pathly/layout"

Desktop frame · ≥ 1024px

Sidebar232px
Header 60px · sticky
Content · max 1320px · px 16/24/32

Mobile frame · < 1024px

Header · menu → drawer
pb-24 clears nav

Bottom nav 64px · 5 items · safe-area padded

Breakpoints

NameMin widthWhat changes
base0Single column, bottom nav, full-width sheets. Design at 375px.
sm640pxTwo-column forms, inline actions, page padding 24px
md768pxSearch field in header, 2-column card grids
lg1024pxSidebar appears (232px), bottom nav hides, 12-column dashboards
xl1280pxWider dashboard splits, 3-up grids
2xl1536pxContent stays capped at 1320px

Page widths

  • width="default"Dashboards, lists, calendarsmax-w-[1320px]
  • width="narrow"Details, settings, formsmax-w-[960px]
  • width="prose"Lessons, articlesmax-w-[760px]
  • width="full"Builders, full-bleed canvases

Layers

  • z-20Sticky app header
  • z-30Sidebar, mobile bottom nav
  • z-40Docs header
  • z-50Overlays, dialogs, sheets, popovers, menus
  • z-[60]Tooltips

Dashboard grid recipe

12 columns from lg. Pair spans that add up to 12; collapse to one column below md.

lg:col-span-7lg:col-span-5col-span-53col-span-4

Grid gap is always gap-4 (16px). Rows are separated with mt-4. Wrap the whole row in a card list below md.

02

Components

shadcn-style wrappers around Radix primitives (radix-ui package), cmdk and sonner. Import from src/components/ui; extend with className (merged via tailwind-merge) rather than forking.

Button

8 variants × 4 text sizes + 3 icon sizes. Heights: xs 28 · sm 32 · md 36 (default) · lg 44. Use asChild to render a Link. loading disables the button, shows a spinner and sets aria-busy.

import { Button, buttonVariants } from "@/components/ui/button"

Variants × sizes

Variantxssmmdlg
primary
secondary
outline
ghost
danger
danger-ghost
success
link

States

Hover and focus are simulated with static classes; real states apply on interaction.

VariantDefaultHoverFocus-visibleDisabledLoading
primary
secondary
outline
ghost
danger
danger-ghost
success

Icon buttons

Always aria-label + Tooltip. icon 36 · icon-sm 32 · icon-xs 28.

icon
icon-sm
icon-xs

Composition

leading icon
trailing icon
DashboardasChild + Link
w-full (mobile CTAs)

Input & Textarea

36px high, 8px radius, accent border + 3px ring on focus. Set aria-invalid for the error style and pass the message to Field. Use leading / trailing slots for icons, Kbd hints or a reveal button.

import { Input, Textarea } from "@/components/ui/input"
@/components/ui/label → Field, Label

As it appears on certificates.

K

Managed by your teacher.

Optional

Falls back to your default room.

85/500

Supports **bold** and `code`.

Select

Radix Select with popper positioning. Use for 4+ options; fewer options → RadioGroup or ToggleGroup.

import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"

Grouped with labels and separators

Checkbox · Switch · Radio

18px controls with a label that is always clickable. Switch = settings that apply immediately; Checkbox = selections submitted with a form.

import { Checkbox, Switch, RadioGroup, RadioGroupItem } from "@/components/ui/checkbox"

Checkbox

Switch

md 36×20 · sm 28×16

Radio group

Tabs

underline = page-level sections (scrolls horizontally on mobile); segmented = compact switcher inside cards and toolbars. Optional count pill.

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

variant="underline"

12 submissions across 4 students.

variant="segmented"

Week view with hourly rows.

ToggleGroup & Slider

ToggleGroup mirrors the segmented tabs look for view options that are not navigation. Slider for bounded numeric input.

import { ToggleGroup, ToggleGroupItem, Slider } from "@/components/ui/misc"
type="single" · icon + label
text only
type="multiple"
Slider · 60 min

Badge

Compact status/meta label. sm (20px) inside rows and cards, md (24px) in headers. Domain badges (status, difficulty) are built on this — prefer them.

import { Badge } from "@/components/ui/badge"
Tonemdsmdoticon
neutralLabelLabelLabel+40 XP
accentLabelLabelLabel+40 XP
successLabelLabelLabel+40 XP
warningLabelLabelLabel+40 XP
dangerLabelLabelLabel+40 XP
infoLabelLabelLabel+40 XP
perfectLabelLabelLabel+40 XP
outlineLabelLabelLabel+40 XP
solidLabelLabelLabel+40 XP

Card

rounded-xl · 1px border · bg-surface · shadow-card (none in dark). Interactive cards lift the border and tint the background on hover — wrap them in a Link, never nest buttons inside a clickable card.

import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"

Static card

Header · content · footer

Content padding is 20px horizontally. Headers are pt-4 pb-3 with title + optional action.
In progress
Interactive cardinteractive adds hover border + tint. Hover me.
XP this week
420+18%

Goal 500 XP

Table

13px cells, 36px header, hairline rows with hover tint; data-state="selected" tints accent. Wraps in horizontal scroll — on mobile prefer a card list for more than 3 columns.

import { Table, THead, TBody, TR, TH, TD } from "@/components/ui/table"
StudentAssignmentStatusXP
AMAlex MorganEnemy AI — FSMUnder review2h ago+120
MCMaria ChenInventory SystemNeeds changesYesterday+100
SRSofia RossiLINQ PracticeApprovedSep 16+40

Dialog

Centered modal, sizes sm 384 · md 512 · lg 672 · xl 896. Header / Body (scrolls) / Footer (buttons right, stacked on mobile). Destructive confirmations put the danger button last.

import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from "@/components/ui/dialog"
size="sm" · confirm
size="md" · form

Sheet / Drawer

right = create/edit flows (full width on mobile, 520px from sm) · left = mobile navigation (280px) · bottom = mobile pickers and quick actions.

import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetBody, SheetFooter } from "@/components/ui/sheet"
side="right"
side="left"
side="bottom"

Popover · Tooltip · HoverCard

Popover for interactive content (filters, Meet link details). Tooltip for short labels on icon buttons, optionally with a shortcut. HoverCard for previews on hover (student, article).

import { Popover, PopoverTrigger, PopoverContent, Tooltip, HoverCard } from "@/components/ui/popover"
Popover
Tooltip (+ shortcut)
AMAlex MorganHoverCard

Toast

sonner, themed in providers.tsx: bottom-right on desktop, above the bottom nav on mobile. Confirm completed actions; offer Undo instead of a confirm dialog for reversible ones. Errors that block the page belong in ErrorState, not a toast.

import { toast } from "sonner"

Progress & ProgressRing

Linear bar for XP, course and goals (accent by default; skill scores use skillTone). Ring for compact percentages in cards. Both animate width/offset over 700ms.

@/components/ui/misc → Progress · @/components/pathly/progression → ProgressRing

Progress · tones & sizes

accent
30%
success
42%
warning
54%
danger
66%
info
78%
neutral
90%
xs
4px
sm
6px
md
8px
lg
10px

ProgressRing

40 · stroke 4
64%
56 · default
72 · success
45
warning
22
danger

Avatar

Initials on a hue-tinted gradient (User.avatarHue), falls back gracefully without images. Title attribute exposes the full name.

import { Avatar } from "@/components/ui/avatar"
AMxs · 20px
AMsm · 24px
AMmd · 32px
AMlg · 40px
AMxl · 56px
AM2xl · 80px
PUPUPUPUPU
hue variants
AMMCSRLW+2
stack · ring

Skeleton

Shimmer placeholder that mirrors the final layout (same heights and radii) so content doesn't jump. Presets live in pathly/states — see Patterns → States.

import { Skeleton } from "@/components/ui/misc"

Text

List row

Profile

Kbd & Command palette

The palette opens with ⌘K / Ctrl K anywhere in the app shell (useCommandPalette() from @/components/shell/command-palette). Navigation shortcuts use G-sequences (G H, G R…). Below: the cmdk primitives rendered inline.

@/components/ui/misc → Kbd · @/components/ui/command

Kbd

K⌘ K
GthenHG then H
EscEsc · ↵ · ↑↓
Header search trigger (md+)

Command (inline)

03

Pathly patterns

Domain components in src/components/pathly, src/components/charts and src/components/shell. They encode product rules (node shapes, rank tiers, scheduling states) — reuse them instead of rebuilding the visuals.

Roadmap nodes

NodeGlyph: shape encodes the type (circle = lesson / practice / quiz / challenge, hexagon = project, diamond = checkpoint, shield = rank exam, seal = milestone), colour + icon encode the state. Completed nodes swap the type icon for a check (showTypeIconWhenDone keeps it). current adds the accent glow — only one per roadmap.

import { NodeGlyph } from "@/components/pathly/roadmap-node"

Type × state matrix

8 types × 5 states + current

Typelockedavailablein_progresscompletedperfectcurrent
Lessonlesson · circle
Practicepractice · circle
Quizquiz · circle
Challengechallenge · circle
Projectproject · hexagon
Checkpointcheckpoint · diamond
Rank Examrank_exam · shield
Milestonemilestone · star

Roadmap strip

Connector: bg-accent/60 up to the current node, bg-border after it.

  1. Classes
  2. Practice
  3. OOP Quiz
  4. Enemy AI
  5. Platformer
  6. Checkpoint
  7. Rank Exam
  8. Chapter done

Sizes & state badges

24px
32px
40px
48px
LockedAvailableIn progressCompletedPerfect

Ranks

RankEmblem is monochrome and geometric. Tier (1–4) = number of nested hexagon rings (tier 4 gets a filled core); order within the tier = one or two pips. muted for locked / next ranks, glow only on celebration screens.

import { RankEmblem } from "@/components/pathly/progression"
Beginnertier 1 · order 0Your journey starts here.
Apprenticetier 1 · order 1Reads and writes simple programs.
Junior Itier 2 · order 2Comfortable with syntax and debugging.
Junior IItier 2 · order 3Builds small features independently.
Developer Itier 3 · order 4Writes clean gameplay code with OOP.
Developer IItier 3 · order 5Designs systems, not just scripts.
Advanced Developertier 4 · order 6Owns architecture decisions.
Engineertier 4 · order 7Ships complete, maintainable games.

Sizes & variants

16px
24px
40px
64px
muted (next rank)
glow (rank up)

Level & XP

XP needed for level L → L+1 is 400 + 100 × (L − 1) (levelFromXp in @/lib/progression). XP values always use tabular and the accent text colour; negative adjustments are danger.

import { LevelBadge, XpBar, XpAmount } from "@/components/pathly/progression"

LevelBadge

Lv12size="sm"
Lv12size="md"
Lv3level 3

XpBar

Level 121,240 / 1,500 XP
default · levelFromXp(11140)
120 / 600 XP
compact · showLevel={false}

XpAmount

+40 XP+40 (earned)
+1,500 XP+1,500
25 XP−25 (teacher adjustment)
120 XPsigned={false}

Streak

Warning-toned flame for an active streak, neutral when broken. StreakDots shows the last 14 days; snowflake = day covered by a Streak Freeze (info).

import { StreakChip, StreakDots } from "@/components/pathly/progression"
12active · 12 days
00 days
12active={false} (not today yet)
StreakDots · 14 days

Progression widgets

Composite cards shared by the student Dashboard, Progress and the teacher's Student details. Pass data from query hooks; they own no fetching.

import { RankProgressCard, WeeklyGoalCard, SkillRow } from "@/components/pathly/widgets"

Rank progress

4/5 requirements
Developer IDeveloper II
  • Reach Level 12Level 12
  • Complete Unity FoundationsChapter 3
  • Complete 3 Projects3 / 3
  • Checkpoint score 80%+84%
  • Pass Developer II ExamNot attempted

SkillRow + SkillMeter

Tone from skillTone(score): ≥75 success · ≥60 accent · ≥45 warning · else danger

C#+478%
Unity+364%
Architecture+641%
Algorithms53%
82%
64%
50%
30%

Next lesson card — 7 scheduling states

The dashboard hero. The display state is derived client-side by lessonDisplayState(lesson, now) in @/lib/progression: server cancelled wins, then time decides. Only starting_soon and live get the accent border + glow and a primary Meet button. Use stateOverride for previews and tests.

import { NextLessonCard } from "@/components/pathly/lesson-schedule"

Meet button · Countdown · LessonRow

MeetButton never shows the raw URL as its label. Inside the 15-minute join window (or live) it is a primary link; before that it is secondary and opens a popover with the link and a copy button; it renders nothing for completed / cancelled lessons.

import { MeetButton, Countdown, LessonRow } from "@/components/pathly/lesson-schedule"

MeetButton by state

scheduled
today
starting_soon
renders null
completed
renders null
cancelled
rescheduled

Countdown

LessonRow

Compact list row. Badge appears for any state except scheduled.

Status badges

One badge per domain enum so labels, tones and icons stay consistent. The maps (SUBMISSION_STATUS, NODE_STATE, PROJECT_STATUS, LESSON_STATE) are exported for custom renderings.

import { SubmissionStatusBadge, NodeStateBadge, ProjectStatusBadge, LessonStateBadge, DifficultyBadge } from "@/components/pathly/status"
SubmissionSubmissionStatusBadge
Not startedIn progressSubmittedUnder reviewNeeds changesApproved
Roadmap nodeNodeStateBadge
LockedAvailableIn progressCompletedPerfect
ProjectProjectStatusBadge
LockedAvailableIn progressCompleted
Lesson (scheduling)LessonStateBadge
ScheduledTodayStarting soonLiveCompletedCancelledRescheduled
DifficultyDifficultyBadge
EasyMediumHard

Empty · Error · Loading

Every data-driven region handles all three. QueryBoundary wires them to a TanStack query: skeleton while pending, ErrorState with Retry on error, empty when isEmpty(data). Preview in the app via account menu → Simulate (slow / error / empty).

import { QueryBoundary, EmptyState, ErrorState, CardSkeleton, ListSkeleton, GridSkeleton } from "@/components/pathly/states"

EmptyState

Icon + title + one-line reason + optional next step

You're all caught up

No homework right now. New assignments appear here as soon as your teacher publishes them.

ErrorState

role=alert · Retry shows a loading state

compact

Inside cards and popovers

No notifications yet

We'll let you know when something happens.

Skeleton presets

GridSkeleton

count · className · itemClassName

Notification center

Header bell → popover (All / Unread tabs, grouped Today / Yesterday / Earlier, 12 items max) → full page. Unread = semibold title + accent dot. Lesson reminders inside the join window get an inline Join button; reschedules show old → new time. Every type maps to an icon + tone in NOTIFICATION_META; emails mirror the same types.

import { NotificationBell, NotificationList, NotificationItem } from "@/components/shell/notification-center"

Popover (static preview)

Real component needs the query client + router

2

Types → icon + tone

@/components/pathly/icons → NOTIFICATION_META
  • lesson_createdaccent
  • lesson_rescheduledwarning
  • lesson_cancelleddanger
  • lesson_24h_reminderaccent
  • lesson_1h_reminderaccent
  • lesson_15m_reminderaccent
  • assignment_createdinfo
  • assignment_due_soonwarning
  • submission_reviewedsuccess
  • submission_receivedinfo
  • teacher_commentneutral
  • achievement_unlockedperfect
  • xp_awardedaccent
  • level_upaccent
  • rank_upperfect

Calendar event chips

One chip per CalendarEventKind using CALENDAR_EVENT_META (label, icon, tone). Tone lives on the left border, tint and icon; the title stays text-fg for legibility. Cancelled = dashed + strikethrough; rescheduled = warning clock.

import { CALENDAR_EVENT_META } from "@/components/pathly/icons"

Month chip

18:00Programming Architecture23:59Interfaces Intro23:59Platformer due18:00Gameplay Checkpoint18:00Developer II Exam

States

19:00Unity Physics18:00Unity Physics

Week block

LessonUnity Physics18:00–19:00
Rank examDeveloper II18:00–19:00

Legend / agenda dot

  • Lessonaccent
  • Homework deadlinewarning
  • Project deadlineinfo
  • Checkpointsuccess
  • Rank examperfect

Code

CodeBlock lazy-loads a fine-grained Shiki bundle (C#, TypeScript, JSON, Bash, Python) with GitHub light/dark themes, and falls back to plain monospace until ready. Filename, language label, copy, line numbers and highlighted lines. RichText renders **bold** and `code` in plain strings (feedback, hints).

import { CodeBlock, CopyButton, RichText } from "@/components/pathly/code-block"
EnemyController.cs
C#
public class EnemyController : MonoBehaviour
{
    [SerializeField] private float chaseRange = 6f;
    private IEnemyState _state;
 
    private void Update()
    {
        _state = _state.Tick(this);
    }
}

RichText

Great work! Consider caching GetComponent<Rigidbody>() in Awake instead of calling it every frame.

CopyButton

meet.google.com/abc-defg-hij

Charts

Single-series, token-coloured, hover-first. Bars ≤ 24px with 4px rounded data-end, 2px lines with a 10% area wash, hairline recessive grid, end dot with a surface ring. Text never wears the series colour; every mark has a tooltip and an accessible label.

import { ColumnChart, LineChart, ActivityHeatmap, Sparkline } from "@/components/charts"

ColumnChart

Weekly XP · highlightIndex = current day

MonTueWedThuFriSatSun

LineChart

Skill score over 13 weeks · yMax 100

W1W7W13
W142%
W245%
W347%
W451%
W552%
W656%
W758%
W861%
W963%
W1066%
W1170%
W1274%
W1378%

ActivityHeatmap

16 weeks · GitHub-style, 5 steps of accent

Sparkline

Muted line + accent end dot, inside stat cards

Study time
6h 40m

This week

Quiz average
86%+4

Page scaffolding

PageHeader (breadcrumbs, eyebrow, title, description, actions — actions wrap under the title on mobile), SectionHeader (15px title, optional icon, count and action), StatCard, MetaRow for detail sidebars.

import { Page, PageHeader, SectionHeader, StatCard, MetaRow, Breadcrumbs } from "@/components/pathly/layout"

PageHeader

Homework · Chapter 4

Enemy AI — Finite State Machine

Implement Idle, Patrol and Chase states for the enemy using a simple finite state machine.

SectionHeader

Upcoming lessons4

Next 14 days

MetaRow · Breadcrumbs

Details

Due soon
DueThu, Sep 24 · 23:59
Reward+120 XP
TeacherINInzavic

StatCard row

Level
12

1,240 / 1,500 XP

Streak
12 days

Longest 19

Lessons
18 / 42+3
Late work
1+1

Icon tiles & domain icons

IconTile = tinted rounded square (sm 28 · md 36 · lg 44) used in lists, notifications and empty states. Domain icon maps keep the same concept on the same glyph everywhere.

import { IconTile, TONE_CLASSES, NODE_META, SKILL_ICONS, ACHIEVEMENT_ICONS } from "@/components/pathly/icons"

IconTile · tones × sizes

sm
md
lg

accent · warning · info · success · danger · perfect · neutral

Skills · node types

csharp
unity
debugging
architecture
algorithms
ui
lesson
practice
quiz
challenge
project
checkpoint
rank_exam
milestone

Achievement icons

ACHIEVEMENT_ICONS — keyed by Achievement.icon

footprints
bug
eye-off
target
flame
hammer
book
zap
trophy
brain
rocket
shield
clock
star
puzzle
git
sunrise
layers