ui-conventions
UI patterns, styling, and component conventions for the Ckourse app. Use when creating pages, components, or UI elements.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
UI patterns, styling, and component conventions for the Ckourse app. Use when creating pages, components, or UI elements.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | ui-conventions |
| description | UI patterns, styling, and component conventions for the Ckourse app. Use when creating pages, components, or UI elements. |
Instructions for creating any new page, element, or component in the Ckourse app.
cn() from @/lib/utils (clsx + tailwind-merge)src/
App.tsx # Root layout and page composition
main.tsx # Entry point
index.css # Centralized theme (single source of truth)
lib/
utils.ts # cn() helper
components/
ui/ # shadcn/ui base primitives (Card, Badge, Button, etc.)
Header.tsx # App-level components
Sidebar.tsx # App sidebar with navigation
CourseCard.tsx # Feature components
ProgressBar.tsx # Reusable custom components
SquircleSearch.tsx # Reusable squircle search input
SquircleButton.tsx # Reusable squircle button (primary/secondary/ghost)
pages/
Dashboard.tsx # Library / Home page
data/
courses.ts # Data types and mock data
assets/ # Static assets (images, logos)
| Type | Location | Example |
|---|---|---|
| shadcn/ui primitive | src/components/ui/ | button.tsx, dialog.tsx |
| Feature component | src/components/ | LessonCard.tsx, Sidebar.tsx |
| Page component | src/pages/ | Dashboard.tsx, Settings.tsx |
| Data types / mock data | src/data/ | lessons.ts, users.ts |
| Utility functions | src/lib/ | format.ts, api.ts |
| Static assets | src/assets/ | logo-dark.png |
All design tokens live in src/index.css. Never hardcode color values in components.
| Token | Value | Usage |
|---|---|---|
--background | #0A0A0A | Page background |
--foreground | #EDEDED | Primary text |
--card | #111214 | Card surfaces |
--primary | #C8F135 | Electric Lime accent, CTAs |
--secondary | #1A1C1F | Hover states, secondary surfaces |
--muted | #111214 | Subdued backgrounds |
--muted-foreground | #7A7D85 | Secondary text, labels |
--border | #252729 | Borders, dividers |
--destructive | #FF6B6B | Errors, danger actions |
--info | #5B9CF6 | Informational states |
--success | #C8F135 | Success states (same as primary) |
// Correct - use theme tokens
<div className="bg-background text-foreground" />
<p className="text-muted-foreground" />
<button className="bg-primary text-primary-foreground" />
// Wrong - never hardcode hex values
<div style={{ color: "#EDEDED" }} />
Use Tailwind's opacity syntax for transparent variants:
<Badge className="bg-primary/15 text-primary border-primary/20" />
<div className="bg-info/15 text-info" />
Three font families are defined in the theme:
| Font | Variable | Tailwind Class | Usage |
|---|---|---|---|
| DM Sans | --font-sans | font-sans | Body text (default) |
| Syne | --font-heading | font-heading | Headings, titles |
| JetBrains Mono | --font-mono | font-mono | Numbers, stats, code |
// Page heading
<h2 className="font-heading text-2xl font-bold text-foreground">Title</h2>
// Body text
<p className="font-sans text-sm text-muted-foreground">Description</p>
// Stats / numeric data
<span className="font-mono text-xs font-medium text-muted-foreground">42/80</span>
// Bold brand text
<span className="text-primary">CK</span>OURSE
className prop and merge it with cn()// src/components/MyComponent.tsx
import { cn } from "@/lib/utils";
interface MyComponentProps {
title: string;
className?: string;
}
export function MyComponent({ title, className }: MyComponentProps) {
return (
<div className={cn("rounded-xl border border-border bg-card p-4", className)}>
<h3 className="font-sans text-sm font-semibold text-foreground">
{title}
</h3>
</div>
);
}
Two reusable components wrap the squircle layered-border pattern for use across the app:
SquircleSearch — search input with squircle shape, focus border, and clear button:
import { SquircleSearch } from "@/components/SquircleSearch";
<SquircleSearch
value={search}
onChange={setSearch}
placeholder="Search courses..."
className="flex-1"
/>
Props: value, onChange, placeholder?, className?
SquircleButton — button with squircle shape and three variants:
import { SquircleButton } from "@/components/SquircleButton";
// Primary CTA (lime background)
<SquircleButton variant="primary" onClick={handleImport}>
<Plus className="size-4" weight="bold" />
Import Course
</SquircleButton>
// Secondary with active/toggle state (bordered, switches to accent when active)
<SquircleButton variant="secondary" active={showFilters} onClick={toggle}>
<Funnel className="size-4" />
Filters
</SquircleButton>
// Ghost (no border, hover background)
<SquircleButton variant="ghost" onClick={handleClick}>
Label
</SquircleButton>
Props: variant? ("primary" | "secondary" | "ghost"), active?, className?, plus all native button attributes.
shadcn/ui primitives live in src/components/ui/ and use CVA for variants:
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
<Card className="overflow-hidden border-border">
<CardContent className="p-4">
<Badge variant="secondary">Label</Badge>
</CardContent>
</Card>
To add a new shadcn component, run:
npx shadcn@latest add <component-name>
cn()Always use cn() to merge classes. This ensures Tailwind classes resolve correctly:
import { cn } from "@/lib/utils";
<div className={cn(
"base-classes here",
isActive && "bg-primary text-primary-foreground",
className
)} />
| Pattern | Classes |
|---|---|
| Page container | mx-auto max-w-6xl px-6 py-8 |
| Section header | mb-6 flex items-baseline justify-between |
| Card grid | grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 |
| Flex row with gap | flex items-center gap-3 |
| Flex column with gap | flex flex-col gap-3 |
The base radius is 0.625rem. Ckourse uses two shape systems:
Standard border-radius — for cards, containers, and badges:
<div className="rounded-xl" /> // Cards, containers, page panels
<div className="rounded-md" /> // Standard buttons, text inputs
<div className="rounded-full" /> // Badges, avatars, progress bars
Squircle (iOS-style superellipse) — for interactive surfaces that need a premium feel. Uses SVG <clipPath> definitions at root level and CSS utility classes.
Two variants are available:
| Variant | CSS Class | Usage |
|---|---|---|
squircle | .squircle | More rounded — sidebar buttons, search bars, toolbar buttons, tooltips |
squircle-subtle | .squircle-subtle | Less rounded — cards and wider rectangular elements |
// Apply squircle shape via CSS class
<button className="squircle bg-sidebar-accent p-3">
<div className="squircle-subtle bg-card">
| Element | Shape | Class |
|---|---|---|
| Nav button backgrounds | Squircle | squircle |
| Sidebar toggle button | Squircle | squircle |
| Search bar | Squircle | squircle |
| Toolbar buttons | Squircle | squircle |
| Tooltips | Squircle | squircle |
| Icon containers (collapsed states) | Squircle | squircle |
| Course cards | Squircle (subtle) | squircle-subtle |
| Badges / pills | Standard | rounded-full |
| Page containers / panels | Standard | rounded-xl |
| Inner buttons (inside cards) | Standard | rounded-lg or rounded-md |
| Modals / dialogs (surface) | Either | squircle or rounded-xl |
Since clip-path clips box-shadow, outline, and ring, use layered divs to create borders on squircled elements:
// Squircle with border (e.g., search bar, card)
<div className="relative">
<div className="squircle absolute inset-0 bg-border" /> {/* border layer */}
<div className="squircle absolute inset-px bg-card" /> {/* fill layer */}
<div className="relative px-4 py-2.5"> {/* content */}
...
</div>
</div>
// With focus state (use group)
<div className="group/search relative">
<div className="squircle absolute inset-0 bg-border/25 transition-colors group-focus-within/search:bg-primary" />
<div className="squircle absolute inset-px bg-card" />
<div className="relative">
<input ... />
</div>
</div>
// With hover state (e.g., card)
<div className="group relative">
<div className="squircle-subtle absolute inset-0 bg-border" />
<div className="squircle-subtle absolute inset-[1px] bg-card transition-colors group-hover:bg-secondary" />
...
</div>
Both SVG clip-path definitions must exist in the DOM. They are currently rendered by SquircleClipDefs in src/components/Sidebar.tsx. If the sidebar is not present on a page, ensure the clip-path SVGs are rendered elsewhere (e.g., in App.tsx):
<svg width="0" height="0" className="absolute">
<defs>
<clipPath id="squircle" clipPathUnits="objectBoundingBox">
<path d="M 0.5,0 C 0.82,0 0.95,0 0.975,0.025 C 1,0.05 1,0.18 1,0.5 C 1,0.82 1,0.95 0.975,0.975 C 0.95,1 0.82,1 0.5,1 C 0.18,1 0.05,1 0.025,0.975 C 0,0.95 0,0.82 0,0.5 C 0,0.18 0,0.05 0.025,0.025 C 0.05,0 0.18,0 0.5,0 Z" />
</clipPath>
<clipPath id="squircle-subtle" clipPathUnits="objectBoundingBox">
<path d="M 0.5,0 C 0.9,0 0.97,0 0.985,0.015 C 1,0.03 1,0.1 1,0.5 C 1,0.9 1,0.97 0.985,0.985 C 0.97,1 0.9,1 0.5,1 C 0.1,1 0.03,1 0.015,0.985 C 0,0.97 0,0.9 0,0.5 C 0,0.1 0,0.03 0.015,0.015 C 0.03,0 0.1,0 0.5,0 Z" />
</clipPath>
</defs>
</svg>
clip-path clips overflow — no visible box-shadow, outline, or ring on squircled elements. Use the layered div border pattern instead.squircle-subtle for wider elements like cards.Use transition-colors for hover/focus animations:
// Button / clickable element
<button className="rounded-md p-2 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
// Card hover
<Card className="transition-colors hover:bg-secondary">
Use Lottie animations for loading indicators. The library lottie-react is installed. Lottie JSON files live in src/assets/lotties/.
import Lottie from "lottie-react";
import loadingAnimation from "@/assets/lotties/loading.json";
// Inline loading (inside a card or section)
<Lottie animationData={loadingAnimation} loop className="size-28" />
// Full-screen loading overlay (for blocking operations like imports)
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm">
<Lottie animationData={loadingAnimation} loop className="size-40" />
<p className="mt-2 font-sans text-sm font-semibold text-foreground">
Importing course...
</p>
<p className="mt-1.5 font-sans text-xs text-muted-foreground">
Setting up your library
</p>
</div>
size-28 for inline/contextual loading, size-40 for full-screen overlaysbg-background/80 backdrop-blur-sm for the backdropfont-sans text-sm font-semibold) and optional sublabel (text-xs text-muted-foreground) below the animationasync fn in Rust so they run off the main thread — otherwise the webview freezes and animations won't playUse @phosphor-icons/react. Always import with the Icon suffix (non-deprecated API), aliased to a clean name. Standard size is size-4, default weight is "regular":
import { GearSixIcon as GearSix, UserIcon as User } from "@phosphor-icons/react";
<GearSix className="size-4" />
<User className="size-4" weight="bold" />
Available weights: "thin", "light", "regular" (default), "bold", "fill", "duotone". Use "bold" for active/emphasized states and "regular" for default.
Define types and data in src/data/:
// src/data/lessons.ts
export interface Lesson {
id: number;
title: string;
duration: number;
}
export const lessons: Lesson[] = [
{ id: 1, title: "Introduction", duration: 12 },
];
Use the @/ alias for all project imports (configured in tsconfig.json):
// Correct
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
// Wrong
import { cn } from "../../lib/utils";
src/index.css (no hardcoded colors)cn() for class merging@/ import aliasclassName prop supported and mergedfont-sans, font-heading, or font-mono)transition-colors and hover statessquircle for interactive surfaces, squircle-subtle for cards, rounded-full for badgesring or border)@phosphor-icons/react with size-4 default