# kui-ejs v0.1.0 — Full component catalog Generated 2026-08-17T03:58:18.720Z. Machine-readable catalog of every EJS partial, theme, design token, and convention in the kui-ejs library. Consume this from an AI assistant to know what exists, where it lives, and how to use it. ## Layers - **ui** — modules/ui/ — Primitive EJS partials (atoms + molecules). Stateless presentational HTML; no business logic. Included via <%- include('modules/ui/', { ...locals }) %>. - **app** — modules/app/ — Application-level patterns (shells, navigation, drawers, search, state placeholders). Compose ui/ partials and own page-level structure. - **domain** — modules/domain// — Industry-vertical partials (auth, payment, address, modem, invoice, ups, api-doc, …). Compose ui/ + app/. modules/domain/modem/ also exports a TypeScript types.ts. - **theme** — views/theme// — Full multi-page server-rendered demos. Each theme has its own Express router under src/routes/themes/.ts. - **library** — External npm packages featured in the showcase. filePath points to the showcase entry, not first-party source; the `external` field carries homepage/npm/github links. ## Conventions - **templating** — EJS 3 server-rendered. All data passed via res.render(view, locals). Use <%= value %> for escaped output; reserve <%- html %> for trusted EJS includes only (audited in docs/raw-output-allowlist.md). - **icons** — Font Awesome 6 Free via CDN. Decorative icons require aria-hidden="true"; icon-only interactive elements require aria-label. No inline SVG; no other icon libraries. - **styling** — Tailwind CSS 4 + CSS-variable design tokens defined in public/assets/css/input.css. Use token names (bg-primary, text-text-secondary, border-border-focus) — never raw hex. scripts/audit-tokens.sh enforces this in CI. - **includes** — Include partials with relative paths: <%- include('../../partials/_head') %>. Use modules/ui|app|domain partials by referencing their repo-relative paths from the consuming view. - **accessibility** — Semantic HTML. Apply aria-busy / aria-invalid / aria-describedby / aria-pressed / aria-expanded where applicable. Always include focus-visible:ring-2 focus-visible:ring-border-focus on interactive elements. Standard disabled pattern: disabled:opacity-50 disabled:cursor-not-allowed. - **rawOutput** — Unescaped EJS output (<%- %>) is restricted; every site is enumerated in docs/raw-output-allowlist.md and validated by scripts/audit-raw-output.sh. Adding a new raw-output site requires a justification entry in that doc. - **fileNaming** — EJS partials: PascalCase .ejs in modules/{ui,app,domain}/. Shared partials: underscore-prefixed camelCase .ejs in views/partials/. Routes: camelCase .ts. Data: camelCase .data.ts. Theme directories: lowercase. ## Design tokens | Token | Light value | Purpose | |---|---|---| | `--primary` | `#3b82f6` | Primary actions | | `--primary-hover` | `#2563eb` | Hover state | | `--primary-active` | `#1d4ed8` | Active/pressed | | `--primary-subtle` | `#eff6ff` | Tinted backgrounds | | `--primary-fg` | `#ffffff` | Text on primary | | `--secondary` | `#8b5cf6` | Secondary actions | | `--secondary-hover` | `#7c3aed` | Secondary hover | | `--secondary-active` | `#6d28d9` | Secondary active | | `--secondary-subtle` | `#f3f0ff` | Secondary tint | | `--secondary-fg` | `#ffffff` | Text on secondary | | `--surface-base` | `#ffffff` | Page background | | `--surface-raised` | `#f9fafb` | Cards | | `--surface-overlay` | `#f3f4f6` | Hover overlays | | `--surface-sunken` | `#e5e7eb` | Inset areas | | `--text-primary` | `#111827` | Body text | | `--text-secondary` | `#6b7280` | Muted text | | `--text-disabled` | `#9ca3af` | Disabled text | | `--text-inverse` | `#ffffff` | Text on dark bg | | `--border` | `#e5e7eb` | Default borders | | `--border-strong` | `#d1d5db` | Emphasized borders | | `--border-focus` | `#3b82f6` | Focus rings | | `--success` | `#22c55e` | Success state | | `--success-subtle` | `#f0fdf4` | Success background | | `--success-fg` | `#14532d` | Text on success | | `--warning` | `#f59e0b` | Warning state | | `--warning-subtle` | `#fffbeb` | Warning background | | `--warning-fg` | `#78350f` | Text on warning | | `--error` | `#ef4444` | Error/danger state | | `--error-subtle` | `#fef2f2` | Error background | | `--error-fg` | `#7f1d1d` | Text on error | | `--info` | `#06b6d4` | Informational state | | `--info-subtle` | `#ecfeff` | Info background | | `--info-fg` | `#164e63` | Text on info | ## Components — app / App ### AppShell (`app-shell`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppShell.ejs` - **Status:** stable (since 2025-03) - **Description:** Full-screen layout wrapper with logo, sidebar and topbar slots. Sidebar renders as an aside on desktop and opens via a drawer on mobile. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Sidebar + topbar + content* ```ejs <%- include('modules/app/AppShell', { logoContent: 'Acme', sidebarContent: '<%- include("modules/app/AppSidebar", { navGroups: navGroups, activeId: activeId, searchable: true }) %>', topbarContent: '<%- include("modules/app/AppTopBar", { children: topbarHtml }) %>', children: bodyHtml }) %> ``` *Sadece topbar (sidebar yok)* ```ejs <%- include('modules/app/AppShell', { topbarContent: '<%- include("modules/app/AppTopBar", { children: topbarHtml }) %>', children: bodyHtml }) %> ``` ### AppSidebar (`app-sidebar`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppSidebar.ejs` - **Status:** stable (since 2025-03) - **Description:** Collapsible side navigation. Accepts navGroups or navItems with a built-in collapse toggle. The searchable prop adds an inline filter and a footer slot can host a user block or any content. - **Design tokens:** --border, --border-focus, --primary, --primary-subtle, --secondary, --surface-base, --surface-overlay, --text-disabled, --text-primary, --text-secondary **Variants:** *Açık (grouped nav + footer)* ```ejs <%- include('modules/app/AppSidebar', { navGroups: [ { label: 'Main', items: [ { id: 'dashboard', label: 'Dashboard', icon: 'fa-solid fa-house', href: '/' }, { id: 'analytics', label: 'Analytics', icon: 'fa-solid fa-chart-bar', badge: 3, href: '/analytics' }, ]}, { label: 'Settings', items: [ { id: 'team', label: 'Team', icon: 'fa-solid fa-users', href: '/team' }, { id: 'settings', label: 'Settings', icon: 'fa-solid fa-gear', href: '/settings' }, ]}, ], activeId: currentPage, footerContent: userMenuHtml }) %> ``` *Arama filtreli sidebar* ```ejs <%- include('modules/app/AppSidebar', { navGroups: navGroups, activeId: currentPage, searchable: true }) %> ``` *Daraltılmış (icon-only)* ```ejs <%- include('modules/app/AppSidebar', { navGroups: navGroups, activeId: currentPage, collapsed: true }) %> ``` ### AppTopBar (`app-top-bar`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppTopBar.ejs` - **Status:** stable (since 2025-03) - **Description:** Top bar wrapper passed into AppShell's header slot. The logo slot anchors the left side while children (GlobalSearch, UserMenu, Button, etc.) are arranged in a flex row. **Variants:** *Arama + actions + kullanıcı* ```ejs <%- include('modules/app/AppTopBar', { children: ` <%- include('modules/app/GlobalSearch', { placeholder: 'Search everything…' }) %> <%- include('modules/domain/common/user/UserMenu', { name: user.name, role: user.role }) %> ` }) %> ``` *Logo + action + kullanıcı* ```ejs <%- include('modules/app/AppTopBar', { logoContent: 'Acme Dashboard', children: `
<%- include('modules/ui/Button', { children: 'New', iconLeft: '' }) %> <%- include('modules/domain/common/user/UserMenu', { name: user.name }) %> ` }) %> ``` ### AppNav (`app-nav`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppNav.ejs` - **Status:** stable (since 2025-03) - **Description:** Horizontal navigation bar. Renders inline links on desktop and a hamburger that opens a NavDrawer on mobile. Provides logo, navItems and actions slots. - **Design tokens:** --border, --border-focus, --primary, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Marketing bar (logo + links + CTA)* ```ejs <%- include('modules/app/AppNav', { logoContent: 'Acme', navItems: [ { label: 'Home', href: '/', active: currentPath === '/' }, { label: 'Features', href: '/features', active: currentPath === '/features' }, { label: 'Pricing', href: '/pricing', active: currentPath === '/pricing' }, { label: 'Docs', href: '/docs', active: currentPath === '/docs' }, ], children: ` <%- include('modules/ui/Button', { variant: 'outline', children: 'Sign in', href: '/login' }) %> <%- include('modules/ui/Button', { children: 'Get started', href: '/register' }) %> ` }) %> ``` *App bar (links + UserMenu)* ```ejs <%- include('modules/app/AppNav', { logoContent: 'Dashboard', navItems: [ { label: 'Overview', href: '/dashboard', active: true }, { label: 'Analytics', href: '/dashboard/analytics' }, { label: 'Projects', href: '/dashboard/projects' }, { label: 'Team', href: '/dashboard/team' }, ], children: ` <%- include('modules/domain/common/user/UserMenu', { name: user.name, role: user.role }) %> ` }) %> ``` ### NavDrawer (`nav-drawer`) - **Layer:** app - **Category:** App - **File:** `modules/app/NavDrawer.ejs` - **Status:** stable (since 2025-03) - **Description:** Wrapper that wraps any trigger and children inside a drawer. Manages its own open/closed state. Used as AppNav's mobile menu and also works standalone. - **Design tokens:** --primary, --primary-subtle, --surface-overlay, --text-primary **Variants:** *Sol nav (standalone)* ```ejs <%- include('modules/app/NavDrawer', { title: 'Navigation', side: 'left', navItems: [ { label: 'Home', href: '/', active: currentPath === '/' }, { label: 'Features', href: '/features' }, { label: 'Pricing', href: '/pricing' }, { label: 'Blog', href: '/blog' }, ] }) %> ``` *Sağ panel (cart/settings)* ```ejs <%- include('modules/app/NavDrawer', { title: 'Cart (' + cartCount + ')', side: 'right', children: cartItemsHtml, footerContent: '' }) %> ``` ### GlobalSearch (`global-search`) - **Layer:** app - **Category:** App - **File:** `modules/app/GlobalSearch.ejs` - **Status:** stable (since 2025-04) - **Description:** Command-palette-style global search field. Supports a categorised result list, keyboard navigation and result selection. - **Design tokens:** --border, --primary, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Sonuçlarla (with results)* ```ejs <%- include('modules/app/GlobalSearch', { placeholder: 'Search…', query: req.query.q || '', results: searchResults, action: '/search' }) %> ``` *Loading state* ```ejs <%- include('modules/app/GlobalSearch', { placeholder: 'Search…', query: req.query.q, loading: true }) %> ``` ### AppCommandBar (`app-command-bar`) - **Layer:** app - **Category:** App - **File:** `modules/app/CommandPalette/CommandPalette.ejs` - **Status:** beta (since 2025-04) - **Description:** Keyboard-first command palette. Opens with ⌘K; an items prop accepts custom commands while a default navigation/actions/recent set is included. - **Design tokens:** --border, --border-focus, --info, --info-fg, --info-subtle, --primary, --secondary, --surface-overlay, --surface-raised, --surface-sunken, --text-primary, --text-secondary **Variants:** *Varsayılan komutlar (açık panel)* ```ejs <%- include('modules/app/AppCommandBar') %> ``` *Özel items + trigger butonu* ```ejs <%- include('modules/app/AppCommandBar', { placeholder: 'Search commands…', items: [ { icon: 'fa-solid fa-basket-shopping', label: 'View Orders', shortcut: 'G O', category: 'Navigation', href: '/orders' }, { icon: 'fa-solid fa-box', label: 'Inventory', shortcut: 'G I', category: 'Navigation', href: '/inventory' }, { icon: 'fa-solid fa-tag', label: 'New Sale', shortcut: 'C N', category: 'Actions' }, { icon: 'fa-solid fa-file-export', label: 'Export Report', shortcut: 'C E', category: 'Actions' }, { icon: 'fa-solid fa-clock-rotate-left', label: 'Order #1042', category: 'Recent', href: '/orders/1042' }, { icon: 'fa-solid fa-clock-rotate-left', label: 'Customer: Acme Co', category: 'Recent', href: '/customers/acme' }, ] }) %> ``` *Fuzzy search + özel gruplar* ```ejs <%- include('modules/app/AppCommandBar', { placeholder: 'Try "kbd", "asgn" or "rls"…', items: [ { icon: 'fa-solid fa-keyboard', label: 'Open Keyboard Shortcuts', shortcut: '?', category: 'Help', keywords: ['kbd'] }, { icon: 'fa-solid fa-book', label: 'Browse Documentation', shortcut: 'G H', category: 'Help' }, { icon: 'fa-solid fa-bell', label: 'Notification Preferences', shortcut: 'G N', category: 'Preferences' }, { icon: 'fa-solid fa-screwdriver-wrench', label: 'Assign Reviewer to PR-42', shortcut: 'A R', category: 'Workflows', keywords: ['asgn'] }, { icon: 'fa-solid fa-rocket', label: 'Release & Tag v1.4.0', shortcut: 'R T', category: 'Workflows', keywords: ['rls'] }, ] }) %> ``` ### Form (`form`) - **Layer:** app - **Category:** App - **File:** `modules/app/Form.ejs` - **Status:** stable (since 2025-03) - **Description:** Form layout wrapper with title, description, error and actions slots. `columns` prop renders fields in a 1 or 2 column grid. - **Design tokens:** --border, --primary, --secondary, --text-primary, --text-secondary **Variants:** *Single column* ```ejs <%- include('modules/app/Form', { title: 'Edit Profile', description: 'Update your personal information.', action: '/account/profile', actionsContent: ` <%- include('modules/ui/Button', { variant: 'outline', children: 'Cancel', href: '/account' }) %> <%- include('modules/ui/Button', { type: 'submit', children: 'Save changes' }) %> `, children: ` <%- include('modules/ui/Input', { label: 'Name', name: 'name', value: user.name }) %> <%- include('modules/ui/Input', { label: 'Email', name: 'email', type: 'email', value: user.email }) %> <%- include('modules/ui/Textarea', { label: 'Bio', name: 'bio', value: user.bio }) %> ` }) %> ``` *Two column* ```ejs <%- include('modules/app/Form', { title: 'Personal Details', columns: 2, action: '/account/details', actionsContent: `<%- include('modules/ui/Button', { type: 'submit', children: 'Save' }) %>`, children: ` <%- include('modules/ui/Input', { label: 'First Name', name: 'firstName' }) %> <%- include('modules/ui/Input', { label: 'Last Name', name: 'lastName' }) %> <%- include('modules/ui/Input', { label: 'Email', name: 'email', type: 'email' }) %> <%- include('modules/ui/Input', { label: 'Phone', name: 'phone', type: 'tel' }) %> ` }) %> ``` ### FilterBar (`filter-bar`) - **Layer:** app - **Category:** App - **File:** `modules/app/FilterBar.ejs` - **Status:** stable (since 2025-03) - **Description:** Select, multiselect, daterange and text-based filter panel. Supports URL-based filtering via GET form submit. - **Design tokens:** --border, --surface-raised **Variants:** *Full filter set* ```ejs <%- include('modules/app/FilterBar', { action: req.path, method: 'get', fields: [ { type: 'select', id: 'status', label: 'Status', options: [{value:'active',label:'Active'},{value:'inactive',label:'Inactive'}] }, { type: 'select', id: 'category', label: 'Category', options: categories.map(c => ({value: c.id, label: c.name})) }, { type: 'daterange', id: 'date', label: 'Date range' }, ], values: req.query }) %> ``` *Compact filters* ```ejs <%- include('modules/app/FilterBar', { action: req.path, fields: [ { type: 'select', id: 'status', label: 'Status', options: statusOptions }, { type: 'select', id: 'role', label: 'Role', options: roleOptions }, ], values: req.query }) %> ``` ### DetailHeader (`detail-header`) - **Layer:** app - **Category:** App - **File:** `modules/app/DetailHeader.ejs` - **Status:** stable (since 2025-03) - **Description:** Page header for detail/record views: title, subtitle, status badge, action buttons, and optional tab navigation. - **Design tokens:** --border, --border-focus, --border-strong, --error, --error-fg, --error-subtle, --info, --info-fg, --info-subtle, --primary, --primary-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-raised, --surface-sunken, --text-primary, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *With actions, no tabs* ```ejs <%- include('modules/app/DetailHeader', { title: 'Invoice #1042', subtitle: 'Created 3 days ago · Due Jan 15, 2026', status: 'PAID', statusVariant: 'success', actionsContent: ` <%- include('modules/ui/Button', { variant: 'outline', iconLeft: '', children: 'Edit' }) %> <%- include('modules/ui/Button', { variant: 'danger', iconLeft: '', children: 'Delete' }) %> ` }) %> ``` *With tabs* ```ejs <%- include('modules/app/DetailHeader', { title: 'Project Alpha', subtitle: 'Started Jan 2026 · 5 members', status: 'ACTIVE', statusVariant: 'success', tabs: [ { value: 'overview', label: 'Overview' }, { value: 'tasks', label: 'Tasks' }, { value: 'members', label: 'Members' }, { value: 'settings', label: 'Settings' }, ], activeTab: req.query.tab || 'overview', actionsContent: `<%- include('modules/ui/Button', { variant: 'outline', iconLeft: '', children: 'Settings' }) %>` }) %> ``` ### ErrorState (`error-state`) - **Layer:** app - **Category:** App - **File:** `modules/app/ErrorState.ejs` - **Status:** stable (since 2025-03) - **Description:** Error state pairing an inline alert banner with a centered empty state. Optional retry action. - **Design tokens:** --border, --border-focus, --error, --error-fg, --error-subtle, --primary, --secondary, --surface-overlay, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/app/ErrorState', { message: 'Failed to load user data. Please check your connection.' }) %> ``` *With retry link* ```ejs <%- include('modules/app/ErrorState', { title: 'Database connection failed', message: 'Could not connect to the database. Please try again.', retryHref: req.originalUrl, retryLabel: 'Try again' }) %> ``` ### NotFoundState (`not-found-state`) - **Layer:** app - **Category:** App - **File:** `modules/app/NotFoundState.ejs` - **Status:** stable (since 2025-03) - **Description:** Not-found / empty record state with optional go-back action. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-overlay, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/app/NotFoundState') %> ``` *With back link* ```ejs <%- include('modules/app/NotFoundState', { title: 'User not found', description: 'This user account doesn't exist or may have been deleted.', backHref: '/users', backLabel: 'Back to users' }) %> ``` ### LoadingState (`loading-state`) - **Layer:** app - **Category:** App - **File:** `modules/app/LoadingState.ejs` - **Status:** stable (since 2025-03) - **Description:** Skeleton loading animations. Variants: spinner / table / cards / list / detail / form. - **Design tokens:** --border, --surface-base, --surface-sunken **Variants:** *Spinner* ```ejs <%- include('modules/app/LoadingState', { variant: 'spinner' }) %> ``` *Table skeleton* ```ejs <%- include('modules/app/LoadingState', { variant: 'table', rows: 5, cols: 4 }) %> ``` *Cards skeleton* ```ejs <%- include('modules/app/LoadingState', { variant: 'cards', cards: 3 }) %> ``` *List skeleton* ```ejs <%- include('modules/app/LoadingState', { variant: 'list', rows: 4 }) %> ``` *Form skeleton* ```ejs <%- include('modules/app/LoadingState', { variant: 'form', rows: 3 }) %> ``` ### SplashScreen (`splash-screen`) - **Layer:** app - **Category:** App - **File:** `modules/app/SplashScreen.ejs` - **Status:** beta (since 2025-05) - **Description:** Full-screen overlay shown during app initialisation. Accepts a logo slot, optional progress bar, and fades out when visible=false. - **Design tokens:** --primary, --secondary, --surface-base, --surface-sunken, --text-secondary **Variants:** *Logo + progress + message* ```ejs <%- include('modules/app/SplashScreen', { logoContent: 'Acme', message: 'Loading your workspace…', progress: loadProgress }) %> ``` *Spinner only* ```ejs <%- include('modules/app/SplashScreen') %> ``` ### AccessibilityKit (`accessibility-kit`) - **Layer:** app - **Category:** App - **File:** `modules/app/AccessibilityKit.ejs` - **Status:** stable (since 2026-05) - **Description:** A11y helpers bundle that exposes the SkipLink, LiveRegion (Announcer) and Tooltip primitives through a single partial. The `part` prop (skip|live|announcer|tooltip|all) picks which piece to render; the default usage mounts the SkipLink + LiveRegion at the top of the page and exposes the `window.announce` API. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --surface-overlay, --text-primary **Variants:** *SkipLink + LiveRegion (default)* ```ejs <%- include('modules/app/AccessibilityKit') %> <%# Anywhere in the page %>
``` *Tooltip part* ```ejs <%- include('modules/app/AccessibilityKit', { part: 'tooltip', content: 'Helpful hint', placement: 'top', children: `<%- include('modules/ui/Button', { variant: 'outline', size: 'sm', children: 'Hover me' }) %>` }) %> ``` ### AppDrawer (`app-drawer`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppDrawer.ejs` - **Status:** stable (since 2026-05) - **Description:** Modal navigation panel that slides in from the page edge: trigger button, backdrop, dismiss, focus trap and Escape-to-close. Renders grouped items via `navGroups`, an optional search box (filterAppDrawer) and emits an `appdrawer:select` custom event. - **Design tokens:** --border, --border-focus, --primary, --primary-subtle, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Left side with search + groups* ```ejs <%- include('modules/app/AppDrawer', { id: 'main-nav', title: 'Navigation', side: 'left', activeId: 'dashboard', navGroups: [ { label: 'Main', items: [ { id: 'dashboard', label: 'Dashboard', iconClass: 'fa-gauge' }, { id: 'projects', label: 'Projects', iconClass: 'fa-folder', badge: 4 }, { id: 'tasks', label: 'Tasks', iconClass: 'fa-list-check', badge: 12 } ]}, { label: 'Account', items: [ { id: 'settings', label: 'Settings', iconClass: 'fa-gear' }, { id: 'signout', label: 'Sign out', iconClass: 'fa-right-from-bracket' } ]} ] }) %> ``` *Right side, no search* ```ejs <%- include('modules/app/AppDrawer', { id: 'account-drawer', title: 'Account', side: 'right', searchable: false, navItems: [ { id: 'profile', label: 'Profile', iconClass: 'fa-user' }, { id: 'billing', label: 'Billing', iconClass: 'fa-credit-card' }, { id: 'keys', label: 'API keys', iconClass: 'fa-key' }, { id: 'team', label: 'Team', iconClass: 'fa-users' } ] }) %> ``` ### FormField (`form-field`) - **Layer:** app - **Category:** App - **File:** `modules/app/FormField.ejs` - **Status:** stable (since 2026-05) - **Description:** Form field wrapper: label + slot + hint + error. Exposes computed `id`, `aria-describedby` and `aria-invalid` via the `data-form-field` container — the static EJS counterpart of the NextJS react-hook-form binding. - **Design tokens:** --error, --primary, --secondary, --text-primary, --text-secondary **Variants:** *With hint* ```ejs <%- include('modules/app/FormField', { name: 'email', label: 'Email', hint: "We'll never share your email.", children: `` }) %> ``` *Required with error* ```ejs <%- include('modules/app/FormField', { name: 'password', label: 'Password', required: true, error: 'Password must be at least 8 characters.', children: `` }) %> ``` ### NotificationSystem (`notification-system`) - **Layer:** app - **Category:** App - **File:** `modules/app/NotificationSystem.ejs` - **Status:** stable (since 2026-05) - **Description:** Page-level toast container with global `window.notify` / `window.pushNotification` API. 6 positions, 5 variants (success/warning/error/info/loading), auto-dismiss progress bar, hover to pause, manual dismiss. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --info, --info-subtle, --primary, --secondary, --success, --success-fg, --success-subtle, --surface-raised, --text-primary, --text-secondary, --warning, --warning-subtle **Variants:** *Top-right stack* ```ejs <%# Mount once near %> <%- include('modules/app/NotificationSystem', { position: 'top-right' }) %> ``` *Error + loading (bottom-left)* ```ejs <%- include('modules/app/NotificationSystem', { position: 'bottom-left' }) %> ``` ### SectionCard (`section-card`) - **Layer:** app - **Category:** App - **File:** `modules/app/SectionCard.ejs` - **Status:** stable (since 2026-05) - **Description:** Titled content card with rounded-xl + border + bg-surface-raised + p-6. Header is separated by an underline; children slot accepts arbitrary content. - **Design tokens:** --border, --primary, --surface-raised, --text-primary **Variants:** *Basic — title + content* ```ejs <%- include('modules/app/SectionCard', { title: 'Account details', children: `

Name: <%= user.name %>

Email: <%= user.email %>

Role: <%= user.role %>

` }) %> ``` *Form section* ```ejs <%- include('modules/app/SectionCard', { title: 'Notification preferences', children: `
<%- include('modules/ui/Toggle', { label: 'Email updates', checked: prefs.email }) %> <%- include('modules/ui/Toggle', { label: 'Push notifications', checked: prefs.push }) %> <%- include('modules/ui/Toggle', { label: 'Weekly digest', checked: prefs.weekly }) %>
` }) %> ``` ### InlineAlert (`inline-alert`) - **Layer:** app - **Category:** App - **File:** `modules/app/InlineAlert.ejs` - **Status:** stable (since 2026-05) - **Description:** Compact inline alert strip used next to form fields or inside cards. success / error / warning / info variants; icon + single-line message. - **Design tokens:** --error, --error-subtle, --info, --info-subtle, --primary, --success, --success-fg, --success-subtle, --text-primary, --warning, --warning-subtle **Variants:** *All variants* ```ejs <%- include('modules/app/InlineAlert', { variant: 'success', message: 'Settings saved successfully.' }) %> <%- include('modules/app/InlineAlert', { variant: 'error', message: 'Failed to save. Check your input and try again.' }) %> <%- include('modules/app/InlineAlert', { variant: 'warning', message: 'Your subscription expires in 3 days.' }) %> <%- include('modules/app/InlineAlert', { variant: 'info', message: 'A new version is available — refresh to update.' }) %> ``` *Below a form field* ```ejs
<%- include('modules/ui/Input', { label: 'Email address', type: 'email', value: form.email, error: !!errors.email }) %> <% if (errors.email) { %> <%- include('modules/app/InlineAlert', { variant: 'error', message: errors.email }) %> <% } %>
``` ### StepShell (`step-shell`) - **Layer:** app - **Category:** App - **File:** `modules/app/StepShell.ejs` - **Status:** stable (since 2026-05) - **Description:** Wrapper card for a single step in a multi-step flow. Border and number circle change based on active / done / inactive state; the done + onEdit combination shows an Edit button alongside the summary. - **Design tokens:** --border, --primary, --primary-fg, --secondary, --success, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Three states — done / active / inactive* ```ejs <%- include('modules/app/StepShell', { number: 1, title: 'Account details', done: true, onEdit: '/checkout?step=1', summary: '

' + user.email + ' · ' + user.role + ' role

' }) %> <%- include('modules/app/StepShell', { number: 2, title: 'Billing address', active: true, children: addressFormHtml }) %> <%- include('modules/app/StepShell', { number: 3, title: 'Payment method' }) %> ``` *Active step only* ```ejs <%- include('modules/app/StepShell', { number: 2, title: 'Choose a plan', active: true, children: planSelectorHtml }) %> ``` ### AppBreadcrumbs (`app-breadcrumbs`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppBreadcrumbs.ejs` - **Status:** stable (since 2026-05) - **Description:** Page header with breadcrumb trail, title, description, and optional status badge. Collapses to a Breadcrumb + dropdown menu on mobile for deep paths. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Title + description + breadcrumb* ```ejs <%- include('modules/app/AppBreadcrumbs', { title: 'Edit invoice', description: 'Update line items, taxes, and payment terms.', badgeContent: '<%- include("modules/ui/Badge", { variant: "warning", children: "DRAFT" }) %>', items: [ { label: 'Home', href: '/' }, { label: 'Invoices', href: '/invoices' }, { label: 'INV-1042', href: '/invoices/' + invoice.id }, { label: 'Edit' } ] }) %> ``` *Breadcrumb only* ```ejs <%- include('modules/app/AppBreadcrumbs', { items: [ { label: 'Dashboard', href: '/' }, { label: 'Settings', href: '/settings' }, { label: 'Security' } ] }) %> ``` ### AppFooter (`app-footer`) - **Layer:** app - **Category:** App - **File:** `modules/app/AppFooter.ejs` - **Status:** stable (since 2026-05) - **Description:** Two-row application footer with logo, navigation links, system status badge, version, copyright, and social slot. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --primary, --secondary, --success, --success-fg, --success-subtle, --surface-base, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Full — logo + nav + status + social* ```ejs <%- include('modules/app/AppFooter', { logoContent: 'Acme', version: '2.4.1', status: 'operational', copyright: '© 2026 Acme Corp. All rights reserved.', navContent: ` Docs Changelog Support Pricing `, socialContent: ` ` }) %> ``` *Minimal — copyright + status only* ```ejs <%- include('modules/app/AppFooter', { logoContent: 'Acme', status: 'degraded', copyright: '© 2026 Acme Corp.' }) %> ``` ### ThemeSwitcher (`theme-switcher`) - **Layer:** app - **Category:** App - **File:** `modules/app/ThemeSwitcher.ejs` - **Status:** stable (since 2026-05) - **Description:** Tri-state theme selector (light / dark / system). Persists the choice to localStorage and toggles the .dark class on . Mounts safely on the server with a placeholder until hydrated. - **Design tokens:** --text-disabled **Variants:** *Closed trigger — system mode* ```ejs <%- include('modules/app/ThemeSwitcher') %> ``` *Open menu — dark active* ```ejs <%- include('modules/app/ThemeSwitcher', { id: 'header-theme-switcher', className: 'shrink-0' }) %> ``` ### RichTextEditor (`rich-text-editor`) - **Layer:** app - **Category:** App - **File:** `modules/app/RichTextEditor/index.ejs` - **Status:** stable (since 2026-05) - **Description:** Quill 2.x WYSIWYG with token-tinted snow theme. Production features: controlled value (via `kui-rte:set-html` event), imperative API via `window.KuiRte.get(id)`, name+hidden-form-sync, drag-and-drop image upload (with `imageUploadFn` global override), paste sanitization (Word / GDocs cleanup), char + word counter with maxLength, autosave to localStorage, markdown shortcuts, color + highlight, sub/sup, indent/outdent, horizontal rule, tables, emoji picker, @-mentions, /-slash command menu, selection bubble menu, image resize/align overlay, fullscreen mode. Pixel-identical React sibling at modules/app/RichTextEditor/index.tsx. - **Design tokens:** --border, --border-focus, --error, --primary, --secondary, --surface-base, --surface-sunken, --text-primary, --text-secondary **Variants:** *Empty + counter* ```ejs <%- include('modules/app/RichTextEditor', { id: 'article-body', name: 'body', label: 'Article body', showCounter: true, showWordCount: true }) %> ``` *Pre-populated* ```ejs <%- include('modules/app/RichTextEditor', { id: 'release-notes', name: 'releaseNotes', label: 'Release notes', value: '

Release notes

...

' }) %> ``` *Read-only* ```ejs <%- include('modules/app/RichTextEditor', { id: 'archived', name: 'archived', label: 'Archived document', value: savedHtml, readOnly: true }) %> ``` *Max length (200 chars)* ```ejs <%- include('modules/app/RichTextEditor', { id: 'short-summary', name: 'summary', label: 'Short summary', maxLength: 200, showCounter: true }) %> ``` *Mentions + slash commands* ```ejs <%- include('modules/app/RichTextEditor', { id: 'comment', name: 'comment', label: 'Comment', mentions: [ { id: 'u1', label: 'Jane Doe', description: 'Designer' }, { id: 'u2', label: 'John Smith', description: 'Engineer' } ], slashItems: [ { id: 'h1', label: 'Heading 1', command: 'header:1' }, { id: 'list', label: 'Bullet list', command: 'list:bullet' }, { id: 'hr', label: 'Divider', command: 'hr' } ] }) %> ``` *Imperative ref API* ```ejs <%- include('modules/app/RichTextEditor', { id: 'reply', name: 'reply', value: '

Hello

' }) %> ``` *Form integration* ```ejs
<%- include('modules/app/RichTextEditor', { id: 'post', name: 'body', value: savedHtml }) %>
``` *Autosave* ```ejs <%- include('modules/app/RichTextEditor', { id: 'draft', name: 'draft', autosaveKey: 'my-draft' }) %> ``` ### ContextMenu (`context-menu`) - **Layer:** app - **Category:** App - **File:** `modules/app/ContextMenu.ejs` - **Status:** stable (since 2026-05) - **Description:** Right-click context menu. Wraps any element as a trigger. Supports item groups, keyboard shortcuts, separators, danger items, and disabled items. Positions itself via viewport-aware boundary detection, auto-flips when near screen edges. Full keyboard navigation: ↑↓ arrows, Enter, Escape. - **Design tokens:** --border, --error, --error-subtle, --primary, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Text editor — clipboard + format actions* ```ejs <%- include('modules/app/ContextMenu', { children: '
Right-click anywhere in this area
', items: [ { label: 'Cut', icon: '', shortcut: '⌘X' }, { label: 'Copy', icon: '', shortcut: '⌘C' }, { label: 'Paste', icon: '', shortcut: '⌘V' }, { type: 'separator' }, { label: 'Copy link', icon: '', shortcut: '⌘⇧C' }, { type: 'separator' }, { label: 'Rename', icon: '' }, { label: 'Delete', icon: '', shortcut: '⌫', danger: true }, ] }) %> ``` *File manager — groups + shortcut hint* ```ejs <% files.forEach(function(file) { %> <%- include('modules/app/ContextMenu', { children: fileCardHtml(file), items: [ { type: 'group', label: 'Actions' }, { label: 'Open', icon: '' }, { label: 'Download', icon: '', shortcut: '⌘D' }, { label: 'Share', icon: '', shortcut: '⌘⇧S' }, { type: 'separator' }, { type: 'group', label: 'Organise' }, { label: 'Move to…', icon: '' }, { label: 'Add tag', icon: '' }, { type: 'separator' }, { label: 'Delete', icon: '', danger: true }, ] }) %> <% }); %> ``` *Code branch — some items disabled* ```ejs <%- include('modules/app/ContextMenu', { children: branchRowHtml, items: [ { label: 'View diff', icon: '' }, { label: 'Copy branch name', icon: '', shortcut: '⌘C' }, { type: 'separator' }, { label: 'Merge into main', icon: '', disabled: true }, { label: 'Cherry-pick', disabled: true }, { type: 'separator' }, { label: 'Delete branch', icon: '', danger: true }, ] }) %> ``` ### ImageGallery (`image-gallery`) - **Layer:** app - **Category:** App - **File:** `modules/app/ImageGallery/index.ejs` - **Status:** stable (since 2026-05) - **Description:** Responsive image grid with a full-screen lightbox, right-click context menu (open, copy URL, move to first/last, remove), and drag-to-reorder. Supports 2–4 columns, square / video / portrait / auto aspect ratios, optional captions, zoom toggle, thumbnail strip, and full keyboard navigation (← → Escape). **Variants:** *Reorderable — drag + right-click menu* ```ejs <%- include('modules/app/ImageGallery', { images: [ { src: '/photo-1.jpg', alt: 'Mountain', caption: 'Sunrise over the Alps' }, { src: '/photo-2.jpg', alt: 'Ocean', caption: 'Golden hour' }, { src: '/photo-3.jpg', alt: 'Forest', caption: 'Morning mist' }, ], columns: 3, aspect: 'square', gap: 'md', reorderable: true }) %> ``` *3-column grid — lightbox only* ```ejs <%- include('modules/app/ImageGallery', { images: images, columns: 3, aspect: 'square', gap: 'md' }) %> ``` *2-column with captions* ```ejs <%- include('modules/app/ImageGallery', { images: images, columns: 2, aspect: 'video', gap: 'lg', showCaptions: true }) %> ``` *4-column compact* ```ejs <%- include('modules/app/ImageGallery', { images: images, columns: 4, aspect: 'square', gap: 'sm' }) %> ``` ### KanbanBoard (`kanban-board`) - **Layer:** app - **Category:** App - **File:** `modules/app/KanbanBoard/KanbanBoard.ejs` - **Status:** beta (since 2026-05) - **Description:** Trello / Linear-style kanban board. M1 ships HTML5-native drag and drop between columns with an inline drop-position indicator (thin line between cards) and full optimistic-UI rewind via window.KuiKanban.get(id).setMover() — throw / reject from the mover to revert. Future milestones: column reorder + collapse + WIP limits (M2), swimlanes + filters + search (M3), inline edit + bulk select + card detail panel (M4), keyboard nav + ARIA announcements (M5), virtualization + auto-archive + dependencies (M6). Pixel-identical React sibling at modules/app/KanbanBoard/index.tsx. - **Composes:** badge, avatar - **Design tokens:** --surface-base, --surface-raised, --surface-overlay, --text-primary, --text-secondary, --text-disabled, --border, --border-strong, --border-focus, --primary, --primary-subtle, --info-subtle, --warning-subtle, --error-subtle, --success-subtle - **A11y:** WCAG AA · ARIA: region, list, listitem, application · Keys: Tab → Move focus across cards / columns; Drag (mouse) → Pick up a card and drop on any column slot · Each column is role="region" with aria-label; the inner list is role="list"; cards are role="listitem". aria-grabbed flips on the active card; aria-dropeffect="move" is set on all columns while a drag is in flight. Full keyboard parity ships in M5. **Variants:** *Three columns (basic)* ```ejs <%- include('modules/app/KanbanBoard', { id: 'engineering-board', ariaLabel: 'Engineering board', columns: [ { id: 'todo', title: 'To Do' }, { id: 'doing', title: 'In Progress' }, { id: 'done', title: 'Done' } ], cards: [ { id: 'c1', columnId: 'todo', title: 'Audit dark-mode tokens', priority: 'medium' }, { id: 'c2', columnId: 'doing', title: 'Refactor Quill toolbar' } ] }) %> ``` *canMove validation* ```ejs <%- include('modules/app/KanbanBoard', { id: 'board', columns, cards }) %> ``` *Async mover with rollback* ```ejs <%- include('modules/app/KanbanBoard', { id: 'board', columns, cards }) %> ``` ### Calendar (`calendar`) - **Layer:** app - **Category:** App - **File:** `modules/app/Calendar/Calendar.ejs` - **Status:** beta (since 2026-05) - **Description:** Month / week / day / agenda / resource calendar with view switcher, full keyboard nav (PageUp/Down + T + arrow keys for day-step), per-event color and icon, all-day bars + timed pills, TR/EN locales, full interactions (anchored popover with Edit/Delete, drag-move, edge-resize, drag-create), in-house RRULE expansion (FREQ/INTERVAL/COUNT/UNTIL/BYDAY + exceptions, server-side), multi-calendar overlay with per-calendar visibility legend, ResourceView lanes with O(n²) conflict highlighting, agenda list (search + date grouping), composable MiniCalendar sibling (modules/app/MiniCalendar), and WAI-ARIA grid pattern with live-region nav announcements + descriptive cell aria-labels. Pixel-identical React sibling at modules/app/Calendar/index.tsx. - **Design tokens:** --primary, --primary-fg, --success, --success-fg, --warning, --error, --info, --secondary, --surface-base, --surface-raised, --surface-overlay, --border, --border-focus, --text-primary, --text-secondary, --text-disabled - **A11y:** WCAG AA · ARIA: Grid (month), Tablist (view switcher), Region (week/day) · Keys: Page Up → Previous period (month / week / day); Page Down → Next period (month / week / day); T → Jump to today **Variants:** *Month view — Türkçe* ```ejs <%- include('modules/app/Calendar', { id: 'main-cal', view: 'month', defaultDate: new Date(2026, 4, 13), events: events, locale: 'tr' }) %> ``` *Week view — working hours shading* ```ejs <%- include('modules/app/Calendar', { id: 'main-cal', view: 'week', defaultDate: new Date(2026, 4, 13), events: events, locale: 'tr', workingHours: { start: 9, end: 18, days: [1,2,3,4,5] } }) %> ``` *Day view — English* ```ejs <%- include('modules/app/Calendar', { id: 'main-cal', view: 'day', defaultDate: new Date(2026, 4, 13), events: events, locale: 'en', workingHours: { start: 9, end: 18, days: [1,2,3,4,5] } }) %> ``` *Recurring — RRULE expansion* ```ejs <%- include('modules/app/Calendar', { id: 'main-cal', view: 'week', defaultDate: new Date(2026, 4, 13), slotMinutes: 15, workingHours: { start: 9, end: 18, days: [1,2,3,4,5] }, events: [ { id: 'standup', title: 'Daily standup', start: new Date(2026, 4, 11, 9, 30), end: new Date(2026, 4, 11, 9, 45), rrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;COUNT=20', exceptions: [new Date(2026, 4, 13)] }, { id: 'coffee', title: 'Coffee with Ada', start: new Date(2026, 4, 12, 8, 30), end: new Date(2026, 4, 12, 9, 0), rrule: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;COUNT=5' } ] }) %> ``` *Interactive — drag, resize, popover* ```ejs <%- include('modules/app/Calendar', { id: 'interactive-cal', view: 'week', defaultDate: new Date(2026, 4, 13), events: events, slotMinutes: 30 }) %> ``` *Resource view — rooms with conflict highlight* ```ejs <%- include('modules/app/Calendar', { id: 'rooms-cal', view: 'resource', defaultDate: new Date(2026, 4, 13), slotMinutes: 15, workingHours: { start: 9, end: 18, days: [1,2,3,4,5] }, resources: [ { id: 'room-a', name: 'Studio A', color: 'primary' }, { id: 'room-b', name: 'Studio B', color: 'success' }, { id: 'room-c', name: 'Boardroom', color: 'warning' } ], events: events // each carries a resourceId }) %> ``` *Agenda view — date-grouped + search* ```ejs <%- include('modules/app/Calendar', { id: 'agenda-cal', view: 'agenda', defaultDate: new Date(2026, 4, 13), events: events, locale: 'en' }) %> ``` *MiniCalendar sidebar — composes with Calendar via CustomEvent* ```ejs
<%- include('modules/app/MiniCalendar', { id: 'mini', value: new Date(2026, 4, 13), locale: 'en' }) %> <%- include('modules/app/Calendar', { id: 'main', view: 'week', defaultDate: new Date(2026, 4, 13), events: events }) %>
``` *Multi-calendar overlay — toggle visibility* ```ejs <%- include('modules/app/Calendar', { id: 'multi-cal', view: 'week', defaultDate: new Date(2026, 4, 13), calendars: [ { id: 'work', name: 'Work', color: 'primary' }, { id: 'personal', name: 'Personal', color: 'success' }, { id: 'family', name: 'Family', color: 'warning' } ], events: events // each carries a calendarId }) %> ``` ### MaintenancePage (`maintenance-page`) - **Layer:** app - **Category:** App - **File:** `modules/app/MaintenancePage.ejs` - **Status:** stable (since 2026-05) - **Description:** Full-page maintenance screen. Optional ETA countdown badge and external status-page link. Use for planned downtime or unplanned outages. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-base, --surface-overlay, --text-disabled, --text-inverse, --text-primary, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Plain (no ETA)* ```ejs <%- include('modules/app/MaintenancePage', { title: 'System Maintenance', description: "We're performing a short maintenance." }) %> ``` *With ETA + status link* ```ejs <%- include('modules/app/MaintenancePage', { title: 'System Maintenance', description: "We're shipping new features.", eta: new Date(Date.now() + 45 * 60 * 1000), statusUrl: 'https://status.example.com', statusLabel: 'Status Page' }) %> ``` ### ShareDialog (`share-dialog`) - **Layer:** app - **Category:** App - **File:** `modules/app/ShareDialog.ejs` - **Status:** stable (since 2026-05) - **Description:** Share modal: copyable link, email invitation with permission picker, and a list of current invitees with permission/remove controls. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --info, --info-fg, --info-subtle, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --secondary-hover, --surface-base, --surface-raised, --surface-sunken, --text-disabled, --text-primary, --text-secondary, --warning, --warning-fg, --warning-subtle - **A11y:** WCAG AA · ARIA: role="dialog", aria-modal="true", aria-labelledby, aria-describedby **Variants:** *With invitees* ```ejs <%- include('modules/app/ShareDialog', { open: true, shareUrl: 'https://app.example.com/docs/x4y9-zk7', invitees: [ { id: '1', name: 'Alice Brooks', email: 'alice@example.com', permission: 'owner' }, { id: '2', name: 'Marcus Reed', email: 'marcus@example.com', permission: 'editor' }, { id: '3', name: 'Priya Sharma', email: 'priya@example.com', permission: 'viewer' } ] }) %> ``` *Empty / link only* ```ejs <%- include('modules/app/ShareDialog', { open: true, shareUrl: 'https://app.example.com/projects/empty-share', invitees: [] }) %> ``` ### CommentThread (`comment-thread`) - **Layer:** app - **Category:** App - **File:** `modules/app/CommentThread.ejs` - **Status:** stable (since 2026-05) - **Description:** Generic threaded comments with replies, like counts, delete-own actions, and a composer. Domain-agnostic — pass comments + handlers. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --surface-base, --surface-overlay, --text-disabled, --text-primary, --text-secondary - **A11y:** WCAG AA · ARIA: aria-label="Comments", aria-pressed (like), aria-expanded (reply) **Variants:** *With replies* ```ejs <%- include('modules/app/CommentThread', { comments: comments, currentUserId: 'me', replyAction: '/comments/reply', deleteAction: '/comments/delete' }) %> ``` *Empty state* ```ejs <%- include('modules/app/CommentThread', { comments: [], currentUserId: 'me' }) %> ``` ### MentionPicker (`mention-picker`) - **Layer:** app - **Category:** App - **File:** `modules/app/MentionPicker.ejs` - **Status:** stable (since 2026-05) - **Description:** @-trigger autocomplete picker. Headless: takes users + query + position, fires onSelect. Keyboard nav (ArrowUp/Down, Enter/Tab, Escape). - **Design tokens:** --border, --primary, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary - **A11y:** WCAG AA · ARIA: role="listbox", role="option", aria-selected · Keys: ArrowDown / ArrowUp → Move selection; Enter / Tab → Insert highlighted mention; Escape → Cancel picker **Variants:** *Filtered list* ```ejs <%- include('modules/app/MentionPicker', { users: candidates, query: 'al', anchorInputId: 'comment-input' }) %> ``` *Empty results* ```ejs <%- include('modules/app/MentionPicker', { users: candidates, query: 'zzz' }) %> ``` ### OnboardingWizard (`onboarding-wizard`) - **Layer:** app - **Category:** App - **File:** `modules/app/OnboardingWizard.ejs` - **Status:** stable (since 2026-05) - **Description:** Multi-step onboarding flow with dots/bar progress, optional skip, and page or modal presentation. Server-rendered with ?step=N querystring. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --primary-hover, --secondary, --surface-overlay, --surface-raised, --surface-sunken, --text-disabled, --text-primary, --text-secondary - **A11y:** WCAG AA · ARIA: role="progressbar" **Variants:** *Dots indicator (page mode)* ```ejs <%- include('modules/app/OnboardingWizard', { steps: onboardingSteps, current: 0, indicator: 'dots', allowSkip: true }) %> ``` *Progress bar indicator* ```ejs <%- include('modules/app/OnboardingWizard', { steps: onboardingSteps, current: 1, indicator: 'bar', allowSkip: false }) %> ``` ## Components — domain / App ### UserMenu (`user-menu`) - **Layer:** domain - **Category:** App - **File:** `modules/domain/common/user/UserMenu.ejs` - **Status:** stable (since 2025-03) - **Description:** User dropdown opened by a trigger showing avatar, name and role. Accepts a SafeUser prop; the dropdown header shows the name and email. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-overlay, --text-disabled, --text-primary, --text-secondary **Variants:** *Varsayılan (isim + e-posta + rol)* ```ejs <%- include('modules/domain/common/user/UserMenu', { name: user.name, email: user.email, role: user.role, profileHref: '/profile', settingsHref: '/settings', signOutHref: '/auth/logout' }) %> ``` *POST sign-out (form submit)* ```ejs <%- include('modules/domain/common/user/UserMenu', { name: user.name, email: user.email, role: user.role, signOutHref: '/auth/logout', signOutMethod: 'post' }) %> ``` ## Components — domain / Domain ### LoginForm (`login-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/LoginForm.ejs` - **Status:** stable (since 2025-03) - **Description:** Email + password sign-in form with inline validation, "Remember me" checkbox, and server-error banner. - **Design tokens:** --border, --border-focus, --error, --error-fg, --error-subtle, --secondary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/domain/common/auth/LoginForm', { action: '/auth/login', method: 'post' }) %> ``` *With error* ```ejs <%- include('modules/domain/common/auth/LoginForm', { action: '/auth/login', error: 'Invalid email or password. Please try again.' }) %> ``` ### RegisterForm (`register-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/RegisterForm.ejs` - **Status:** stable (since 2025-03) - **Description:** Registration form with email, password, and confirm-password fields. Real-time password match validation and server-error support. - **Design tokens:** --error, --error-fg, --error-subtle **Variants:** *Default* ```ejs <%- include('modules/domain/common/auth/RegisterForm', { action: '/auth/register', method: 'post' }) %> ``` *With field errors* ```ejs <%- include('modules/domain/common/auth/RegisterForm', { action: '/auth/register', errors: { email: 'Enter a valid email address.', password: 'Password must be at least 8 characters.' } }) %> ``` ### OAuthButtons (`oauth-buttons`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/OAuthButtons.ejs` - **Status:** stable (since 2025-03) - **Description:** Social sign-in buttons for Google, GitHub, Discord, and Microsoft. Accepts a providers prop to show only a subset. - **Design tokens:** --primary, --text-primary **Variants:** *All providers* ```ejs <%- include('modules/domain/common/auth/OAuthButtons', { action: '/auth/oauth', providers: ['google', 'github', 'discord', 'microsoft'] }) %> ``` *Google + GitHub only* ```ejs <%- include('modules/domain/common/auth/OAuthButtons', { action: '/auth/oauth', providers: ['google', 'github'] }) %> ``` ### UserAvatar (`user-avatar`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserAvatar.ejs` - **Status:** stable (since 2025-03) - **Description:** Avatar that consumes the SafeUser type. Falls back to initials when no profile picture is set; supports online/away/busy/offline status dots. **Variants:** *Initials (no photo)* ```ejs <%- include('modules/domain/common/user/UserAvatar', { name: 'Alice Johnson', size: 'md' }) %> ``` *Online / Away / Busy / Offline* ```ejs <%- include('modules/domain/common/user/UserAvatar', { name: user.name, src: user.profilePicture, status: 'online' }) %> ``` ### UserRoleBadge (`user-role-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserRoleBadge.ejs` - **Status:** stable (since 2025-03) - **Description:** Color-coded badge for ADMIN / AUTHOR / USER roles. ADMIN renders as error (red), AUTHOR as primary (blue), USER as neutral. **Variants:** *All roles* ```ejs <%- include('modules/domain/common/user/UserRoleBadge', { role: 'ADMIN' }) %> <%- include('modules/domain/common/user/UserRoleBadge', { role: 'AUTHOR' }) %> <%- include('modules/domain/common/user/UserRoleBadge', { role: 'USER' }) %> ``` ### UserStatusBadge (`user-status-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserStatusBadge.ejs` - **Status:** stable (since 2025-03) - **Description:** Color-coded badge for ACTIVE / INACTIVE / BANNED user statuses. Optional dot prop adds a leading status indicator. **Variants:** *All statuses* ```ejs <%- include('modules/domain/common/user/UserStatusBadge', { status: 'ACTIVE' }) %> <%- include('modules/domain/common/user/UserStatusBadge', { status: 'INACTIVE' }) %> <%- include('modules/domain/common/user/UserStatusBadge', { status: 'BANNED' }) %> ``` *With dot* ```ejs <%- include('modules/domain/common/user/UserStatusBadge', { status: 'ACTIVE', dot: true }) %> ``` ### UserMenu (`user-menu`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserMenu.ejs` - **Status:** stable (since 2025-03) - **Description:** Avatar + name + role trigger. Dropdown with Profile, Settings, and Sign out items. Closes on outside click. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-overlay, --text-disabled, --text-primary, --text-secondary **Variants:** *Closed (default)* ```ejs <%- include('modules/domain/common/user/UserMenu', { name: user.name, email: user.email, role: user.role, src: user.profilePicture, profileHref: '/account/profile', settingsHref: '/account/settings', signOutHref: '/auth/logout', signOutMethod: 'post' }) %> ``` *Dropdown open (static preview)* ```ejs <%- include('modules/domain/common/user/UserMenu', { name: 'Alice Johnson', email: 'alice@example.com', align: 'right' }) %> ``` ### PriceDisplay (`price-display`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/money/PriceDisplay.ejs` - **Status:** stable (since 2025-03) - **Description:** Currency formatter using Intl.NumberFormat. Supports any ISO 4217 code and locale. Strikethrough prop renders an original/crossed-out price. - **Design tokens:** --secondary, --text-secondary **Variants:** *Sizes* ```ejs <%- include('modules/domain/common/money/PriceDisplay', { amount: 1299.99, currency: 'TRY', size: 'lg' }) %> ``` *Multi-currency + strikethrough* ```ejs <%- include('modules/domain/common/money/PriceDisplay', { amount: 2499, currency: 'TRY', size: 'lg' }) %> <%- include('modules/domain/common/money/PriceDisplay', { amount: 1799, currency: 'TRY', size: 'lg', strikethrough: true }) %> ``` ### PaymentStatusBadge (`payment-status-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/PaymentStatusBadge.ejs` - **Status:** stable (since 2025-03) - **Description:** Color-coded badge for all PaymentStatus values: PENDING / AUTHORIZED / PAID / FAILED / CANCELLED / REFUNDED. **Variants:** *All statuses* ```ejs <%- include('modules/domain/common/payment/PaymentStatusBadge', { status: 'PAID' }) %> <%- include('modules/domain/common/payment/PaymentStatusBadge', { status: 'PENDING' }) %> ``` *With dot, large* ```ejs <%- include('modules/domain/common/payment/PaymentStatusBadge', { status: 'PENDING', dot: true, size: 'lg' }) %> <%- include('modules/domain/common/payment/PaymentStatusBadge', { status: 'PAID', dot: true, size: 'lg' }) %> <%- include('modules/domain/common/payment/PaymentStatusBadge', { status: 'FAILED', dot: true, size: 'lg' }) %> ``` ### AddressForm (`address-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/address/AddressForm.ejs` - **Status:** stable (since 2025-03) - **Description:** Full address form with full name, phone, address lines, city, state/district, postal code, and country. Pre-fillable via the initial prop. - **Design tokens:** --border, --error, --error-subtle, --surface-base **Variants:** *Empty* ```ejs <%- include('modules/domain/common/address/AddressForm', { action: '/addresses/new' }) %> ``` *Pre-filled* ```ejs <%- include('modules/domain/common/address/AddressForm', { action: '/addresses/1/edit', initial: existingAddress, submitLabel: 'Update', cancelHref: '/addresses' }) %> ``` ### AddressCard (`address-card`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/address/AddressCard.ejs` - **Status:** stable (since 2025-03) - **Description:** Read-only address display card with full name, phone, address lines, city, state, postal code, and country. Supports selected state and optional Edit/Delete actions. - **Design tokens:** --border, --error, --primary, --primary-hover, --secondary, --surface-base, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/domain/common/address/AddressCard', { address: savedAddress, editHref: '/addresses/1/edit', deleteAction: '/addresses/1/delete' }) %> ``` *Selectable list* ```ejs <%- include('modules/domain/common/address/AddressCard', { address: address, selected: selectedIdx === i }) %> ``` ### OrderTotalsCard (`order-totals-card`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/money/OrderTotalsCard.ejs` - **Status:** stable (since 2025-03) - **Description:** Order summary card showing subtotal, discount, tax, service fee, shipping, and an emphasized total. Zero-value lines are hidden automatically. - **Design tokens:** --border, --primary, --secondary, --success, --success-fg, --surface-raised, --text-primary, --text-secondary **Variants:** *No extras* ```ejs <%- include('modules/domain/common/money/OrderTotalsCard', { totals: { subtotal: 89.99, total: 89.99, currency: 'USD' }, locale: 'en-US' }) %> ``` *With discount, tax & shipping* ```ejs <%- include('modules/domain/common/money/OrderTotalsCard', { totals: { subtotal: 149.99, discountTotal: 20, taxTotal: 11.99, shippingTotal: 9.99, total: 153.96, currency: 'USD' }, locale: 'en-US' }) %> ``` ### PublishStatusBadge (`publish-status-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/status/PublishStatusBadge.ejs` - **Status:** stable (since 2025-03) - **Description:** Badge for DRAFT / PUBLISHED / ARCHIVED content states with contextual Font Awesome icons. Icon can be hidden via showIcon={false}. **Variants:** *All statuses* ```ejs <%- include('modules/domain/common/status/PublishStatusBadge', { status: 'DRAFT' }) %> <%- include('modules/domain/common/status/PublishStatusBadge', { status: 'PUBLISHED' }) %> <%- include('modules/domain/common/status/PublishStatusBadge', { status: 'ARCHIVED' }) %> ``` *Without icon, small* ```ejs <%- include('modules/domain/common/status/PublishStatusBadge', { status: 'DRAFT', showIcon: false, size: 'sm' }) %> ``` ### VisibilityBadge (`visibility-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/status/VisibilityBadge.ejs` - **Status:** stable (since 2025-03) - **Description:** Badge for PUBLIC / PRIVATE / UNLISTED visibility states with eye/lock icons. PUBLIC is green, PRIVATE is red, UNLISTED is neutral. **Variants:** *All states* ```ejs <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'PUBLIC' }) %> <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'PRIVATE' }) %> <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'UNLISTED' }) %> ``` *Sizes* ```ejs <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'PUBLIC', size: 'sm' }) %> <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'PUBLIC', size: 'md' }) %> <%- include('modules/domain/common/status/VisibilityBadge', { visibility: 'PUBLIC', size: 'lg' }) %> ``` ### LanguageSwitcher (`language-switcher`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/i18n/LanguageSwitcher.ejs` - **Status:** stable (since 2025-04) - **Description:** Language selector using a native select. Defaults to en/tr/de/fr/ar; with autoSubmit the form is submitted automatically on change. - **Design tokens:** --text-disabled **Variants:** *Default (English)* ```ejs <%- include('modules/domain/common/i18n/LanguageSwitcher', { value: currentLang, name: 'language' }) %> ``` *Turkish selected* ```ejs <%- include('modules/domain/common/i18n/LanguageSwitcher', { value: 'tr' }) %> ``` *Auto-submit on change* ```ejs
<%- include('modules/domain/common/i18n/LanguageSwitcher', { value: currentLang, autoSubmit: true }) %>
``` ### ChangePasswordForm (`change-password-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/ChangePasswordForm.ejs` - **Status:** stable (since 2025-04) - **Description:** Current password + new password + confirm fields with match validation and server-error banner. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --surface-base **Variants:** *Default* ```ejs <%- include('modules/domain/common/auth/ChangePasswordForm', { action: '/account/change-password' }) %> ``` *With mismatch error* ```ejs <%- include('modules/domain/common/auth/ChangePasswordForm', { action: '/account/change-password', errors: { confirmPassword: "Passwords don't match." } }) %> ``` ### UserProfileCard (`user-profile-card`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserProfileCard.ejs` - **Status:** stable (since 2025-04) - **Description:** Profile card with cover banner, avatar, display name, username, bio, role and status badges, and an optional actions slot. - **Design tokens:** --border, --primary, --primary-subtle, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Full profile* ```ejs <%- include('modules/domain/common/user/UserProfileCard', { name: user.name, email: user.email, role: user.role, status: user.status, username: user.username, biography: user.biography, src: user.profilePicture }) %> ``` *No profile data* ```ejs <%- include('modules/domain/common/user/UserProfileCard', { email: user.email, role: user.role, status: user.status }) %> ``` ### UserProfileForm (`user-profile-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserProfileForm.ejs` - **Status:** stable (since 2025-04) - **Description:** Controlled form for editing display name, username, bio, and profile picture URL. Username validation: 3–32 chars, lowercase alphanumeric + underscore. - **Design tokens:** --border, --border-focus, --error, --error-fg, --error-subtle, --primary, --surface-overlay, --text-primary **Variants:** *Default* ```ejs <%- include('modules/domain/common/user/UserProfileForm', { action: '/account/profile', initial: { name: user.name, username: user.username, biography: user.biography, profilePicture: user.profilePicture } }) %> ``` *Pre-filled* ```ejs <%- include('modules/domain/common/user/UserProfileForm', { action: '/account/profile', cancelHref: '/account', initial: { name: 'Alice Johnson', username: 'alicejohnson', ... } }) %> ``` ### UserPreferencesForm (`user-preferences-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/user/UserPreferencesForm.ejs` - **Status:** stable (since 2025-04) - **Description:** Preferences form with theme and language selects plus email/push notification and newsletter toggles. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --primary, --text-primary **Variants:** *Default* ```ejs <%- include('modules/domain/common/user/UserPreferencesForm', { action: '/account/preferences', initial: { theme: 'SYSTEM', language: 'en', emailNotifications: true, pushNotifications: true, newsletter: false } }) %> ``` ### CouponInput (`coupon-input`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/discount/CouponInput.ejs` - **Status:** stable (since 2025-04) - **Description:** Coupon code input with apply/remove flow. Calls onApply which returns success/error; shows applied state once a valid code is accepted. - **Design tokens:** --error, --success, --success-fg, --success-subtle **Variants:** *Default* ```ejs <%- include('modules/domain/common/discount/CouponInput', { action: '/cart/coupon/apply' }) %> ``` *Applied state* ```ejs <%- include('modules/domain/common/discount/CouponInput', { appliedCode: 'SAVE20', removeAction: '/cart/coupon/remove' }) %> ``` ### DiscountBadge (`discount-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/discount/DiscountBadge.ejs` - **Status:** stable (since 2025-04) - **Description:** Formats and displays a discount: percentage (e.g. "20% off"), fixed amount with currency, or free shipping. - **Design tokens:** --error, --error-subtle **Variants:** *All types* ```ejs <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'PERCENTAGE', discountValue: 20 }) %> <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'FIXED', discountValue: 50, currency: 'TRY' }) %> <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'FREE_SHIPPING', discountValue: 0 }) %> ``` *Sizes* ```ejs <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'PERCENTAGE', discountValue: 10, size: 'sm' }) %> <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'PERCENTAGE', discountValue: 10, size: 'md' }) %> <%- include('modules/domain/common/discount/DiscountBadge', { discountType: 'PERCENTAGE', discountValue: 10, size: 'lg' }) %> ``` ### PaymentMethodSelector (`payment-method-selector`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/PaymentMethodSelector.ejs` - **Status:** stable (since 2025-04) - **Description:** Radio-group style card selector for payment methods. Shows icon, label, and description. Default set: credit card, debit card, bank transfer, wallet. **Variants:** *Default (4 methods)* ```ejs <%- include('modules/domain/common/payment/PaymentMethodSelector', { name: 'paymentMethod', value: selectedMethod }) %> ``` ### PaymentSummaryCard (`payment-summary-card`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/PaymentSummaryCard.ejs` - **Status:** stable (since 2025-04) - **Description:** Read-only payment summary card: amount, method, provider, provider reference, and status badge. - **Design tokens:** --border, --primary, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Paid via Stripe* ```ejs <%- include('modules/domain/common/payment/PaymentSummaryCard', { payment: { provider: 'Stripe', method: 'CREDIT_CARD', status: 'PAID', amount: 153.96, currency: 'USD', providerPaymentId: 'pi_3Nf9xZ' } }) %> ``` *Pending bank transfer* ```ejs <%- include('modules/domain/common/payment/PaymentSummaryCard', { payment: { provider: 'Iyzico', method: 'BANK_TRANSFER', status: 'PENDING', amount: 2499, currency: 'TRY' } }) %> ``` ### ForgotPasswordForm (`forgot-password-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/ForgotPasswordForm.ejs` - **Status:** stable (since 2025-04) - **Description:** Email input that triggers a password reset link. Shows an inline success state after submission instead of a redirect. - **Design tokens:** --error, --error-fg, --error-subtle, --success, --success-fg, --success-subtle **Variants:** *Default* ```ejs <%- include('modules/domain/common/auth/ForgotPasswordForm', { action: '/auth/forgot-password' }) %> ``` *Sent state* ```ejs <%- include('modules/domain/common/auth/ForgotPasswordForm', { sent: true, email: 'user@example.com' }) %> ``` ### SessionExpiredBanner (`session-expired-banner`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/auth/SessionExpiredBanner.ejs` - **Status:** stable (since 2025-04) - **Description:** Warning banner shown when the user session has expired. Includes a "Sign in again" action button. - **Design tokens:** --primary, --secondary, --text-primary, --text-secondary, --warning, --warning-subtle **Variants:** *Default* ```ejs <%- include('modules/domain/common/auth/SessionExpiredBanner', { loginUrl: '/auth/login' }) %> ``` *Custom message* ```ejs <%- include('modules/domain/common/auth/SessionExpiredBanner', { loginUrl: '/auth/login', message: 'You have been inactive for 30 minutes. Reconnect to continue your work.' }) %> ``` ### SeoForm (`seo-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/seo/SeoForm.ejs` - **Status:** stable (since 2025-04) - **Description:** SEO metadata form: title (60 char limit), meta description (160 char limit), and keyword tag input with character counters. - **Design tokens:** --error, --error-fg, --error-subtle **Variants:** *Empty* ```ejs <%- include('modules/domain/common/seo/SeoForm', { action: '/content/1/seo' }) %> ``` *Pre-filled* ```ejs <%- include('modules/domain/common/seo/SeoForm', { action: '/content/1/seo', cancelHref: '/content/1', initial: { seoTitle: 'Best Running Shoes 2025', seoDescription: 'Discover the top-rated running shoes…', keywords: ['running', 'shoes', 'sports'] } }) %> ``` ### SeoPreview (`seo-preview`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/seo/SeoPreview.ejs` - **Status:** stable (since 2025-04) - **Description:** Google search result preview card. Shows title, URL, and description with character count indicators. Empty fields render placeholder text. - **Design tokens:** --border, --error, --primary, --secondary, --success, --success-fg, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Filled* ```ejs <%- include('modules/domain/common/seo/SeoPreview', { seo: { seoTitle: 'My Page Title', seoDescription: 'A clear meta description.', keywords: ['next', 'react'] }, url: 'https://example.com/page' }) %> ``` *Empty (placeholders)* ```ejs <%- include('modules/domain/common/seo/SeoPreview', { seo: {}, url: 'https://example.com/page' }) %> ``` ### AddressSelector (`address-selector`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/address/AddressSelector.ejs` - **Status:** beta (since 2025-04) - **Description:** Selectable list of saved addresses built on AddressCard. Supports add, edit, and delete callbacks. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-overlay, --text-primary, --text-secondary **Variants:** *Multiple addresses* ```ejs <%- include('modules/domain/common/address/AddressSelector', { addresses: savedAddresses, selectedIndex: 0, addHref: '/addresses/new' }) %> ``` *Empty state* ```ejs <%- include('modules/domain/common/address/AddressSelector', { addresses: [], addHref: '/addresses/new' }) %> ``` ### LocationPicker (`location-picker`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/location/LocationPicker.ejs` - **Status:** beta (since 2025-04) - **Description:** Location form with country selector (countries-list), city, state, postal code, and optional lat/lng. 2-column grid layout. **Variants:** *Empty* ```ejs <%- include('modules/domain/common/location/LocationPicker', { action: '/locations/update', countries: countryList }) %> ``` *Pre-filled* ```ejs <%- include('modules/domain/common/location/LocationPicker', { action: '/locations/update', cancelHref: '/settings', countries: countryList, initial: { city: 'Istanbul', countryCode: 'TR', postalCode: '34000', latitude: 41.0082, longitude: 28.9784 } }) %> ``` ### GeoPointDisplay (`geo-point-display`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/location/GeoPointDisplay.ejs` - **Status:** beta (since 2025-04) - **Description:** Displays latitude/longitude coordinates with a Google Maps link. Configurable precision and optional label. - **Design tokens:** --border, --border-focus, --primary, --primary-hover, --secondary, --text-disabled, --text-primary, --text-secondary **Variants:** *With label* ```ejs <%- include('modules/domain/common/location/GeoPointDisplay', { point: { latitude: 41.0082, longitude: 28.9784 }, label: 'Istanbul' }) %> ``` *Coordinates only* ```ejs <%- include('modules/domain/common/location/GeoPointDisplay', { point: { latitude: 51.5074, longitude: -0.1278 }, showMapLink: false, precision: 4 }) %> ``` ### ProcessingStatusIndicator (`processing-status-indicator`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/status/ProcessingStatusIndicator.ejs` - **Status:** stable (since 2025-04) - **Description:** Animated status indicator for UPLOADING / PROCESSING / READY / FAILED states. Optional progress bar with percentage. - **Design tokens:** --error, --info, --primary, --secondary, --success, --surface-sunken, --text-primary, --text-secondary, --warning **Variants:** *All states* ```ejs <%- include('modules/domain/common/status/ProcessingStatusIndicator', { status: 'UPLOADING', progress: 30 }) %> <%- include('modules/domain/common/status/ProcessingStatusIndicator', { status: 'PROCESSING', progress: 65 }) %> <%- include('modules/domain/common/status/ProcessingStatusIndicator', { status: 'READY', progress: 100 }) %> <%- include('modules/domain/common/status/ProcessingStatusIndicator', { status: 'FAILED' }) %> ``` *Custom label + sizes* ```ejs <%- include('modules/domain/common/status/ProcessingStatusIndicator', { status: 'PROCESSING', label: 'Encoding video…', progress: 45, size: 'lg' }) %> ``` ### CurrencySelector (`currency-selector`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/money/CurrencySelector.ejs` - **Status:** stable (since 2025-04) - **Description:** Currency dropdown built from countries-list. Deduped, alphabetically sorted ISO 4217 currency codes. - **Design tokens:** --border, --border-focus, --primary, --primary-subtle, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/domain/common/money/CurrencySelector', { value: currentCurrency, name: 'currency', currencies: availableCurrencies }) %> ``` *No label* ```ejs <%- include('modules/domain/common/money/CurrencySelector', { value: 'USD', label: '', name: 'currency' }) %> ``` ### CountrySelector (`country-selector`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/location/CountrySelector.ejs` - **Status:** stable (since 2026-05) - **Description:** Country dropdown built from countries-list. Shows flag + full name + ISO2 code. Supports search by name or code, error/hint states. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-subtle, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/domain/common/location/CountrySelector', { name: 'countryCode', value: 'TR', countries: countryList }) %> ``` *With hint & error* ```ejs <%- include('modules/domain/common/location/CountrySelector', { name: 'countryCode', countries: countryList, hint: 'Used for shipping address.' }) %> <%- include('modules/domain/common/location/CountrySelector', { name: 'countryCode', countries: countryList, error: 'Please select a country.', required: true }) %> ``` *No label* ```ejs <%- include('modules/domain/common/location/CountrySelector', { name: 'countryCode', value: 'US', countries: countryList, label: '' }) %> ``` ### DirectionProvider (`direction-provider`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/i18n/DirectionProvider.ejs` - **Status:** stable (since 2025-04) - **Description:** Wrapper div that sets dir="rtl"/"ltr" based on the language code. RTL languages: ar, he, fa, ur, yi, ku, ps, sd. **Variants:** *RTL (Arabic)* ```ejs <%- include('modules/domain/common/i18n/DirectionProvider', { lang: 'ar' }) %> ``` *LTR (English)* ```ejs <%- include('modules/domain/common/i18n/DirectionProvider', { lang: 'en' }) %> ``` ### CreditCardVisual (`credit-card-visual`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/CreditCardVisual.ejs` - **Status:** beta (since 2025-05) - **Description:** Animated 3-D flip credit card. Front shows number, name, expiry; back shows CVV strip. Supports VISA, Mastercard, AMEX, Discover. **Variants:** *Brands* ```ejs <%- include('modules/domain/common/payment/CreditCardVisual', { brand: 'VISA', cardNumber: '4111111111111111', cardholderName: 'JANE DOE', expiryMonth: '08', expiryYear: '28' }) %> ``` *Flipped (CVV)* ```ejs <%- include('modules/domain/common/payment/CreditCardVisual', { brand: 'AMEX', cardNumber: '378282246310005', cardholderName: 'JANE DOE', expiryMonth: '03', expiryYear: '26', cvv: '1234', flipped: true }) %> ``` ### CreditCardForm (`credit-card-form`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/CreditCardForm.ejs` - **Status:** beta (since 2025-05) - **Description:** Full credit card entry form with live card visual preview. Auto-detects brand, formats number, flips card on CVV focus, validates expiry. - **Design tokens:** --error, --error-fg, --error-subtle **Variants:** *Default* ```ejs <%- include('modules/domain/common/payment/CreditCardForm', { action: '/payment/cards', method: 'post' }) %> ``` *Server error* ```ejs <%- include('modules/domain/common/payment/CreditCardForm', { action: '/payment/cards', error: 'Card declined. Please try a different card.' }) %> ``` ### SavedCardSelector (`saved-card-selector`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/SavedCardSelector.ejs` - **Status:** beta (since 2025-05) - **Description:** Radio-group list of saved payment cards. Shows brand badge, masked number, expiry, and default indicator. Supports remove and add-new callbacks. - **Design tokens:** --border, --border-focus, --border-strong, --error, --primary, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Multiple cards* ```ejs <%- include('modules/domain/common/payment/SavedCardSelector', { cards: savedCards, selectedCardId: selectedCard, addHref: '/payment/cards/new', removeAction: '/payment/cards/{id}/remove' }) %> ``` *Empty state* ```ejs <%- include('modules/domain/common/payment/SavedCardSelector', { cards: [], addHref: '/payment/cards/new' }) %> ``` ### NotFoundPage (`not-found-page`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/NotFoundPage.ejs` - **Status:** stable (since 2025-05) - **Description:** Full-page 404 screen with a gradient "404" heading, icon slot, title, description, and home/back action buttons. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --surface-base, --surface-overlay, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/domain/common/NotFoundPage') %> ``` *Custom title & description* ```ejs <%- include('modules/domain/common/NotFoundPage', { title: 'Nothing here yet', description: 'This section is under construction. Check back soon.', homeLabel: 'Return home', backLabel: 'Previous page' }) %> ``` ### ChatBox (`chat-box`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/chat/ChatBox.ejs` - **Status:** beta (since 2026-05) - **Description:** Floating chat widget that anchors to the bottom-right of the screen. Includes a FAB toggle, collapsible panel, scrollable message list with typing indicator, and an auto-growing textarea input. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-active, --primary-fg, --primary-hover, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *With initial messages* ```ejs <%- include('modules/domain/common/chat/ChatBox', { open: true, title: 'Support Chat', subtitle: 'We typically reply in a few minutes', messages: [ { id: 'm1', role: 'agent', text: 'Hi there! How can I help you today?' }, { id: 'm2', role: 'user', text: 'I have a question about my order.' } ] }) %> ``` *Empty / initial state* ```ejs <%- include('modules/domain/common/chat/ChatBox', { open: true, title: 'Sales Chat', subtitle: 'Ask us anything', messages: [] }) %> ``` ### Charts (`charts`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/charts/Charts.ejs` - **Status:** beta (since 2025-05) - **Description:** Chart.js chart components wrapped in Cards: Bar, Line, Doughnut, Radar, and Polar Area. - **Design tokens:** --border, --primary, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Bar — Revenue vs Expenses* ```ejs <%- include('modules/domain/common/charts/Charts', { chartId: 'revenue-bar', type: 'bar', title: 'Revenue vs Expenses', subtitle: 'Monthly comparison (USD)', data: barData, options: { scales: { y: { beginAtZero: true } } } }) %> ``` *Line — User Activity* ```ejs <%- include('modules/domain/common/charts/Charts', { chartId: 'user-activity-line', type: 'line', title: 'User Activity', subtitle: 'Daily active users vs new signups', data: lineData }) %> ``` *Doughnut — Sales by Category* ```ejs <%- include('modules/domain/common/charts/Charts', { chartId: 'sales-doughnut', type: 'doughnut', title: 'Sales by Category', data: doughnutData, options: { cutout: '65%', plugins: { legend: { position: 'bottom' } } } }) %> ``` *Radar — Product Comparison* ```ejs <%- include('modules/domain/common/charts/Charts', { chartId: 'product-radar', type: 'radar', title: 'Product Comparison', data: radarData, options: { scales: { r: { beginAtZero: true, max: 100 } } } }) %> ``` *Polar Area — Regional Sales* ```ejs <%- include('modules/domain/common/charts/Charts', { chartId: 'regional-polar', type: 'polarArea', title: 'Regional Sales', data: polarData }) %> ``` ### NotificationFilterTabs (`notification-filter-tabs`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/notification/NotificationFilterTabs.ejs` - **Status:** stable (since 2026-05) - **Description:** Pill-style filter tabs for a notification list. Includes count badges, active/inactive colors, and is accessible via role="tab" / aria-selected. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --surface-base, --surface-overlay, --surface-sunken, --text-primary, --text-secondary **Variants:** *Default (with counts)* ```ejs <%- include('modules/domain/common/notification/NotificationFilterTabs', { tabs: [ { id: 'all', label: 'All', count: 24 }, { id: 'unread', label: 'Unread', count: 5 }, { id: 'mentions', label: 'Mentions', count: 2 }, { id: 'system', label: 'System', count: 17 }, ], activeId: 'all' }) %> ``` *Active = Unread, no counts* ```ejs <%- include('modules/domain/common/notification/NotificationFilterTabs', { tabs: [ { id: 'all', label: 'All' }, { id: 'unread', label: 'Unread' }, { id: 'archived', label: 'Archived' }, ], activeId: 'unread' }) %> ``` ### NotificationListItem (`notification-list-item`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/notification/NotificationListItem.ejs` - **Status:** stable (since 2026-05) - **Description:** Single notification row. Includes per-kind icon tones (order/message/system/alert/success/social), read/unread emphasis, relative time, and an optional "Mark read" button. - **Design tokens:** --border, --border-focus, --info, --info-subtle, --primary, --primary-subtle, --secondary, --success, --success-subtle, --surface-overlay, --text-primary, --text-secondary, --warning, --warning-subtle **Variants:** *Mixed kinds (read + unread)* ```ejs <%- include('modules/domain/common/notification/NotificationListItem', { kind: 'order', title: 'Order #1042 has been shipped', body: 'Your package is on the way and should arrive by Friday.', createdAt: new Date(Date.now() - 5 * 60 * 1000), read: false, href: '/orders/1042' }) %> ``` *Single unread system notification* ```ejs <%- include('modules/domain/common/notification/NotificationListItem', { kind: 'system', title: 'Scheduled maintenance tonight at 02:00 UTC', body: 'The dashboard will be briefly unavailable for ~10 minutes.', createdAt: new Date(), read: false, onMarkRead: 'function(){ /* mark read */ }' }) %> ``` ### NotificationMenu (`notification-menu`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/notification/NotificationMenu.ejs` - **Status:** stable (since 2026-05) - **Description:** Bell icon button with unread count badge. Opens a dropdown panel showing notification items grouped by read/unread state with variant color dots, timestamps, and mark-all-read / view-all actions. - **Design tokens:** --border, --border-focus, --error, --info, --primary, --primary-fg, --primary-subtle, --secondary, --success, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary, --warning **Variants:** *With unread notifications* ```ejs <%- include('modules/domain/common/notification/NotificationMenu', { items: notifications, align: 'right', onMarkAllRead: 'markAllRead', viewAllHref: '/notifications' }) %> ``` *Empty state* ```ejs <%- include('modules/domain/common/notification/NotificationMenu', { items: [], align: 'right' }) %> ``` ### SubscriptionPlanCard (`subscription-plan-card`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/subscription/SubscriptionPlanCard.ejs` - **Status:** stable (since 2026-05) - **Description:** Subscription plan card displaying name, price with currency formatting, billing interval, and feature list with checkmarks. Highlights the popular and current plans. Accepts onSelect callback for plan switching. - **Design tokens:** --border, --border-focus, --primary, --primary-active, --primary-fg, --primary-hover, --secondary, --success, --surface-raised, --text-primary, --text-secondary **Variants:** *Plan grid* ```ejs
<%- include('modules/domain/common/subscription/SubscriptionPlanCard', { plan: freePlan, action: '/billing/subscribe' }) %> <%- include('modules/domain/common/subscription/SubscriptionPlanCard', { plan: proPlan, isCurrent: true }) %> <%- include('modules/domain/common/subscription/SubscriptionPlanCard', { plan: enterprisePlan, action: '/billing/subscribe' }) %>
``` *Single card states* ```ejs <%- include('modules/domain/common/subscription/SubscriptionPlanCard', { plan: plan, isCurrent: true }) %> <%- include('modules/domain/common/subscription/SubscriptionPlanCard', { plan: Object.assign({}, plan, { isPopular: true }), action: '/billing/subscribe' }) %> ``` ### CheckoutSuccessState (`checkout-success-state`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/payment/CheckoutSuccessState.ejs` - **Status:** stable (since 2026-05) - **Description:** Success screen shown after checkout completion; includes a confirmation icon, payment summary, and optional delivery address. - **Design tokens:** --border, --border-focus, --primary, --secondary, --success, --success-subtle, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Başarı ekranı* ```ejs <%- include('modules/domain/common/payment/CheckoutSuccessState', { payment: { paymentId: 'pay_demo_001', provider: 'Stripe', providerPaymentId: 'pi_3NxYz2EwLHMpEt9Q1', method: 'CREDIT_CARD', status: 'PAID', amount: 1299.90, currency: 'TRY' }, onReset: 'function(){ window.location.reload(); }' }) %> ``` ### CartBadge (`cart-badge`) - **Layer:** domain - **Category:** Domain - **File:** `modules/domain/common/cart/CartBadge.ejs` - **Status:** stable (since 2026-05) - **Description:** Round cart button for the header that shows the number of items in the cart. Counts above 99 render as a "99+" badge. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --surface-overlay, --surface-raised, --text-primary **Variants:** *States* ```ejs <%- include('modules/domain/common/cart/CartBadge', { cart: { items: [] } }) %> <%- include('modules/domain/common/cart/CartBadge', { cart: { items: [{ quantity: 1 }, { quantity: 2 }] } }) %> <%- include('modules/domain/common/cart/CartBadge', { cart: { items: [{ quantity: 150 }] } }) %> ``` *In a header* ```ejs
Shop
``` ## Components — domain / Domain · API Doc ### StatusCodeBadge (`api-doc-status-code-badge`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/StatusCodeBadge.ejs` - **Status:** stable (since 2025-04) - **Description:** Displays an HTTP status code with a semantic color and an optional human-readable label. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --info, --info-fg, --info-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-overlay, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Success & redirect codes* ```ejs <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '200' }) %> <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '201' }) %> <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '204' }) %> ``` *Client & server error codes* ```ejs <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '400' }) %> <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '401' }) %> <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '404' }) %> <%- include('modules/domain/api-doc/StatusCodeBadge', { code: '500' }) %> ``` ### HttpMethodBadge (`api-doc-http-method-badge`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/HttpMethodBadge.ejs` - **Status:** stable (since 2025-04) - **Description:** Color-coded badge for HTTP methods — GET, POST, PUT, PATCH, DELETE, and more. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --primary, --primary-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-sunken, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *All methods* ```ejs <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'GET' }) %> <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'POST' }) %> <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'DELETE' }) %> ``` *Sizes* ```ejs <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'GET', size: 'sm' }) %> <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'GET', size: 'md' }) %> <%- include('modules/domain/api-doc/HttpMethodBadge', { method: 'GET', size: 'lg' }) %> ``` ### SecurityBadge (`api-doc-security-badge`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/SecurityBadge.ejs` - **Status:** stable (since 2025-04) - **Description:** Inline badge representing an API security scheme type with a matching icon and color. - **Design tokens:** --border, --info, --info-fg, --info-subtle, --primary, --primary-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-sunken, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Scheme types* ```ejs <%- include('modules/domain/api-doc/SecurityBadge', { type: 'http', name: 'BearerAuth' }) %> <%- include('modules/domain/api-doc/SecurityBadge', { type: 'apiKey', name: 'ApiKey' }) %> <%- include('modules/domain/api-doc/SecurityBadge', { type: 'oauth2', name: 'OAuth2' }) %> ``` ### SecuritySchemeBadge (`api-doc-security-scheme-badge`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/SecuritySchemeBadge.ejs` - **Status:** stable (since 2025-04) - **Description:** Badge variant of SecurityBadge that uses the design-system Badge component for consistent styling. - **Design tokens:** --error, --error-fg, --error-subtle, --info, --info-fg, --info-subtle, --primary, --primary-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-sunken, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Pill variants* ```ejs <%- include('modules/domain/api-doc/SecuritySchemeBadge', { scheme: { name: 'BearerAuth', type: 'http' } }) %> <%- include('modules/domain/api-doc/SecuritySchemeBadge', { scheme: { name: 'ApiKey', type: 'apiKey' } }) %> ``` ### ParameterTable (`api-doc-parameter-table`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/ParameterTable.ejs` - **Status:** stable (since 2025-04) - **Description:** Table displaying API parameters with location (path/query/header/cookie), type, required flag, and description. - **Design tokens:** --border, --primary, --secondary, --surface-base, --surface-raised, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Query parameters* ```ejs <%- include('modules/domain/api-doc/ParameterTable', { parameters: [ { parameterId: 'p1', name: 'page', in: 'query', schema: { type: 'integer' }, description: 'Page number (1-based)' }, { parameterId: 'p2', name: 'pageSize', in: 'query', required: true, schema: { type: 'integer' }, description: 'Items per page' }, ] }) %> ``` ### SchemaViewer (`api-doc-schema-viewer`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/SchemaViewer.ejs` - **Status:** stable (since 2025-04) - **Description:** Interactive JSON Schema tree viewer with type coloring, constraint display, and collapsible nodes. - **Design tokens:** --border, --error, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary, --warning, --warning-subtle **Variants:** *Object schema* ```ejs <%- include('modules/domain/api-doc/SchemaViewer', { schema: { type: 'object', required: ['id', 'name', 'price'], properties: { id: { type: 'string', description: 'UUID identifier', readOnly: true }, name: { type: 'string', description: 'Display name' }, price: { type: 'number' }, } } }) %> ``` ### CodeSamplePanel (`api-doc-code-sample-panel`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/CodeSamplePanel.ejs` - **Status:** stable (since 2025-04) - **Description:** Dark-themed panel showing code samples in multiple languages with tab switching and a one-click copy button. - **Design tokens:** --border, --border-focus, --success **Variants:** *Multi-language samples* ```ejs <%- include('modules/domain/api-doc/CodeSamplePanel', { samples: [ { lang: 'curl', label: 'cURL', source: "curl -X GET https://api.example.com/users \ -H 'Authorization: Bearer '" }, { lang: 'javascript', label: 'JavaScript', source: "const res = await fetch('/users', { headers: { Authorization: 'Bearer ' } });" }, ] }) %> ``` ### ServerSelector (`api-doc-server-selector`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/ServerSelector.ejs` - **Status:** stable (since 2025-04) - **Description:** Dropdown for selecting the active API server, with environment badges (production, staging, development, sandbox). - **Design tokens:** --border, --border-focus, --border-strong, --primary, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Production selected* ```ejs <%- include('modules/domain/api-doc/ServerSelector', { servers: [ { serverId: 'srv-prod', url: 'https://api.commerce.io/v2', description: 'Production', environment: 'production' }, { serverId: 'srv-stg', url: 'https://staging-api.commerce.io/v2', description: 'Staging', environment: 'staging' }, ] }) %> ``` ### ResponseCard (`api-doc-response-card`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/ResponseCard.ejs` - **Status:** stable (since 2025-04) - **Description:** Collapsible card for a single API response showing the status code, description, and response body schema. - **Design tokens:** --border, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *200 OK response* ```ejs <%- include('modules/domain/api-doc/ResponseCard', { response: { responseId: 'r-200', statusCode: '200', description: 'Token issued', content: { 'application/json': { schema: { type: 'object' } } } } }) %> ``` ### OperationPanel (`api-doc-operation-panel`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/OperationPanel.ejs` - **Status:** stable (since 2025-04) - **Description:** Tabbed panel showing all details of an API operation — parameters, request body, responses, and code samples. - **Design tokens:** --border, --primary, --secondary, --surface-base, --surface-raised, --text-disabled, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *GET operation with parameters* ```ejs <%- include('modules/domain/api-doc/OperationPanel', { operation: { operationId: 'list-products', operationKey: 'list-products', method: 'GET', tags: ['Products'], description: 'Returns a paginated list of products.', security: [{ BearerAuth: [] }], parameters: [ { parameterId: 'p1', name: 'page', in: 'query', schema: { type: 'integer' } }, { parameterId: 'p2', name: 'pageSize', in: 'query', schema: { type: 'integer' } }, { parameterId: 'p3', name: 'category', in: 'query', schema: { type: 'string' } }, ], responses: [ { responseId: 'r1', statusCode: '200', description: 'OK' }, ], } }) %> ``` ### EndpointRow (`api-doc-endpoint-row`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/EndpointRow.ejs` - **Status:** stable (since 2025-04) - **Description:** Collapsible row representing a single API endpoint — shows the method badge, path, summary, and expands to an OperationPanel. - **Design tokens:** --border, --primary, --secondary, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary, --warning **Variants:** *GET and POST rows* ```ejs <%- include('modules/domain/api-doc/EndpointRow', { path: '/products', operation: { operationId: 'list-products', operationKey: 'list-products', method: 'GET', summary: 'List products', security: [{ BearerAuth: [] }], parameters: [], responses: [], } }) %> ``` ### ApiTagSection (`api-doc-api-tag-section`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/ApiTagSection.ejs` - **Status:** stable (since 2025-04) - **Description:** Collapsible section grouping endpoints under a named tag — the primary navigation unit in API documentation. - **Design tokens:** --border, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Products tag section* ```ejs <%- include('modules/domain/api-doc/ApiTagSection', { tag: { tagId: 'tag-products', name: 'Products', description: 'Product catalogue — create, read, update, delete' }, paths: [ { pathItem: { pathItemId: 'pi-products', path: '/products', operations: [ { operationId: 'list-products', operationKey: 'list-products', method: 'GET', summary: 'List products', parameters: [], responses: [] }, { operationId: 'create-product', operationKey: 'create-product', method: 'POST', summary: 'Create product', parameters: [], responses: [] }, ]}} ] }) %> ``` ### ApiKeyTokenCard (`api-doc-api-key-token-card`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/ApiKeyTokenCard.ejs` - **Status:** stable (since 2026-05) - **Description:** Card for a single API key — reveal/hide, copy-to-clipboard, env badge, last-used metadata. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --primary-subtle, --secondary, --success, --surface-base, --surface-overlay, --surface-raised, --text-disabled, --text-primary, --text-secondary, --warning, --warning-subtle **Variants:** *Production key with scopes* ```ejs <%- include('modules/domain/api-doc/ApiKeyTokenCard', { name: 'Production key', token: 'demo_production_token_REDACTED', environment: 'production', createdAt: '2026-01-12', lastUsedAt: '2026-05-20', scopes: ['read:products', 'write:orders'] }) %> ``` *Staging key with revoke action* ```ejs <%- include('modules/domain/api-doc/ApiKeyTokenCard', { name: 'Staging key', token: 'demo_staging_token_REDACTED', environment: 'staging', createdAt: '2026-04-01', lastUsedAt: null, onRevoke: true }) %> ``` ### AuthSchemeCard (`api-doc-auth-scheme-card`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/AuthSchemeCard.ejs` - **Status:** stable (since 2026-05) - **Description:** Selectable card describing a single auth scheme (apiKey, http, oauth2, openIdConnect, mutualTLS). - **Design tokens:** --border, --border-focus, --primary, --primary-subtle, --secondary, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *OAuth 2.0 recommended scheme* ```ejs <%- include('modules/domain/api-doc/AuthSchemeCard', { name: 'OAuth 2.0', type: 'oauth2', recommended: true, description: 'Authorization Code flow with PKCE — preferred for user-facing apps.', metaItems: [ { label: 'Client ID', value: 'prod_client_42' }, { label: 'Scopes', value: 'read write' }, ], href: '/auth/oauth2' }) %> ``` *API Key static scheme* ```ejs <%- include('modules/domain/api-doc/AuthSchemeCard', { name: 'X-API-Key', type: 'apiKey', description: 'Static header-based key for server-to-server integrations.' }) %> ``` ### OAuthFlowDiagram (`api-doc-oauth-flow-diagram`) - **Layer:** domain - **Category:** Domain · API Doc - **File:** `modules/domain/api-doc/OAuthFlowDiagram.ejs` - **Status:** stable (since 2026-05) - **Description:** Visual walkthrough of an OAuth 2.0 flow with actors, numbered steps, endpoints, and scopes. - **Design tokens:** --border, --primary, --primary-subtle, --secondary, --success, --surface-base, --surface-raised, --text-disabled, --text-primary, --text-secondary **Variants:** *Authorization Code flow* ```ejs <%- include('modules/domain/api-doc/OAuthFlowDiagram', { flow: 'authorizationCode', authorizationUrl: 'https://auth.example.com/authorize', tokenUrl: 'https://auth.example.com/token', scopes: [ { name: 'read', description: 'Read access' }, { name: 'write', description: 'Write access' }, ] }) %> ``` *Client Credentials flow* ```ejs <%- include('modules/domain/api-doc/OAuthFlowDiagram', { flow: 'clientCredentials', tokenUrl: 'https://auth.example.com/token' }) %> ``` ## Components — domain / Domain · Invoice ### InvoiceStatusBadge (`invoice-status-badge`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/InvoiceStatusBadge.ejs` - **Status:** stable (since 2025-05) - **Description:** Invoice lifecycle status badge: Draft, Sent, Paid, Overdue, Partial, Cancelled. - **Design tokens:** --border, --error, --info, --secondary, --success, --surface-overlay, --surface-sunken, --text-secondary, --warning **Variants:** *All statuses* ```ejs <%- include('../../../modules/domain/invoice/InvoiceStatusBadge', { status: 'PAID' }) %> ``` ### PaymentTermsBadge (`invoice-payment-terms-badge`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/PaymentTermsBadge.ejs` - **Status:** stable - **Description:** Payment terms badge: Net 7, Net 15, Net 30, Net 60, Due on Receipt, Immediate. - **Design tokens:** --info **Variants:** *Payment terms* ```ejs <%- include('../../../modules/domain/invoice/PaymentTermsBadge', { terms: 'NET_30' }) %> ``` ### ClientInfoBlock (`invoice-client-info-block`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/ClientInfoBlock.ejs` - **Status:** stable - **Description:** Address block component for BILL TO / SHIP TO / FROM sections in an invoice. - **Design tokens:** --primary, --secondary, --text-primary, --text-secondary **Variants:** *Bill To block* ```ejs <%- include('../../../modules/domain/invoice/ClientInfoBlock', { type: 'BILL_TO', entity: client }) %> ``` ### InvoiceLineItem (`invoice-line-item`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/InvoiceLineItem.ejs` - **Status:** stable (since 2025-05) - **Description:** Table row for a single invoice line item with description, quantity, unit price, and total. - **Design tokens:** --primary, --secondary, --surface-base, --surface-raised, --text-primary, --text-secondary **Variants:** *Line items table* ```ejs <% lineItems.forEach(function(item, i) { %> <%- include('../../../modules/domain/invoice/InvoiceLineItem', { item: item, index: i, currency: 'TRY' }) %> <% }); %> ``` ### InvoiceTotals (`invoice-totals`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/InvoiceTotals.ejs` - **Status:** stable (since 2025-05) - **Description:** Invoice totals block: subtotal, discount, VAT, total, paid amount, and balance due. - **Design tokens:** --border, --error, --primary, --secondary, --success, --text-primary, --text-secondary **Variants:** *Full totals* ```ejs <%- include('../../../modules/domain/invoice/InvoiceTotals', { subtotal: inv.subtotal, taxRate: inv.taxRate, taxAmount: inv.taxAmount, discount: inv.discount, total: inv.total, currency: 'TRY' }) %> ``` ### InvoiceNotes (`invoice-notes`) - **Layer:** domain - **Category:** Domain · Invoice - **File:** `modules/domain/invoice/InvoiceNotes.ejs` - **Status:** stable (since 2025-05) - **Description:** Invoice footer section with optional notes text, bank details, and custom footer. - **Design tokens:** --border, --primary, --secondary, --text-primary, --text-secondary **Variants:** *Notes + bank details* ```ejs <%- include('../../../modules/domain/invoice/InvoiceNotes', { notes: inv.notes, bankDetails: issuer.bankDetails }) %> ``` ## Components — domain / Domain · Modem ### ConnectionStatusBadge (`modem-connection-status-badge`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/ConnectionStatusBadge.ejs` - **Status:** stable (since 2025-05) - **Description:** Color-coded badge for router/modem connection states. CONNECTED · DISCONNECTED · CONNECTING (pulse) · ERROR · ENABLED · DISABLED · ACTIVE · INACTIVE. - **Design tokens:** --border, --error, --info, --secondary, --success, --surface-overlay, --text-secondary, --warning **Variants:** *Connection states* ```ejs <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'CONNECTED' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'DISCONNECTED' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'CONNECTING' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'ERROR' }) %> ``` *Enabled / Active states* ```ejs <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'ENABLED' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'DISABLED' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'ACTIVE' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'INACTIVE' }) %> ``` *Custom label* ```ejs <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'CONNECTED', label: 'Online' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'DISCONNECTED', label: 'No signal' }) %> <%- include('modules/domain/modem/ConnectionStatusBadge', { status: 'CONNECTING', label: 'Dialing…' }) %> ``` ### SystemStatusCard (`modem-system-status-card`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/SystemStatusCard.ejs` - **Status:** stable (since 2025-05) - **Description:** Device model, CPU/RAM progress bars, temperature, uptime, and firmware version. Color changes based on temperature thresholds (>55 warning, >70 error). - **Design tokens:** --border, --error, --primary, --secondary, --success, --surface-raised, --surface-sunken, --text-primary, --text-secondary, --warning **Variants:** *Normal load* ```ejs <%- include('modules/domain/modem/SystemStatusCard', { hostname: system.hostname, model: system.model, firmware: system.firmware, resources: system.resources, time: system.time, }) %> ``` *High load (warning)* ```ejs <%- include('modules/domain/modem/SystemStatusCard', { hostname: system.hostname, model: system.model, firmware: system.firmware, resources: { cpuPercent: 75, memoryPercent: 76, memoryUsedMb: 390, memoryTotalMb: 512, temperatureCelsius: 67 }, time: system.time, }) %> ``` ### WanStatusCard (`modem-wan-status-card`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/WanStatusCard.ejs` - **Status:** stable (since 2025-05) - **Description:** WAN connection type, IP/gateway/DNS information, and ISP speed indicator. Color-coded based on status badge. - **Design tokens:** --border, --error, --info, --primary, --primary-subtle, --secondary, --success, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Connected (PPPoE)* ```ejs <%- include('modules/domain/modem/WanStatusCard', { wan: state.wan }) %> ``` *Disconnected* ```ejs <%- include('modules/domain/modem/WanStatusCard', { wan: { status: 'DISCONNECTED', connectionType: 'DHCP', ipAddress: null, gateway: null, dnsPrimary: null, mtu: 1500 } }) %> ``` ### WifiNetworkCard (`modem-wifi-network-card`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/WifiNetworkCard.ejs` - **Status:** stable (since 2025-05) - **Description:** SSID, band (2.4/5/6 GHz), security mode, and guest/disabled labels. An edit link is added via the editHref prop. - **Design tokens:** --border, --info, --primary, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Dual-band + guest* ```ejs <% wifi.networks.forEach(function(net) { %> <%- include('modules/domain/modem/WifiNetworkCard', { network: net }) %> <% }); %> ``` *Disabled network* ```ejs <%- include('modules/domain/modem/WifiNetworkCard', { network: { ...net, enabled: false } }) %> ``` *With edit link* ```ejs <%- include('modules/domain/modem/WifiNetworkCard', { network: net, editHref: '/wifi/edit/' + net.id }) %> ``` ### ConnectedDeviceRow (`modem-connected-device-row`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/ConnectedDeviceRow.ejs` - **Status:** stable (since 2025-05) - **Description:** Connected device table row. Device icon, hostname/MAC, IP, connection type (wired/wifi + dBm), traffic, and DHCP/Static/Blocked status. - **Design tokens:** --border, --error, --info, --primary, --primary-subtle, --secondary, --success, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Mixed device list* ```ejs ... <% state.connectedDevices.forEach(function(dev) { %> <%- include('modules/domain/modem/ConnectedDeviceRow', { device: dev }) %> <% }); %>
``` ### PortForwardRow (`modem-port-forward-row`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/PortForwardRow.ejs` - **Status:** stable (since 2025-05) - **Description:** Port forwarding table row. Rule name, protocol, external port, internal IP:port, and enabled/disabled state. - **Design tokens:** --border, --primary, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Port forwarding rules* ```ejs ... <% nat.portForwardRules.forEach(function(rule) { %> <%- include('modules/domain/modem/PortForwardRow', { rule: rule }) %> <% }); %>
``` *Port range rule* ```ejs <%- include('modules/domain/modem/PortForwardRow', { rule: { name: 'Game Server', enabled: true, protocol: 'UDP', externalPort: { start: 27015, end: 27030 }, internalIp: '192.168.1.10', internalPort: { start: 27015, end: 27030 }, } }) %> ``` ### AlertItem (`modem-alert-item`) - **Layer:** domain - **Category:** Domain · Modem - **File:** `modules/domain/modem/AlertItem.ejs` - **Status:** stable (since 2025-05) - **Description:** Router notification row. INFO · WARNING · CRITICAL severity levels. Unread alerts are marked with a blue dot. - **Design tokens:** --error, --info, --primary, --secondary, --text-primary, --text-secondary, --warning **Variants:** *All severities (unread)* ```ejs <% state.alerts.forEach(function(alert) { %> <%- include('modules/domain/modem/AlertItem', { alert: alert }) %> <% }); %> ``` *Read vs unread* ```ejs <%- include('modules/domain/modem/AlertItem', { alert: { ...alert, read: false } }) %> <%- include('modules/domain/modem/AlertItem', { alert: { ...alert, read: true } }) %> ``` *Unread-only filter* ```ejs <% state.alerts.filter(function(a){ return !a.read; }).forEach(function(alert) { %> <%- include('modules/domain/modem/AlertItem', { alert: alert }) %> <% }); %> ``` ## Components — domain / Domain · UPS ### UpsStatusBadge (`ups-status-badge`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/UpsStatusBadge.ejs` - **Status:** stable (since 2025-05) - **Description:** UPS power status badge: On Line, On Battery (pulse), Low Battery, Fault, Calibrating, Bypassed, Offline. - **Design tokens:** --border, --error, --info, --secondary, --success, --surface-overlay, --text-secondary, --warning **Variants:** *All statuses* ```ejs <%- include('../../../modules/domain/ups/UpsStatusBadge', { status: 'ON_LINE' }) %> ``` ### BatteryLevelBar (`ups-battery-level-bar`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/BatteryLevelBar.ejs` - **Status:** stable (since 2025-05) - **Description:** Battery charge indicator with percent, color-coded bar (green/yellow/red), charge status, and runtime remaining. - **Design tokens:** --error, --secondary, --success, --surface-sunken, --text-secondary, --warning **Variants:** *Full charge* ```ejs <%- include('../../../modules/domain/ups/BatteryLevelBar', { percent: battery.percent, status: battery.status, runtimeMinutes: battery.runtimeMinutes }) %> ``` *Low battery* ```ejs <%- include('../../../modules/domain/ups/BatteryLevelBar', { percent: 12, status: 'DISCHARGING', runtimeMinutes: 6 }) %> ``` ### PowerLoadGauge (`ups-power-load-gauge`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/PowerLoadGauge.ejs` - **Status:** stable (since 2025-05) - **Description:** Load percentage gauge with watts/VA display and color-coded bar (green < 50%, yellow 50-80%, red > 80%). - **Design tokens:** --error, --secondary, --success, --surface-sunken, --text-secondary, --warning **Variants:** *Normal load* ```ejs <%- include('../../../modules/domain/ups/PowerLoadGauge', { loadPercent: output.loadPercent, watts: output.watts, va: output.va, capacity: ups }) %> ``` *Critical load* ```ejs <%- include('../../../modules/domain/ups/PowerLoadGauge', { loadPercent: 87, watts: 2349, va: 2610 }) %> ``` ### OutletCard (`ups-outlet-card`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/OutletCard.ejs` - **Status:** stable (since 2025-05) - **Description:** Outlet status card showing name, ON/OFF status badge, power draw, and protection indicator. - **Design tokens:** --border, --info, --primary, --secondary, --success, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Outlet states* ```ejs <%- include('../../../modules/domain/ups/OutletCard', { outlet: outlet }) %> ``` ### EventLogRow (`ups-event-log-row`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/EventLogRow.ejs` - **Status:** stable (since 2025-05) - **Description:** Event log table row with timestamp, severity indicator, event code, and message. - **Design tokens:** --error, --info, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Event log* ```ejs <% events.forEach(function(ev, i) { %> <%- include('../../../modules/domain/ups/EventLogRow', { event: ev, index: i }) %> <% }); %> ``` ### UpsInfoCard (`ups-info-card`) - **Layer:** domain - **Category:** Domain · UPS - **File:** `modules/domain/ups/UpsInfoCard.ejs` - **Status:** stable (since 2025-05) - **Description:** 2-column info grid displaying UPS model, serial, firmware, capacity, and voltage specs. - **Design tokens:** --border, --primary, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *UPS info grid* ```ejs <%- include('../../../modules/domain/ups/UpsInfoCard', { info: state.ups }) %> ``` ## Components — theme / Email ### WelcomeEmail (`email-welcome`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/welcome.ejs` - **Status:** stable (since 2025-05) - **Description:** Welcome email sent after registration. Includes an email verification CTA and a "next steps" list. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --text-primary, --text-secondary **Variants:** *Welcome Email* ```ejs // GET /theme/common/email/auth/welcome res.render('theme/common/email/auth/welcome', { layout: 'layouts/blank', subject: 'Welcome to Acme Corp! Confirm your email to get started', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, date: new Date().toLocaleString(), company: { name: 'Acme Corp', address: '123 Main St · San Francisco, CA' }, confirmUrl: generateConfirmUrl(user.id), }); ``` ### VerifyEmailOTP (`email-verify-otp`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/verify-email.ejs` - **Status:** stable (since 2025-05) - **Description:** 6-digit OTP code. Large mono-font code display and expiry notice. - **Design tokens:** --border, --primary, --primary-fg, --primary-subtle, --secondary, --surface-overlay, --text-primary, --text-secondary **Variants:** *OTP Code Email* ```ejs // GET /theme/common/email/auth/verify-email res.render('theme/common/email/auth/verify-email', { layout: 'layouts/blank', subject: 'Your Acme Corp verification code', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, otp: generateOTP(), // e.g. '847 392' expiresIn: '15 minutes', }); ``` ### PasswordResetEmail (`email-password-reset`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/password-reset.ejs` - **Status:** stable (since 2025-05) - **Description:** Password reset link email. Includes an expiry notice and a security note. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --text-primary, --text-secondary, --warning **Variants:** *Password Reset* ```ejs // GET /theme/common/email/auth/password-reset res.render('theme/common/email/auth/password-reset', { layout: 'layouts/blank', subject: 'Reset your Acme Corp password', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, resetUrl: generateResetUrl(token), expiresIn: '1 hour', }); ``` ### PasswordChangedEmail (`email-password-changed`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/password-changed.ejs` - **Status:** stable (since 2025-05) - **Description:** Password change confirmation email. Device, IP, and location info with a "Wasn't me" alert. - **Design tokens:** --border, --error, --primary, --primary-fg, --secondary, --success, --surface-raised, --text-primary, --text-secondary **Variants:** *Password Changed* ```ejs // GET /theme/common/email/auth/password-changed res.render('theme/common/email/auth/password-changed', { layout: 'layouts/blank', subject: 'Your password has been changed', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, changedAt: new Date().toLocaleString(), ipAddress: req.ip, location: resolveLocation(req.ip), supportUrl: '/support', }); ``` ### LoginAlertEmail (`email-login-alert`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/login-alert.ejs` - **Status:** stable (since 2025-05) - **Description:** New device sign-in alert. Device, location, and IP info; "Secure My Account" and "This Was Me" CTA pair. - **Design tokens:** --border, --error, --primary, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *New Login Alert* ```ejs // GET /theme/common/email/auth/login-alert res.render('theme/common/email/auth/login-alert', { layout: 'layouts/blank', subject: 'New sign-in to your Acme Corp account', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, device: parseUserAgent(req.headers['user-agent']), location: resolveLocation(req.ip), ipAddress: req.ip, loginTime: new Date().toLocaleString(), secureUrl: '/account/security', }); ``` ### AccountLockedEmail (`email-account-locked`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/auth/account-locked.ejs` - **Status:** stable (since 2025-05) - **Description:** Account lock email after too many failed sign-in attempts. Unlock time, reason, and "Unlock" / "Support" buttons. - **Design tokens:** --border, --error, --primary, --primary-fg, --primary-hover, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Account Locked* ```ejs // GET /theme/common/email/auth/account-locked res.render('theme/common/email/auth/account-locked', { layout: 'layouts/blank', subject: 'Your account has been temporarily locked', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, reason: 'Too many failed login attempts', unlockAt: lockExpiry.toLocaleString(), unlockUrl: '/auth/unlock?token=' + token, supportUrl: '/support', }); ``` ### OrderConfirmationEmail (`email-order-confirmation`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/confirmation.ejs` - **Status:** stable (since 2025-05) - **Description:** Order confirmation receipt. Line items, subtotal/tax/total, and shipping address. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --success, --surface-raised, --text-primary, --text-secondary **Variants:** *Order Confirmed* ```ejs // GET /theme/common/email/order/confirmation res.render('theme/common/email/order/confirmation', { layout: 'layouts/blank', subject: 'Order confirmed — #' + order.id, fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, order: { id: order.id, date: order.createdAt.toLocaleDateString(), items: order.items, // [{ name, variant, qty, price }] totals: order.totals, // { subtotal, discount, tax, shipping, total, currency } shipping: order.address, // { fullName, addressLine1, city, country, postalCode } viewUrl: '/orders/' + order.id, }, }); ``` ### OrderShippedEmail (`email-order-shipped`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/shipped.ejs` - **Status:** stable (since 2025-05) - **Description:** Shipping notification. Carrier, tracking number, estimated delivery date, and a "Track My Package" CTA. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --text-primary, --text-secondary **Variants:** *Order Shipped* ```ejs // GET /theme/common/email/order/shipped res.render('theme/common/email/order/shipped', { layout: 'layouts/blank', subject: 'Your order #' + order.id + ' has shipped!', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, order: { id: order.id, carrier: shipment.carrier, trackingNumber: shipment.trackingNumber, trackingUrl: shipment.trackingUrl, estimatedDelivery: shipment.estimatedDelivery, items: order.items, }, }); ``` ### OrderDeliveredEmail (`email-order-delivered`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/delivered.ejs` - **Status:** stable (since 2025-05) - **Description:** Delivery confirmation. Star-rating buttons with a review request CTA. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --success, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Order Delivered* ```ejs // GET /theme/common/email/order/delivered res.render('theme/common/email/order/delivered', { layout: 'layouts/blank', subject: 'Your order has been delivered!', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, order: { id: order.id, deliveredAt: delivery.timestamp.toLocaleString(), items: order.items, reviewUrl: '/orders/' + order.id + '/review', viewUrl: '/orders/' + order.id, }, }); ``` ### OrderCancelledEmail (`email-order-cancelled`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/cancelled.ejs` - **Status:** stable (since 2025-05) - **Description:** Order cancellation notice. Cancellation reason, struck-through items, and refund window. - **Design tokens:** --border, --info, --primary, --primary-fg, --primary-hover, --secondary, --surface-raised, --surface-sunken, --text-primary, --text-secondary **Variants:** *Order Cancelled* ```ejs // GET /theme/common/email/order/cancelled res.render('theme/common/email/order/cancelled', { layout: 'layouts/blank', subject: 'Your order #' + order.id + ' has been cancelled', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, order: { id: order.id, reason: cancellation.reason, items: order.items, refundAmount: order.total, refundDays: '3–5 business days', currency: order.currency, shopUrl: '/shop', }, }); ``` ### RefundEmail (`email-refund`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/refund.ejs` - **Status:** stable (since 2025-05) - **Description:** Refund processed notice. Amount hero, refund method, and estimated arrival window. - **Design tokens:** --border, --primary, --primary-fg, --secondary, --success, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Refund Processed* ```ejs // GET /theme/common/email/order/refund res.render('theme/common/email/order/refund', { layout: 'layouts/blank', subject: 'Your refund of ₺' + refund.amount + ' has been processed', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, refund: { id: refund.id, orderId: order.id, amount: refund.amount, currency: refund.currency, method: paymentMethod.display, // 'Visa ••••4242' processedAt: new Date().toLocaleDateString(), arrivalDays: '3–5 business days', viewUrl: '/orders/' + order.id, }, }); ``` ### AbandonedCartEmail (`email-abandoned-cart`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/order/abandoned-cart.ejs` - **Status:** stable (since 2025-05) - **Description:** Abandoned cart re-engagement email. Coupon code, discount offer, and cart contents. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Abandoned Cart* ```ejs // GET /theme/common/email/order/abandoned-cart res.render('theme/common/email/order/abandoned-cart', { layout: 'layouts/blank', subject: 'You left something behind…', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, cart: { items: cart.items, total: cart.subtotal, currency: cart.currency, couponCode: 'COMEBACK10', couponPct: 10, expiresAt: expiryDate.toLocaleDateString(), resumeUrl: '/cart?token=' + cart.token, }, }); ``` ### InvoiceEmail (`email-invoice`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/invoice.ejs` - **Status:** stable (since 2025-05) - **Description:** Itemized invoice. Status badge, all line items and totals, PDF download CTA. - **Design tokens:** --border, --error, --primary, --primary-fg, --primary-hover, --secondary, --success, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Invoice — PAID* ```ejs // GET /theme/common/email/billing/invoice res.render('theme/common/email/billing/invoice', { layout: 'layouts/blank', subject: 'Invoice #' + invoice.id + ' from Acme Corp', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, invoice: { id: invoice.id, date: invoice.createdAt.toLocaleDateString(), dueDate: invoice.dueDate.toLocaleDateString(), status: invoice.status, // 'PAID' | 'PENDING' | 'OVERDUE' items: invoice.lineItems, // [{ name, variant, qty, price }] totals: invoice.totals, downloadUrl: '/invoices/' + invoice.id + '/pdf', }, }); ``` ### SubscriptionActivatedEmail (`email-subscription-activated`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/subscription-activated.ejs` - **Status:** stable (since 2025-05) - **Description:** Plan activation confirmation. Feature list, price, and next billing date. - **Design tokens:** --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --success, --text-primary, --text-secondary **Variants:** *Subscription Activated* ```ejs // GET /theme/common/email/billing/subscription-activated res.render('theme/common/email/billing/subscription-activated', { layout: 'layouts/blank', subject: 'Your ' + plan.name + ' subscription is now active!', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, plan: { name: plan.name, price: plan.price, currency: plan.currency, interval: plan.billingInterval, // 'month' | 'year' nextBillingDate: subscription.nextBillingDate.toLocaleDateString(), features: plan.features, manageUrl: '/account/subscription', }, }); ``` ### RenewalReminderEmail (`email-renewal-reminder`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/renewal-reminder.ejs` - **Status:** stable (since 2025-05) - **Description:** 7-day advance renewal reminder. Payment method, amount, and manage/cancel links. - **Design tokens:** --border, --info, --primary, --primary-fg, --primary-hover, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Renewal Reminder* ```ejs // GET /theme/common/email/billing/renewal-reminder res.render('theme/common/email/billing/renewal-reminder', { layout: 'layouts/blank', subject: 'Your ' + plan.name + ' renews in 7 days', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, plan: { name: subscription.planName, renewalDate: subscription.nextBillingDate.toLocaleDateString(), amount: subscription.amount, currency: subscription.currency, paymentMethod: paymentMethod.display, manageUrl: '/account/subscription', cancelUrl: '/account/subscription/cancel', }, }); ``` ### SubscriptionCancelledEmail (`email-subscription-cancelled`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/subscription-cancelled.ejs` - **Status:** stable (since 2025-05) - **Description:** Subscription cancellation. Access end date, reactivation CTA, and feedback link. - **Design tokens:** --border, --info, --primary, --primary-fg, --primary-hover, --secondary, --surface-raised, --surface-sunken, --text-primary, --text-secondary **Variants:** *Subscription Cancelled* ```ejs // GET /theme/common/email/billing/subscription-cancelled res.render('theme/common/email/billing/subscription-cancelled', { layout: 'layouts/blank', subject: 'Your ' + plan.name + ' subscription has been cancelled', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, plan: { name: subscription.planName, cancelledAt: new Date().toLocaleDateString(), accessUntil: subscription.currentPeriodEnd.toLocaleDateString(), reactivateUrl: '/account/subscription/reactivate', feedbackUrl: '/feedback/cancellation', }, }); ``` ### PaymentFailedEmail (`email-payment-failed`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/payment-failed.ejs` - **Status:** stable (since 2025-05) - **Description:** Payment failed notice. Failure reason, retry date, and an update-payment-method CTA. - **Design tokens:** --border, --error, --primary, --secondary, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Payment Failed* ```ejs // GET /theme/common/email/billing/payment-failed res.render('theme/common/email/billing/payment-failed', { layout: 'layouts/blank', subject: "We couldn't process your payment", fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, payment: { amount: charge.amount, currency: charge.currency, attemptedAt: charge.createdAt.toLocaleString(), failReason: charge.failureMessage, retryDate: nextRetry.toLocaleDateString(), updateUrl: '/account/payment-methods', }, }); ``` ### CardExpiringEmail (`email-card-expiring`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/billing/card-expiring.ejs` - **Status:** stable (since 2025-05) - **Description:** Card expiration warning. Card visual, plan/billing info, and an update CTA. - **Design tokens:** --info, --primary, --primary-fg, --primary-hover, --secondary, --text-primary, --text-secondary, --warning **Variants:** *Card Expiring Soon* ```ejs // GET /theme/common/email/billing/card-expiring res.render('theme/common/email/billing/card-expiring', { layout: 'layouts/blank', subject: 'Your ' + card.brand + ' ••••' + card.last4 + ' expires next month', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, card: { last4: card.last4, brand: card.brand, expiryMonth: card.expMonth.toString().padStart(2, '0'), expiryYear: card.expYear.toString().slice(-2), updateUrl: '/account/payment-methods', }, plan: { name: subscription.planName, nextBillingDate: subscription.nextBillingDate.toLocaleDateString(), }, }); ``` ### CommentReplyEmail (`email-comment-reply`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/notification/comment-reply.ejs` - **Status:** stable (since 2025-05) - **Description:** Comment reply notification. Original comment + new reply in a thread view. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Comment Reply* ```ejs // GET /theme/common/email/notification/comment-reply res.render('theme/common/email/notification/comment-reply', { layout: 'layouts/blank', subject: reply.authorName + ' replied to your comment', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: originalComment.authorName, toEmail: originalComment.authorEmail, company: { name: 'Acme Corp', address: '...' }, notification: { senderName: reply.authorName, postTitle: post.title, postUrl: '/posts/' + post.slug, yourComment: originalComment.body, replyText: reply.body, viewUrl: '/posts/' + post.slug + '#comment-' + reply.id, }, }); ``` ### MentionEmail (`email-mention`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/notification/mention.ejs` - **Status:** stable (since 2025-05) - **Description:** @mention notification. Mentioner, context, and quoted excerpt display. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Mention Notification* ```ejs // GET /theme/common/email/notification/mention res.render('theme/common/email/notification/mention', { layout: 'layouts/blank', subject: mention.authorName + ' mentioned you in a comment', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: mentionedUser.name, toEmail: mentionedUser.email, company: { name: 'Acme Corp', address: '...' }, notification: { mentionerName: mention.authorName, context: 'mentioned you in a comment on "' + post.title + '"', excerpt: buildExcerptAround(mention, comment.body), viewUrl: '/posts/' + post.slug + '#comment-' + comment.id, }, }); ``` ### NewMessageEmail (`email-new-message`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/notification/new-message.ejs` - **Status:** stable (since 2025-05) - **Description:** Inbox message notification. Sender, message preview, and Reply/Inbox CTA pair. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *New Message* ```ejs // GET /theme/common/email/notification/new-message res.render('theme/common/email/notification/new-message', { layout: 'layouts/blank', subject: 'You have a new message from ' + sender.name, fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: recipient.name, toEmail: recipient.email, company: { name: 'Acme Corp', address: '...' }, notification: { senderName: sender.name, messageCount: unreadCount, preview: message.body.slice(0, 120) + '…', sentAt: message.createdAt.toLocaleString(), replyUrl: '/messages/' + thread.id, }, }); ``` ### NewsletterEmail (`email-newsletter`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/marketing/newsletter.ejs` - **Status:** stable (since 2025-05) - **Description:** Weekly newsletter. Article cards, tag colors, and reading time. - **Design tokens:** --border, --primary, --primary-fg, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Weekly Newsletter* ```ejs // GET /theme/common/email/marketing/newsletter res.render('theme/common/email/marketing/newsletter', { layout: 'layouts/blank', subject: 'Acme Weekly — Issue #' + newsletter.issue, fromName: 'Acme Corp', fromEmail: 'hello@acme.example.com', toName: subscriber.name, toEmail: subscriber.email, company: { name: 'Acme Corp', address: '...' }, newsletter: { issue: newsletter.issueNumber, date: newsletter.publishedAt.toLocaleDateString(), intro: newsletter.intro, articles: newsletter.articles.map(a => ({ tag: a.category, title: a.title, summary: a.excerpt, url: '/blog/' + a.slug, readTime: a.readingTime + ' min', })), ctaUrl: '/blog', }, }); ``` ### PromotionalEmail (`email-promotional`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/marketing/promotional.ejs` - **Status:** stable (since 2025-05) - **Description:** Limited-time promotion. Gradient header, price comparison, dashed-border coupon code. - **Design tokens:** --border, --primary, --primary-subtle, --secondary, --success, --surface-raised, --text-primary, --text-secondary **Variants:** *Promotional Offer* ```ejs // GET /theme/common/email/marketing/promotional res.render('theme/common/email/marketing/promotional', { layout: 'layouts/blank', subject: promo.discountPct + '% off ' + plan.name + ' — This weekend only', fromName: 'Acme Corp', fromEmail: 'deals@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, promo: { headline: promo.headline, subheadline: promo.subheadline, discountPct: promo.discountPercent, code: promo.couponCode, validUntil: promo.expiresAt.toLocaleString(), originalPrice: plan.price, discountedPrice: plan.price * (1 - promo.discountPercent / 100), currency: plan.currency, shopUrl: '/pricing', features: plan.features, }, }); ``` ### ProductUpdateEmail (`email-product-update`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/marketing/product-update.ejs` - **Status:** stable (since 2025-05) - **Description:** Product release announcement. Version badge, highlighted change cards, and changelog link. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Product Update* ```ejs // GET /theme/common/email/marketing/product-update res.render('theme/common/email/marketing/product-update', { layout: 'layouts/blank', subject: "What's new in Acme Corp v" + release.version, fromName: 'Acme Corp', fromEmail: 'updates@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, update: { version: release.version, date: release.releasedAt.toLocaleDateString(), intro: release.intro, highlights: release.highlights.map(h => ({ icon: h.icon, title: h.title, description: h.summary, })), changelogUrl: '/changelog', learnMoreUrl: '/blog/v' + release.version, }, }); ``` ### MaintenanceEmail (`email-maintenance`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/system/maintenance.ejs` - **Status:** stable (since 2025-05) - **Description:** Advance notice before scheduled maintenance. Time window, affected services, and status page link. - **Design tokens:** --info, --primary, --secondary, --text-primary, --text-secondary, --warning **Variants:** *Scheduled Maintenance* ```ejs // GET /theme/common/email/system/maintenance res.render('theme/common/email/system/maintenance', { layout: 'layouts/blank', subject: 'Scheduled maintenance on ' + maintenance.date, fromName: 'Acme Corp', fromEmail: 'status@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, maintenance: { startTime: maintenance.startAt.toUTCString(), endTime: maintenance.endAt.toUTCString(), duration: maintenance.durationLabel, reason: maintenance.reason, affected: maintenance.affectedServices, statusUrl: 'https://status.acme.example.com', }, }); ``` ### PolicyUpdateEmail (`email-policy-update`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/system/policy-update.ejs` - **Status:** stable (since 2025-05) - **Description:** Privacy policy / terms of service update. Point-by-point summary and effective date. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --primary-subtle, --secondary, --surface-overlay, --text-primary, --text-secondary **Variants:** *Privacy Policy Update* ```ejs // GET /theme/common/email/system/policy-update res.render('theme/common/email/system/policy-update', { layout: 'layouts/blank', subject: 'Important updates to our ' + update.type, fromName: 'Acme Corp', fromEmail: 'legal@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, update: { type: 'Privacy Policy', // or 'Terms of Service' effectiveDate: policy.effectiveDate.toLocaleDateString(), summaryPoints: policy.changes.map(c => c.summary), viewUrl: '/legal/privacy', }, }); ``` ### DataExportEmail (`email-data-export`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/system/data-export.ejs` - **Status:** stable (since 2025-05) - **Description:** Data export ready notice. File size, format, expiration date, and download CTA. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --success, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Data Export Ready* ```ejs // GET /theme/common/email/system/data-export res.render('theme/common/email/system/data-export', { layout: 'layouts/blank', subject: 'Your data export is ready to download', fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, export: { requestedAt: exportJob.requestedAt.toLocaleString(), readyAt: exportJob.completedAt.toLocaleString(), fileSize: formatBytes(exportJob.fileSizeBytes), format: 'ZIP (JSON + CSV)', downloadUrl: exportJob.signedDownloadUrl, // time-limited signed URL expiresAt: exportJob.expiresAt.toLocaleDateString(), }, }); ``` ### AccountDeletionEmail (`email-account-deletion`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/system/account-deletion.ejs` - **Status:** stable (since 2025-05) - **Description:** Account deletion scheduled notice. Grace period, permanent deletion warning, and a cancel CTA. - **Design tokens:** --border, --error, --primary, --primary-fg, --primary-hover, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Account Deletion Scheduled* ```ejs // GET /theme/common/email/system/account-deletion res.render('theme/common/email/system/account-deletion', { layout: 'layouts/blank', subject: 'Your account is scheduled for deletion on ' + deletionDate, fromName: 'Acme Corp', fromEmail: 'noreply@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, deletion: { requestedAt: new Date().toLocaleString(), scheduledAt: deletionDate.toLocaleDateString(), gracePeriodDays: 7, cancelUrl: '/account/cancel-deletion?token=' + token, supportUrl: '/support', }, }); ``` ### TicketOpenedEmail (`email-ticket-opened`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/support/ticket-opened.ejs` - **Status:** stable (since 2025-05) - **Description:** Support request received auto-reply. Ticket ID, subject, status badge, and message preview. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --success, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Ticket Opened* ```ejs // GET /theme/common/email/support/ticket-opened res.render('theme/common/email/support/ticket-opened', { layout: 'layouts/blank', subject: '[Ticket #' + ticket.id + '] Your request has been received', fromName: 'Acme Corp Support', fromEmail: 'support@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, ticket: { id: ticket.id, subject: ticket.subject, description: ticket.body, submittedAt: ticket.createdAt.toLocaleString(), viewUrl: '/support/tickets/' + ticket.id, }, }); ``` ### TicketReplyEmail (`email-ticket-reply`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/support/ticket-reply.ejs` - **Status:** stable (since 2025-05) - **Description:** Reply from the support team. Agent avatar, full reply text, and Reply and View CTA pair. - **Design tokens:** --border, --primary, --primary-fg, --primary-hover, --secondary, --surface-overlay, --surface-raised, --text-primary, --text-secondary **Variants:** *Agent Reply* ```ejs // GET /theme/common/email/support/ticket-reply res.render('theme/common/email/support/ticket-reply', { layout: 'layouts/blank', subject: '[Ticket #' + ticket.id + '] New reply from the support team', fromName: 'Acme Corp Support', fromEmail: 'support@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, ticket: { id: ticket.id, subject: ticket.subject, agentName: reply.author.firstName, agentRole: 'Acme Support', replyText: reply.body, repliedAt: reply.createdAt.toLocaleString(), viewUrl: '/support/tickets/' + ticket.id, replyUrl: '/support/tickets/' + ticket.id + '/reply', }, }); ``` ### TicketResolvedEmail (`email-ticket-resolved`) - **Layer:** theme - **Category:** Email - **File:** `views/theme/common/email/support/ticket-resolved.ejs` - **Status:** stable (since 2025-05) - **Description:** Support ticket resolved notice. Resolution summary, emoji rating, and a reopen option. - **Design tokens:** --border, --error, --primary, --secondary, --success, --surface-raised, --text-primary, --text-secondary, --warning **Variants:** *Ticket Resolved* ```ejs // GET /theme/common/email/support/ticket-resolved res.render('theme/common/email/support/ticket-resolved', { layout: 'layouts/blank', subject: '[Ticket #' + ticket.id + '] Your ticket has been resolved', fromName: 'Acme Corp Support', fromEmail: 'support@acme.example.com', toName: user.name, toEmail: user.email, company: { name: 'Acme Corp', address: '...' }, ticket: { id: ticket.id, subject: ticket.subject, resolvedAt: ticket.resolvedAt.toLocaleString(), resolution: ticket.resolutionSummary, feedbackUrl: '/support/tickets/' + ticket.id + '/feedback', reopenUrl: '/support/tickets/' + ticket.id + '/reopen', viewUrl: '/support/tickets/' + ticket.id, }, }); ``` ## Components — ui / App ### CodeEditor (`code-editor`) - **Layer:** ui - **Category:** App - **File:** `modules/ui/CodeEditor/CodeEditor.ejs` - **Status:** stable (since 2026-05) - **Description:** Engine-agnostic code editor primitive. M1 ships a lightweight CodeMirror-style fallback engine (textarea + line-number gutter + active-line + theme + readonly + placeholder) so the public API is stable today. Future milestones: Monaco (VSCode) lazy engine, find/replace, multi-cursor, diagnostics (markers), custom autocomplete + hover, minimap, code folding, vim/emacs keymap. Pixel-identical React sibling at modules/ui/CodeEditor/index.tsx. Used by RulesetEditor M3 + RichTextEditor code-block insert (planned). - **Design tokens:** --surface-base, --surface-raised, --surface-overlay, --surface-sunken, --text-primary, --text-secondary, --text-disabled, --border, --border-strong, --border-focus, --error - **A11y:** WCAG AA · ARIA: textbox · Keys: Tab → Move focus into / out of the editor; Arrow keys → Move caret + sync active line gutter; Ctrl/Cmd + Z → Browser-native undo (M1 textarea fallback) **Variants:** *JavaScript readonly* ```ejs <%- include('modules/ui/CodeEditor', { id: 'example', label: 'example.js', language: 'js', theme: 'light', value: source, readonly: true, showLineNumbers: true }) %> ``` *Markdown editable* ```ejs <%- include('modules/ui/CodeEditor', { id: 'readme', name: 'readme', label: 'README.md', language: 'markdown', theme: 'dark', value: src, placeholder: 'Start typing markdown…' }) %> ``` ## Components — ui / Atom ### Button (`button`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Button.ejs` - **Status:** stable (since 2025-01) - **Description:** Core interactive element. Supports 5 visual styles (variants) and 5 sizes. disabled, loading and selected states are built-in. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-fg, --primary-hover, --secondary, --secondary-fg, --secondary-hover, --surface-overlay, --text-inverse, --text-primary **Variants:** *Primary* ```ejs <%- include('modules/ui/Button', { children: 'Primary' }) %> ``` *Secondary* ```ejs <%- include('modules/ui/Button', { variant: 'secondary', children: 'Secondary' }) %> ``` *Ghost* ```ejs <%- include('modules/ui/Button', { variant: 'ghost', children: 'Ghost' }) %> ``` *Danger* ```ejs <%- include('modules/ui/Button', { variant: 'danger', children: 'Danger' }) %> ``` *Outline* ```ejs <%- include('modules/ui/Button', { variant: 'outline', children: 'Outline' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Button', { disabled: true, children: 'Disabled' }) %> ``` *Sizes* ```ejs <%- include('modules/ui/Button', { size: 'xs', children: 'XS' }) %> <%- include('modules/ui/Button', { size: 'sm', children: 'SM' }) %> <%- include('modules/ui/Button', { size: 'md', children: 'MD' }) %> <%- include('modules/ui/Button', { size: 'lg', children: 'LG' }) %> <%- include('modules/ui/Button', { size: 'xl', children: 'XL' }) %> ``` *Icon left / right* ```ejs <%- include('modules/ui/Button', { iconLeft: '', children: 'Download' }) %> <%- include('modules/ui/Button', { variant: 'outline', iconRight: '', children: 'Next' }) %> ``` *Icon only* ```ejs <%- include('modules/ui/Button', { ariaLabel: 'Delete item', children: '' }) %> <%- include('modules/ui/Button', { variant: 'outline', ariaLabel: 'Edit item', children: '' }) %> ``` *Selected / active state* ```ejs <%- include('modules/ui/Button', { variant: 'outline', selected: true, children: 'Selected' }) %> <%- include('modules/ui/Button', { variant: 'outline', selected: false, children: 'Default' }) %> ``` *Loading state* ```ejs <%- include('modules/ui/Button', { loading: true, children: 'Saving…' }) %> <%- include('modules/ui/Button', { variant: 'outline', loading: true, children: 'Loading details' }) %> ``` *As Link (element override)* ```ejs <%- include('modules/ui/Button', { element: 'a', href: '/docs', children: 'Go to docs' }) %> <%- include('modules/ui/Button', { element: 'a', href: 'https://example.com', target: '_blank', variant: 'outline', children: 'Open in new tab' }) %> <%- include('modules/ui/Button', { element: 'a', href: '/next', variant: 'secondary', iconRight: '', children: 'Next page' }) %> <%- include('modules/ui/Button', { element: 'a', href: '#', disabled: true, children: 'Disabled link' }) %> ``` *Full width* ```ejs <%- include('modules/ui/Button', { fullWidth: true, children: 'Full-width primary' }) %> <%- include('modules/ui/Button', { variant: 'outline', fullWidth: true, children: 'Full-width outline' }) %> ``` ### ButtonGroup (`button-group`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/ButtonGroup.ejs` - **Status:** stable (since 2025-01) - **Description:** Segmented button group for mutually-exclusive options. Supports 4 variants, 4 sizes and disabled items. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --secondary-fg, --surface-base, --surface-overlay, --text-primary **Variants:** *Outline (default)* ```ejs <%- include('modules/ui/ButtonGroup', { value: 'week', items: [ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }, ], }) %> ``` *Primary* ```ejs <%- include('modules/ui/ButtonGroup', { variant: 'primary', value: 'week', items: [...] }) %> ``` *Secondary* ```ejs <%- include('modules/ui/ButtonGroup', { variant: 'secondary', value: 'week', items: [...] }) %> ``` *Ghost* ```ejs <%- include('modules/ui/ButtonGroup', { variant: 'ghost', value: 'week', items: [...] }) %> ``` *Sizes* ```ejs <%- include('modules/ui/ButtonGroup', { size: 'xs', value: 'a', items: [...] }) %> <%- include('modules/ui/ButtonGroup', { size: 'sm', value: 'a', items: [...] }) %> <%- include('modules/ui/ButtonGroup', { size: 'md', value: 'a', items: [...] }) %> <%- include('modules/ui/ButtonGroup', { size: 'lg', value: 'a', items: [...] }) %> ``` *With disabled item* ```ejs <%- include('modules/ui/ButtonGroup', { value: 'week', items: [ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month', disabled: true }, ], }) %> ``` *Icon-style labels* ```ejs <%- include('modules/ui/ButtonGroup', { value: 'grid', items: [ { value: 'list', label: '' }, { value: 'grid', label: '' }, { value: 'map', label: '' }, ], }) %> ``` ### Badge (`badge`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Badge.ejs` - **Status:** stable (since 2025-01) - **Description:** Status, category or label indicator. 6 semantic variants, 3 sizes, dot and dismissible support. - **Used by:** kanban-board - **Design tokens:** --error, --error-fg, --error-subtle, --info, --info-fg, --info-subtle, --primary, --primary-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-sunken, --text-disabled, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Success* ```ejs <%- include('modules/ui/Badge', { variant: 'success', children: 'Active' }) %> ``` *Error* ```ejs <%- include('modules/ui/Badge', { variant: 'error', children: 'Inactive' }) %> ``` *Warning* ```ejs <%- include('modules/ui/Badge', { variant: 'warning', children: 'Pending' }) %> ``` *Info* ```ejs <%- include('modules/ui/Badge', { variant: 'info', children: 'New' }) %> ``` *Neutral* ```ejs <%- include('modules/ui/Badge', { variant: 'neutral', children: 'Design' }) %> ``` *Primary* ```ejs <%- include('modules/ui/Badge', { variant: 'primary', children: 'Frontend' }) %> ``` *Sizes* ```ejs <%- include('modules/ui/Badge', { variant: 'primary', size: 'sm', children: 'Small' }) %> <%- include('modules/ui/Badge', { variant: 'primary', size: 'md', children: 'Medium' }) %> <%- include('modules/ui/Badge', { variant: 'primary', size: 'lg', children: 'Large' }) %> ``` *Dot badge* ```ejs <%- include('modules/ui/Badge', { variant: 'success', dot: true, children: 'Online' }) %> <%- include('modules/ui/Badge', { variant: 'warning', dot: true, children: 'Away' }) %> <%- include('modules/ui/Badge', { variant: 'error', dot: true, children: 'Busy' }) %> <%- include('modules/ui/Badge', { variant: 'neutral', dot: true, children: 'Offline' }) %> ``` *Dismissible* ```ejs <%- include('modules/ui/Badge', { variant: 'primary', dismissible: true, children: 'React' }) %> <%- include('modules/ui/Badge', { variant: 'primary', dismissible: true, children: 'TypeScript' }) %> ``` ### Avatar (`avatar`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Avatar.ejs` - **Status:** stable (since 2025-01) - **Description:** User profile photo or initials indicator. 5 sizes with optional status dot. When no image is provided, initials render on a bg-primary-subtle / text-primary tile. - **Used by:** kanban-board - **Design tokens:** --border, --error, --primary, --primary-subtle, --secondary, --success, --surface-base, --surface-sunken, --text-disabled, --text-secondary, --warning **Variants:** *Initials (sizes)* ```ejs <%- include('modules/ui/Avatar', { name: 'Jane Doe', size: 'xs' }) %> <%- include('modules/ui/Avatar', { name: 'Jane Doe', size: 'sm' }) %> <%- include('modules/ui/Avatar', { name: 'Jane Doe', size: 'md' }) %> <%- include('modules/ui/Avatar', { name: 'Jane Doe', size: 'lg' }) %> <%- include('modules/ui/Avatar', { name: 'Jane Doe', size: 'xl' }) %> ``` *With label* ```ejs
<%- include('modules/ui/Avatar', { name: 'John Smith' }) %>

John Smith

john@example.com

``` *Status dot* ```ejs <%- include('modules/ui/Avatar', { name: 'Alice', status: 'online' }) %> <%- include('modules/ui/Avatar', { name: 'Bob', status: 'away' }) %> <%- include('modules/ui/Avatar', { name: 'Carol', status: 'busy' }) %> <%- include('modules/ui/Avatar', { name: 'Dave', status: 'offline' }) %> ``` *Image source* ```ejs <%- include('modules/ui/Avatar', { src: '/avatars/jane.jpg', name: 'Jane Doe' }) %> ``` ### Spinner (`spinner`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Spinner.ejs` - **Status:** stable (since 2025-01) - **Description:** CSS border-based loading indicator. Does not require FontAwesome. 5 sizes, border-border / border-t-primary colour system. - **Design tokens:** --border **Variants:** *Sizes* ```ejs <%- include('modules/ui/Spinner', { size: 'xs' }) %> <%- include('modules/ui/Spinner', { size: 'sm' }) %> <%- include('modules/ui/Spinner', { size: 'md' }) %> <%- include('modules/ui/Spinner', { size: 'lg' }) %> <%- include('modules/ui/Spinner', { size: 'xl' }) %> ``` *In a Button* ```ejs <%- include('modules/ui/Button', { variant: 'primary', loading: true, children: 'Saving…' }) %> <%- include('modules/ui/Button', { variant: 'outline', loading: true, children: 'Loading details' }) %> ``` ### Skeleton (`skeleton`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Skeleton.ejs` - **Status:** stable (since 2025-01) - **Description:** Animated placeholder shown before content loads. Uses animate-pulse bg-surface-sunken. aria-busy="true" ensures accessibility. - **Design tokens:** --border, --surface-raised, --surface-sunken **Variants:** *Lines* ```ejs <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-full' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-3/4' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-1/2' }) %> ``` *Text block* ```ejs <%- include('modules/ui/Skeleton', { variant: 'text', lines: 4 }) %> ``` *Card* ```ejs <%- include('modules/ui/Skeleton', { variant: 'card' }) %> ``` *Table rows* ```ejs <%- include('modules/ui/Skeleton', { variant: 'tableRow', cols: 4 }) %> <%- include('modules/ui/Skeleton', { variant: 'tableRow', cols: 4 }) %> <%- include('modules/ui/Skeleton', { variant: 'tableRow', cols: 4 }) %>
``` *Dashboard layout* ```ejs <%# Stat cards + table skeleton %>
<% for (var i = 0; i < 3; i++) { %>
<%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-1/2' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-3/4' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-1/3' }) %>
<% } %>
<%- include('modules/ui/Skeleton', { variant: 'tableRow', cols: 4 }) %>
``` *Article layout* ```ejs <%# Blog post / article skeleton %>
<%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-1/4' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-full' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-3/4' }) %>
<%- include('modules/ui/Skeleton', { variant: 'avatar', size: 'sm' }) %> <%- include('modules/ui/Skeleton', { variant: 'line', width: 'w-24' }) %>
<%- include('modules/ui/Skeleton', { variant: 'text', lines: 4 }) %>
``` ### SkipLink + LiveRegion (`skip-link`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/SkipLink.ejs` - **Status:** stable (since 2025-01) - **Description:** SkipLink is visually hidden until focused, enabling keyboard users to bypass navigation. LiveRegion announces dynamic content to screen readers. - **Design tokens:** --border, --border-focus, --primary, --primary-fg **Variants:** *SkipLink (focus to reveal)* ```ejs <%# Place at top of layout: %> <%- include('modules/ui/SkipLink', { href: '#main-content' }) %> <%# Linked target: %>
...
``` *LiveRegion* ```ejs <%# Polite live region — announces `message` to screen readers %>
<%= message %>
``` ### BrandLogo (`brand-logo`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/BrandLogo.ejs` - **Status:** stable (since 2026-05) - **Description:** Square brand mark with rounded corners. Renders a single letter or short token on a primary-coloured tile. 5 sizes (sm → 2xl). - **Design tokens:** --primary, --primary-fg **Variants:** *Default sizes* ```ejs <%- include('modules/ui/BrandLogo', { size: 'sm', children: 'A' }) %> <%- include('modules/ui/BrandLogo', { size: 'md', children: 'B' }) %> <%- include('modules/ui/BrandLogo', { size: 'lg', children: 'C' }) %> <%- include('modules/ui/BrandLogo', { size: 'xl', children: 'D' }) %> <%- include('modules/ui/BrandLogo', { size: '2xl', children: 'E' }) %> ``` *Custom content* ```ejs <%- include('modules/ui/BrandLogo', { size: 'lg', children: 'KU' }) %> <%- include('modules/ui/BrandLogo', { size: 'lg', className: 'bg-secondary', children: 'N' }) %> <%- include('modules/ui/BrandLogo', { size: 'lg', className: 'bg-success', children: '✓' }) %> ``` ### StarRating (`star-rating`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/StarRating.ejs` - **Status:** stable (since 2026-05) - **Description:** Five-star rating indicator. Read-only by default with decimal/half-star rendering; pass `readonly={false}` + `onChange` for interactive whole-star selection. - **When to use:** Surface a 0–5 star score (e.g. product / hotel / restaurant ratings) or let users submit a new rating. - **When not to use:** For non-star scales (e.g. NPS, percentages) use ContentScoreBar or a custom indicator instead. - **Design tokens:** --warning, --text-disabled, --border-focus - **A11y:** WCAG AA · ARIA: role="img" (readonly), role="radiogroup" / role="radio" (interactive) **Variants:** *Readonly with decimals* ```ejs <%- include('modules/ui/StarRating', { value: 4.7, size: 'sm', caption: '(312 reviews)' }) %> <%- include('modules/ui/StarRating', { value: 3.5, size: 'md' }) %> <%- include('modules/ui/StarRating', { value: 2.2, size: 'lg' }) %> ``` *Interactive* ```ejs <%- include('modules/ui/StarRating', { value: 0, readonly: false, size: 'lg', ariaLabel: 'Pick a rating' }) %> ``` ### Toggle (`toggle`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Toggle.ejs` - **Status:** stable (since 2025-02) - **Description:** role="switch" toggle/switch with three sizes, description slot, and disabled support. Fully accessible via CSS transform without a native input. - **Design tokens:** --border, --primary, --secondary, --surface-sunken, --text-primary, --text-secondary **Variants:** *Checked* ```ejs <%- include('modules/ui/Toggle', { id: 'notif', label: 'Notifications enabled', checked: true }) %> ``` *Unchecked* ```ejs <%- include('modules/ui/Toggle', { id: 'dark', label: 'Dark mode' }) %> ``` *No label* ```ejs <%- include('modules/ui/Toggle', { id: 't', checked: true }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Toggle', { id: 'a', label: 'Enabled (disabled)', checked: true, disabled: true }) %> <%- include('modules/ui/Toggle', { id: 'b', label: 'Disabled option', checked: false, disabled: true }) %> ``` *Sizes* ```ejs <%- include('modules/ui/Toggle', { id: 'sm', label: 'Small', size: 'sm', checked: true }) %> <%- include('modules/ui/Toggle', { id: 'md', label: 'Medium', size: 'md', checked: true }) %> <%- include('modules/ui/Toggle', { id: 'lg', label: 'Large', size: 'lg', checked: true }) %> ``` ### Checkbox (`checkbox`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Checkbox.ejs` - **Status:** stable (since 2025-02) - **Description:** Label + checkbox + optional hint / error message. aria-describedby is wired up and border-error is applied on the error state. - **Design tokens:** --border, --border-focus, --error, --primary, --secondary, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb', label: 'Accept terms and conditions' }) %> ``` *Checked* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb', label: 'Remember me', checked: true }) %> ``` *With hint* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb', label: 'Subscribe to newsletter', hint: 'We send at most one email per week' }) %> ``` *Error state* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb', label: 'Accept terms', hint: 'You must accept the terms', error: true }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb1', label: 'Option A', disabled: true }) %> <%- include('modules/ui/Checkbox', { id: 'cb2', label: 'Option B', checked: true, disabled: true }) %> ``` *Sizes* ```ejs <%- include('modules/ui/Checkbox', { id: 'cb1', label: 'Small', size: 'sm', checked: true }) %> <%- include('modules/ui/Checkbox', { id: 'cb2', label: 'Medium', size: 'md', checked: true }) %> <%- include('modules/ui/Checkbox', { id: 'cb3', label: 'Large', size: 'lg', checked: true }) %> ``` ### Input (`input`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Input.ejs` - **Status:** stable (since 2025-02) - **Description:** Text input field with label, hint, error, prefix icon, password toggle, and 3 size variants. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --secondary, --success, --success-fg, --success-subtle, --surface-base, --surface-overlay, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/Input', { id: 'email', label: 'Email address', placeholder: 'you@example.com' }) %> ``` *Required* ```ejs <%- include('modules/ui/Input', { id: 'name', label: 'Full name', required: true, placeholder: 'Jane Doe' }) %> ``` *With hint* ```ejs <%- include('modules/ui/Input', { id: 'user', label: 'Username', hint: 'Letters, numbers and underscores only' }) %> ``` *Error state* ```ejs <%- include('modules/ui/Input', { id: 'email', label: 'Email address', error: 'Enter a valid email address' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Input', { id: 'acc', label: 'Account ID', disabled: true }) %> ``` *Password* ```ejs <%- include('modules/ui/Input', { id: 'pw', label: 'Password', type: 'password', placeholder: '••••••••' }) %> ``` *Prefix icon* ```ejs <%- include('modules/ui/Input', { id: 'search', label: 'Search', iconLeft: '', placeholder: 'Search…' }) %> ``` *Sizes* ```ejs <%- include('modules/ui/Input', { id: 'sm', label: 'Small', size: 'sm' }) %> <%- include('modules/ui/Input', { id: 'md', label: 'Medium', size: 'md' }) %> <%- include('modules/ui/Input', { id: 'lg', label: 'Large', size: 'lg' }) %> ``` ### Textarea (`textarea`) - **Layer:** ui - **Category:** Atom - **File:** `modules/ui/Textarea.ejs` - **Status:** stable (since 2025-02) - **Description:** Label + textarea + hint + error anatomy. Vertical resizing is enabled via resize-y and the parts are linked through aria-describedby. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --secondary, --surface-base, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/Textarea', { id: 'msg', label: 'Message', placeholder: 'Enter your message…' }) %> ``` *Required* ```ejs <%- include('modules/ui/Textarea', { id: 'desc', label: 'Description', required: true }) %> ``` *With hint* ```ejs <%- include('modules/ui/Textarea', { id: 'bio', label: 'Bio', hint: 'Max 500 characters', placeholder: 'Tell us about yourself' }) %> ``` *Error state* ```ejs <%- include('modules/ui/Textarea', { id: 'notes', label: 'Notes', error: 'This field is required' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Textarea', { id: 'ro', label: 'Read-only notes', disabled: true, value: 'This field cannot be edited.' }) %> ``` *Resize none* ```ejs <%- include('modules/ui/Textarea', { id: 'fixed', label: 'Fixed height', resize: 'none', rows: 4 }) %> ``` *Custom rows* ```ejs <%- include('modules/ui/Textarea', { id: 'long', label: 'Long-form content', rows: 6 }) %> ``` ## Components — ui / Molecule ### SearchBar (`search-bar`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/SearchBar.ejs` - **Status:** stable (since 2025-02) - **Description:** role="searchbox" with search icon and clear button. Works in controlled and uncontrolled modes. - **Design tokens:** --border, --border-focus, --primary, --surface-base, --text-disabled, --text-primary **Variants:** *Default* ```ejs <%- include('modules/ui/SearchBar', { placeholder: 'Search..' }) %> ``` *With value (clear button)* ```ejs <%- include('modules/ui/SearchBar', { value: 'Button' }) %> ``` *Small size* ```ejs <%- include('modules/ui/SearchBar', { size: 'sm', placeholder: 'Search…' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/SearchBar', { placeholder: 'Search is disabled', disabled: true }) %> ``` ### Select (`select`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/Select.ejs` - **Status:** stable (since 2025-02) - **Description:** Label + select + hint + error anatomy. appearance-none overrides the native dropdown style and renders a chevron icon. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --secondary, --surface-base, --surface-overlay, --surface-raised, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/Select', { label: 'Role', placeholder: 'Select a role…', options: [ { value: 'admin', label: 'Admin' }, { value: 'editor', label: 'Editor' }, { value: 'viewer', label: 'Viewer' }, ] }) %> ``` *With hint & selected value* ```ejs <%- include('modules/ui/Select', { label: 'Role', hint: 'Determines access level.', value: 'editor', options: ROLES }) %> ``` *Error state* ```ejs <%- include('modules/ui/Select', { label: 'Plan', placeholder: 'Select a plan', required: true, error: 'Please select a plan.', options: PLANS }) %> ``` *Disabled* ```ejs <%- include('modules/ui/Select', { label: 'Plan', disabled: true, value: 'editor', options: ROLES }) %> ``` ### RadioGroup (`radio-group`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/RadioGroup.ejs` - **Status:** stable (since 2025-02) - **Description:** fieldset + legend based radio group. WCAG-compliant keyboard navigation with an optional card-style variant. - **Design tokens:** --border, --border-focus, --error, --primary, --secondary, --surface-base, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/RadioGroup', { name: 'notify', legend: 'Notification preference', options: [ { value: 'email', label: 'Email', hint: 'Sent to your primary email' }, { value: 'sms', label: 'SMS' }, { value: 'none', label: 'None' }, ] }) %> ``` *With selected value* ```ejs <%- include('modules/ui/RadioGroup', { name: 'notify', legend: 'Notification preference', value: 'email', options: NOTIFY_OPTS }) %> ``` *Card style* ```ejs <%- include('modules/ui/RadioGroup', { name: 'plan', legend: 'Choose plan', value: 'pro', cardStyle: true, options: [ { value: 'free', label: 'Free', hint: '$0/mo · 3 projects' }, { value: 'pro', label: 'Pro', hint: '$12/mo · Unlimited' }, { value: 'team', label: 'Team', hint: '$49/mo · 10 seats' }, ] }) %> ``` *Disabled* ```ejs <%- include('modules/ui/RadioGroup', { name: 'notify', legend: 'Notification preference', value: 'email', disabled: true, options: NOTIFY_OPTS }) %> ``` ### CheckboxGroup (`checkbox-group`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/CheckboxGroup.ejs` - **Status:** stable (since 2025-02) - **Description:** Chip-style multi-select group. Selected chips use bg-primary-subtle / border-primary tokens. Keyboard accessible. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-subtle, --surface-base, --surface-overlay, --text-primary **Variants:** *Default* ```ejs <%- include('modules/ui/CheckboxGroup', { legend: 'Tech stack', options: [ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'typescript', label: 'TypeScript' }, ], selected: ['react', 'typescript'] }) %> ``` *Disabled* ```ejs <%- include('modules/ui/CheckboxGroup', { legend: 'Permissions', options: [ { value: 'read', label: 'Read' }, { value: 'write', label: 'Write' }, { value: 'delete', label: 'Delete' }, ], selected: ['read'], disabled: true }) %> ``` *Empty selection* ```ejs <%- include('modules/ui/CheckboxGroup', { legend: 'Tags', options: [ { value: 'design', label: 'Design' }, { value: 'frontend', label: 'Frontend' }, { value: 'backend', label: 'Backend' }, ], selected: [] }) %> ``` ### MultiSelect (`multi-select`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/ComboBox/MultiSelect.ejs` - **Status:** stable (since 2026-05) - **Description:** Chip-based multi-select popover with searchable filter, keyboard navigation, and disabled-option support. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --primary-fg, --primary-subtle, --secondary, --surface-base, --surface-overlay, --surface-raised, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Default with selections* ```ejs <%- include('modules/ui/MultiSelect', { label: 'Skills', options: [ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'typescript', label: 'TypeScript' }, ], value: ['react', 'typescript'], hint: 'Pick the technologies you know.' }) %> ``` *Searchable open* ```ejs <%- include('modules/ui/MultiSelect', { label: 'Skills', options: SKILLS, value: ['react'], searchable: true }) %> ``` *Empty placeholder* ```ejs <%- include('modules/ui/MultiSelect', { label: 'Roles', options: ROLES, placeholder: 'Choose roles…' }) %> ``` *Error state* ```ejs <%- include('modules/ui/MultiSelect', { label: 'Roles', options: ROLES, error: 'Please pick at least one role.' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/MultiSelect', { label: 'Skills', options: SKILLS, value: ['react', 'typescript'], disabled: true }) %> ``` ### ComboBox (`combo-box`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/ComboBox/ComboBox.ejs` - **Status:** beta (since 2026-05) - **Description:** Searchable autocomplete single-select with keyboard navigation, described options, and a clearable button. - **Design tokens:** --border, --error, --error-subtle, --primary, --secondary, --surface-overlay, --surface-sunken, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/ComboBox', { label: 'Country', placeholder: 'Search countries…', hint: 'Start typing to filter.', options: [ { value: 'tr', label: 'Türkiye', description: 'TR · +90' }, { value: 'de', label: 'Germany', description: 'DE · +49' }, { value: 'us', label: 'United States', description: 'US · +1' }, ] }) %> ``` *With selected value* ```ejs <%- include('modules/ui/ComboBox', { label: 'Country', value: 'tr', options: COUNTRIES }) %> ``` *Open with highlight* ```ejs <%- include('modules/ui/ComboBox', { label: 'Framework', value: 'next', options: FRAMEWORKS }) %> ``` *Error state* ```ejs <%- include('modules/ui/ComboBox', { label: 'Country', required: true, error: 'Please select a country.', options: COUNTRIES }) %> ``` *Async loading (debounced)* ```ejs <%- include('modules/ui/ComboBox', { id: 'cb-async', label: 'Search', options: [], placeholder: 'Type to search…', hint: 'Debounced 300ms, AbortController cancels in-flight.' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/ComboBox', { label: 'Country', value: 'de', disabled: true, options: COUNTRIES }) %> ``` ### DateRangePicker (`date-range-picker`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/DatePicker/DateRangePicker.ejs` - **Status:** stable (since 2026-05) - **Description:** Two-month popover for picking a start → end date range. Shares the same Calendar core as DatePicker; locale-aware, fully keyboard navigable, with min/max/disabledDates. Pixel-identical React sibling at modules/ui/DatePicker/index.tsx. - **Design tokens:** --error, --primary, --secondary, --text-primary, --text-secondary **Variants:** *Default (TR locale)* ```ejs <%- include('modules/ui/DateRangePicker', { label: 'Raporlama dönemi', hint: 'Bitiş tarihi başlangıçtan sonra olmalı.' }) %> ``` *With value (EN locale)* ```ejs <%- include('modules/ui/DateRangePicker', { label: 'Booking window', locale: 'en', value: { start: '2026-06-01', end: '2026-06-15' } }) %> ``` *Required + error* ```ejs <%- include('modules/ui/DateRangePicker', { label: 'Campaign dates', locale: 'en', required: true, error: 'Please pick both dates.' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/DateRangePicker', { label: 'Locked range', locale: 'en', value: { start: '2026-01-01', end: '2026-01-31' }, disabled: true }) %> ``` *Locale: Türkçe + custom messages* ```ejs <%- include('modules/ui/DateRangePicker', { label: 'Tatil tarihleri', locale: 'tr', value: { start: '2026-07-01', end: '2026-07-14' }, clear_label: 'Temizle' }) %> ``` ### TagInput (`tag-input`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/TagInput.ejs` - **Status:** stable (since 2026-05) - **Description:** Free-text input that creates chips. Add tags with Enter or comma, double-click to edit, Backspace to delete. Duplicates are ignored. - **Design tokens:** --border, --border-focus, --error, --error-subtle, --primary, --primary-subtle, --secondary, --surface-base, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *With initial tags* ```ejs <%- include('modules/ui/TagInput', { label: 'Tags', value: ['design', 'frontend', 'accessibility'] }) %> ``` *Empty with hint* ```ejs <%- include('modules/ui/TagInput', { label: 'Keywords', hint: 'Press Enter or comma to add a keyword.' }) %> ``` *Error state* ```ejs <%- include('modules/ui/TagInput', { label: 'Tags', value: ['only-one'], error: 'Please add at least 3 tags.' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/TagInput', { label: 'Locked tags', value: ['react', 'typescript'], disabled: true }) %> ``` ### DatePicker (`date-picker`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/DatePicker/DatePicker.ejs` - **Status:** stable (since 2025-02) - **Description:** Popover-based date picker with a locale-aware calendar grid (TR / EN), quick month / year jump from the header, min / max / disabledDates support, and full keyboard navigation (Arrow / PageUp/Down / Shift+Page / Home / End / Enter / Esc). Pixel-identical React sibling at modules/ui/DatePicker/index.tsx. - **Design tokens:** --error, --primary, --secondary, --text-primary, --text-secondary **Variants:** *Default (TR locale)* ```ejs <%- include('modules/ui/DatePicker', { label: 'Randevu tarihi', hint: 'İleri bir tarih seçin.' }) %> ``` *With value* ```ejs <%- include('modules/ui/DatePicker', { label: 'Start date', locale: 'en', value: '2026-06-15' }) %> ``` *Error state* ```ejs <%- include('modules/ui/DatePicker', { label: 'Due date', locale: 'en', required: true, error: 'Please select a date.' }) %> ``` *Disabled* ```ejs <%- include('modules/ui/DatePicker', { label: 'Locked date', locale: 'en', value: '2026-01-01', disabled: true }) %> ``` *With min / max* ```ejs <%- include('modules/ui/DatePicker', { label: 'Booking date', locale: 'en', hint: 'Available: Jun 1–30, 2026', min: '2026-06-01', max: '2026-06-30' }) %> ``` *Locale: Türkçe + custom messages* ```ejs <%- include('modules/ui/DatePicker', { label: 'Randevu tarihi', locale: 'tr', value: '2026-05-26', clear_label: 'Temizle' }) %> ``` ### FileInput (`file-input`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/FileInput.ejs` - **Status:** stable (since 2025-02) - **Description:** Drag-and-drop file upload with validation, file list, and individual remove actions. **Variants:** *Single file* ```ejs <%- include('modules/ui/FileInput', { label: 'Profile photo', hint: 'PNG or JPG, max 2 MB', accept: 'image/*' }) %> ``` *Multiple files* ```ejs <%- include('modules/ui/FileInput', { label: 'Attachments', hint: 'Up to 5 MB each', multiple: true }) %> ``` *Disabled* ```ejs <%- include('modules/ui/FileInput', { label: 'Disabled upload', disabled: true }) %> ``` *With error* ```ejs <%- include('modules/ui/FileInput', { label: 'Document', hint: 'PDF only', accept: '.pdf', error: 'Please upload a valid PDF file.' }) %> ``` ### ColorPicker (`color-picker`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/ColorPicker.ejs` - **Status:** stable (since 2026-05) - **Description:** Color selection control with a 32-swatch preset palette plus optional hex input and native browser color picker for unlimited colors. Pixel-identical React sibling at modules/ui/ColorPicker.tsx. Used by RichTextEditor for text + highlight colors. **Variants:** *Default* ```ejs <%- include('modules/ui/ColorPicker', { id: 'brand', name: 'brand', label: 'Brand color', value: '#3b82f6', showNoColor: true }) %> ``` *Compact (swatches only)* ```ejs <%- include('modules/ui/ColorPicker', { id: 'compact', value: '#22c55e', showHexInput: false, showNativePicker: false }) %> ``` *Hex + native picker only (no swatches)* ```ejs <%- include('modules/ui/ColorPicker', { id: 'bg', label: 'Background', swatches: [], showHexInput: true, showNativePicker: true, showNoColor: true }) %> ``` ### DiffViewer (`diff-viewer`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/DiffViewer/DiffViewer.ejs` - **Status:** stable (since 2026-05) - **Description:** Line-based text diff viewer with unified (GitHub-style) and split (yan yana) modes. M1 ships a zero-dep LCS algorithm, hunk headers in `@@ -old,n +new,n @@` form, old/new line-number gutters, configurable context window, and optional collapsible unchanged regions. Pixel-identical React sibling at modules/ui/DiffViewer/index.tsx. **Variants:** *Unified (default)* ```ejs <%- include('modules/ui/DiffViewer', { id: 'review', oldText: oldSrc, newText: newSrc }) %> ``` *Split (yan yana)* ```ejs <%- include('modules/ui/DiffViewer', { id: 'review', oldText: oldSrc, newText: newSrc, mode: 'split' }) %> ``` *With context=1* ```ejs <%- include('modules/ui/DiffViewer', { id: 'review', oldText: oldSrc, newText: newSrc, context: 1 }) %> ``` *Collapsible unchanged context* ```ejs <%- include('modules/ui/DiffViewer', { id: 'review', oldText: oldSrc, newText: newSrc, context: 3, collapsible: true }) %> ``` ### MapView (`map-view`) - **Layer:** ui - **Category:** Molecule - **File:** `modules/ui/MapView/MapView.ejs` - **Status:** beta (since 2025-04) - **Description:** Leaflet-based interactive map. Tooltip-enabled markers, predefined zones (polygon), route lines (polyline), and click-to-add marker mode. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --primary-hover, --surface-overlay, --surface-raised, --text-primary **Variants:** *Tam özellik — işaretçi + zone + rota* ```ejs <%- include('modules/ui/MapView/MapView', { center: [41.015, 28.979], zoom: 6, height: 400, markers: CITIES, zones: ZONES, routes: ROUTES, }) %> ``` *Tıkla-ekle işaretçi modu* ```ejs <%- include('modules/ui/MapView/MapView', { center: [39.5, 35.0], zoom: 5, height: 360, }) %> ``` *Yalnız zone ve rota* ```ejs <%- include('modules/ui/MapView/MapView', { center: [39.5, 35.0], zoom: 5, height: 360, zones: ZONES, routes: ROUTES, }) %> ``` ## Components — ui / Organism ### Card (`card`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Card.ejs` - **Status:** stable (since 2025-02) - **Description:** Content container with raised / flat / outline variants. Supports title, subtitle, headerRight, footer slots and loading skeleton state. - **Design tokens:** --border, --border-focus, --primary, --secondary, --surface-base, --surface-raised, --surface-sunken, --text-primary, --text-secondary **Variants:** *Default (raised)* ```ejs <%- include('modules/ui/Card', { title: 'Card title', subtitle: 'Optional subtitle', children: '

Card body content goes here.

', footer: '
...
' }) %> ``` *Flat* ```ejs <%- include('modules/ui/Card', { title: 'Flat card', variant: 'flat', children: '

No shadow, transparent background.

' }) %> ``` *Outline* ```ejs <%- include('modules/ui/Card', { title: 'Outline card', variant: 'outline', children: '

Border only, no background fill.

' }) %> ``` *Hoverable* ```ejs <%- include('modules/ui/Card', { title: 'Hoverable card', hoverable: true, children: '

Hover to see the shadow lift effect.

' }) %> ``` *Loading skeleton* ```ejs <%- include('modules/ui/Card', { loading: true }) %> ``` *With headerRight* ```ejs <%- include('modules/ui/Card', { title: 'Recent activity', subtitle: 'Last 7 days', headerRight: 'Live', children: '

Activity feed content here.

' }) %> ``` ### AlertBanner (`alert-banner`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/AlertBanner.ejs` - **Status:** stable (since 2025-02) - **Description:** Page-level info, success, warning or error message. Announced via role="alert" for screen readers with optional dismissible and action support. - **Design tokens:** --border, --border-focus, --error, --error-fg, --error-subtle, --info, --info-fg, --info-subtle, --success, --success-fg, --success-subtle, --warning, --warning-fg, --warning-subtle **Variants:** *Info* ```ejs <%- include('modules/ui/AlertBanner', { variant: 'info', title: 'Heads up', message: 'You can update your preferences at any time.' }) %> ``` *Success* ```ejs <%- include('modules/ui/AlertBanner', { variant: 'success', title: 'Changes saved', message: 'Your profile has been updated successfully.' }) %> ``` *Warning* ```ejs <%- include('modules/ui/AlertBanner', { variant: 'warning', title: 'Subscription expiring', message: 'Your plan expires in 3 days.', actionLabel: 'Renew plan' }) %> ``` *Error* ```ejs <%- include('modules/ui/AlertBanner', { variant: 'error', title: 'Payment failed', message: 'We could not charge your card.', actionLabel: 'Update billing', actionHref: '/billing' }) %> ``` *Dismissible* ```ejs <%- include('modules/ui/AlertBanner', { variant: 'info', message: 'This alert can be dismissed.', dismissible: true }) %> ``` ### Toast (`toast`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Toast/Toast.ejs` - **Status:** stable (since 2025-02) - **Description:** Notification system with success/warning/error/info/loading variants. Hover-to-freeze, progress bar, title, actions, and promise support. **Variants:** *Success* ```ejs <%- include('modules/ui/Toast', { variant: 'success', title: 'File uploaded', message: 'report.pdf has been uploaded successfully.' }) %> ``` *Error* ```ejs <%- include('modules/ui/Toast', { variant: 'error', title: 'Upload failed', message: 'The file exceeds the 10 MB size limit.', actionLabel: 'Try again' }) %> ``` *Warning* ```ejs <%- include('modules/ui/Toast', { variant: 'warning', message: 'Session expires in 5 minutes.' }) %> ``` *Info* ```ejs <%- include('modules/ui/Toast', { variant: 'info', title: 'New update', message: 'Version 2.4 is available. Refresh to apply.' }) %> ``` *Loading* ```ejs <%- include('modules/ui/Toast', { variant: 'loading', message: 'Saving your changes…', persistent: true }) %> ``` *toast.promise() API* ```ejs <%# Mount once — store.js exposes window.toast() %> <%- include('modules/ui/Toast/scripts/store.js') %> ``` ### Pagination (`pagination`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Pagination.ejs` - **Status:** stable (since 2025-02) - **Description:** Page navigation control. Collapses large page counts with ellipsis; accessible via aria-label and aria-current="page". - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --surface-base, --surface-overlay, --text-disabled, --text-primary, --text-secondary **Variants:** *Default (page 3 of 10)* ```ejs <%- include('modules/ui/Pagination', { page: 3, totalPages: 10 }) %> ``` *With first/last buttons* ```ejs <%- include('modules/ui/Pagination', { page: 5, totalPages: 12, showFirstLast: true }) %> ``` *First page (prev disabled)* ```ejs <%- include('modules/ui/Pagination', { page: 1, totalPages: 8 }) %> ``` *Small size* ```ejs <%- include('modules/ui/Pagination', { page: 2, totalPages: 6, size: 'sm' }) %> ``` *Large size* ```ejs <%- include('modules/ui/Pagination', { page: 4, totalPages: 7, size: 'lg' }) %> ``` ### TabGroup (`tab-group`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/TabGroup.ejs` - **Status:** stable (since 2025-02) - **Description:** Accessible tab navigation following the role="tablist" / role="tab" / role="tabpanel" ARIA pattern. Arrow-key navigation; tabIndex=-1 on inactive tabs. - **Design tokens:** --border, --border-focus, --primary, --secondary, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/TabGroup', { tabs: [ { id: 'overview', label: 'Overview', content: '

Overview panel content.

' }, { id: 'details', label: 'Details', content: '

Details panel content.

' }, { id: 'history', label: 'History', content: '

History panel content.

' }, ] }) %> ``` *With icons* ```ejs <%- include('modules/ui/TabGroup', { tabs: [ { id: 'home', label: 'Home', icon: 'fa-house', content: '

Home content.

' }, { id: 'users', label: 'Users', icon: 'fa-users', content: '

Users content.

' }, { id: 'settings', label: 'Settings', icon: 'fa-gear', content: '

Settings content.

' }, ] }) %> ``` *With disabled tab* ```ejs <%- include('modules/ui/TabGroup', { tabs: [ { id: 'active', label: 'Active', content: '

Active tab content.

' }, { id: 'disabled', label: 'Disabled', content: '', disabled: true }, { id: 'another', label: 'Another', content: '

Another tab content.

' }, ] }) %> ``` ### Breadcrumb (`breadcrumb`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Breadcrumb.ejs` - **Status:** stable (since 2025-02) - **Description:** Hierarchical navigation trail wrapped in nav aria-label="Breadcrumb". Last item marked with aria-current="page" and aria-hidden separators. - **Design tokens:** --border, --border-focus, --primary, --secondary, --text-disabled, --text-primary, --text-secondary **Variants:** *Default* ```ejs <%- include('modules/ui/Breadcrumb', { items: [ { label: 'Home', href: '/' }, { label: 'Products', href: '/products' }, { label: 'Laptop Pro 15' } ] }) %> ``` *Single level* ```ejs <%- include('modules/ui/Breadcrumb', { items: [ { label: 'Home', href: '/' }, { label: 'Settings' } ] }) %> ``` *Deep hierarchy* ```ejs <%- include('modules/ui/Breadcrumb', { items: [ { label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }, { label: 'Components', href: '/docs/components' }, { label: 'UI', href: '/docs/components/ui' }, { label: 'Button' } ] }) %> ``` ### Stepper (`stepper`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Stepper.ejs` - **Status:** stable (since 2025-02) - **Description:** Multi-step progress indicator with complete, active, error, and pending states. Supports horizontal and vertical orientations. - **Design tokens:** --border, --error, --error-fg, --primary, --primary-fg, --secondary, --success, --surface-base, --text-disabled, --text-inverse, --text-primary, --text-secondary **Variants:** *Horizontal* ```ejs <%- include('modules/ui/Stepper', { steps: [ { label: 'Account', state: 'complete' }, { label: 'Profile', state: 'active' }, { label: 'Payment', state: 'pending' }, { label: 'Confirm', state: 'pending' }, ] }) %> ``` *Vertical* ```ejs <%- include('modules/ui/Stepper', { orientation: 'vertical', steps: [ { label: 'Create account', description: 'Enter your email and password.', state: 'complete' }, { label: 'Verify email', description: 'Check your inbox.', state: 'complete' }, { label: 'Set up profile', description: 'Add your name and photo.', state: 'active' }, { label: 'Invite your team', description: 'Optional.', state: 'pending' }, ] }) %> ``` *With error state* ```ejs <%- include('modules/ui/Stepper', { steps: [ { label: 'Upload', state: 'complete' }, { label: 'Validate', state: 'error' }, { label: 'Process', state: 'pending' }, ] }) %> ``` ### Modal (`modal`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Modal.ejs` - **Status:** stable (since 2025-02) - **Description:** Focus-trapped dialog. Closes on Escape and backdrop click. Requires role="dialog" + aria-modal + aria-labelledby; supports sm/md/lg sizes. **Variants:** *Default (md)* ```ejs <%- include('modules/ui/Modal', { id: 'confirm-modal', title: 'Confirm action', description: 'This action cannot be undone.', children: '

Are you sure you want to delete this item?

', footer: '' }) %> ``` *Small (sm)* ```ejs <%- include('modules/ui/Modal', { id: 'quick-note', title: 'Quick note', size: 'sm', children: '

A compact modal for short confirmations.

', footer: '' }) %> ``` *Large (lg)* ```ejs <%- include('modules/ui/Modal', { id: 'edit-profile', title: 'Edit profile', size: 'lg', children: '...', footer: '...' }) %> ``` *No footer* ```ejs <%- include('modules/ui/Modal', { id: 'shortcuts', title: 'Keyboard shortcuts', description: 'Press ESC to close at any time.', children: '...' }) %> ``` ### Drawer (`drawer`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Drawer.ejs` - **Status:** stable (since 2025-02) - **Description:** Side panel sliding in from the screen edge. Left / right placement with focus management and Escape close. **Variants:** *Right drawer* ```ejs <%- include('modules/ui/Drawer', { id: 'notif-drawer', title: 'Notifications', side: 'right', children: '...' }) %> ``` *Left drawer* ```ejs <%- include('modules/ui/Drawer', { id: 'nav-drawer', title: 'Navigation', side: 'left', children: '...' }) %> ``` *With footer* ```ejs <%- include('modules/ui/Drawer', { id: 'edit-drawer', title: 'Edit item', children: '...', footer: '
...
' }) %> ``` ### Table (`table`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Table/partials/plain.ejs` - **Status:** stable (since 2025-02) - **Description:** Responsive table. scope="col" headers, hover row highlight, empty-state message, and custom cell render support. **Variants:** *Default* ```ejs <%- include('modules/ui/Table', { columns: [ { key: 'name', header: 'Name' }, { key: 'role', header: 'Role' }, { key: 'status', header: 'Status' }, { key: 'joined', header: 'Joined', align: 'right' }, ], rows: users }) %> ``` *Compact — 2 columns* ```ejs <%- include('modules/ui/Table', { columns: [ { key: 'key', header: 'Setting' }, { key: 'value', header: 'Value', align: 'right' }, ], rows: settings }) %> ``` *Empty state* ```ejs <%- include('modules/ui/Table', { columns: [...], rows: [], emptyMessage: 'No users found. Invite someone to get started.' }) %> ``` ### DataTable (`data-table`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Table/partials/paginated.ejs` - **Status:** stable (since 2025-03) - **Description:** Table + SearchBar + Pagination in a single component. Client-side search and pagination with filtered result counter and rows-per-page selector. **Variants:** *With title & pagination* ```ejs <%- include('modules/ui/DataTable', { title: 'Users', subtitle: total + ' total', columns: [ { key: 'name', header: 'Name' }, { key: 'role', header: 'Role' }, { key: 'status', header: 'Status' }, { key: 'joined', header: 'Joined', align: 'right' }, ], rows: users, page: page, // current page (1-based), from req.query total: total, // total record count, from DB pageSize: 20, qs: qs, // query string without 'page', from route }) %> <%-- In your Express route: --%> <% // const page = parseInt(req.query.page) || 1; // const { users, total } = await UserService.list({ page, pageSize: 20 }); // const qs = new URLSearchParams(req.query); qs.delete('page'); // res.render('users/index', { users, total, page, qs: qs.toString() }); %> ``` *Clickable rows* ```ejs <%- include('modules/ui/DataTable', { columns: [...], rows: users, page: page, total: total, pageSize: 20, qs: qs, getRowHref: function(row) { return '/users/' + row.userId; } }) %> ``` *Custom cell render* ```ejs <%- include('modules/ui/DataTable', { columns: [ { key: 'name', header: 'User', render: function(row) { return '
' + '' + row.name.charAt(0).toUpperCase() + '' + '' + row.name + '' + '
'; } }, { key: 'status', header: 'Status', render: function(row) { var v = row.status === 'Active' ? 'bg-success-subtle text-success-fg' : row.status === 'Pending' ? 'bg-warning-subtle text-warning-fg' : 'bg-surface-overlay text-text-secondary'; return '' + row.status + ''; } }, ], rows: users, page: page, total: total, pageSize: 20, qs: qs, }) %> ``` *Empty state* ```ejs <%- include('modules/ui/DataTable', { columns: [...], rows: [], page: 1, total: 0, pageSize: 20, qs: qs, emptyMessage: 'No users found.' }) %> ``` ### VideoPlayer (`video-player`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/VideoPlayer/VideoPlayer.ejs` - **Status:** beta (since 2025-04) - **Description:** Custom HTML5 video player. Quality, subtitle, audio track, and playback rate selection; custom WebVTT subtitle overlay; auto-hiding controls; programmatic API. Keyboard shortcuts: Space/K=play, ←→=±10s, ↑↓=volume, M=mute, F=fullscreen. - **Design tokens:** --border, --border-focus **Variants:** *Tam özellikli* ```ejs <%- include('modules/ui/VideoPlayer/VideoPlayer', { src: 'https://placeholdervideo.dev/1920x1080', poster: 'https://example.com/poster.jpg', title: 'Big Buck Bunny', qualities: [ { label: '1080p', value: 'https://example.com/video-1080p.mp4' }, { label: '720p', value: 'https://example.com/video-720p.mp4' }, { label: '480p', value: 'https://example.com/video-480p.mp4' }, ], defaultQuality: 'https://example.com/video-1080p.mp4', subtitles: [ { label: 'Türkçe', srclang: 'tr', src: '/subtitles/tr.vtt' }, { label: 'English', srclang: 'en', src: '/subtitles/en.vtt' }, ], audioTracks: [ { label: 'Türkçe' }, { label: 'English' }, ], }) %> ``` *Yalnızca altyazı* ```ejs <%- include('modules/ui/VideoPlayer/VideoPlayer', { src: 'https://example.com/lecture.mp4', title: 'Lecture — Episode 1', subtitles: [ { label: 'Türkçe', srclang: 'tr', src: '/subtitles/tr.vtt' }, ], }) %> ``` *Minimal (kontroller her zaman görünür)* ```ejs <%- include('modules/ui/VideoPlayer/VideoPlayer', { src: 'https://placeholdervideo.dev/1920x1080', autoHideControls: false, }) %> ``` *Otomatik oynat + sessiz* ```ejs <%- include('modules/ui/VideoPlayer/VideoPlayer', { src: 'https://example.com/promo.mp4', autoPlay: true, startMuted: true, loop: true, poster: 'https://example.com/poster.jpg', }) %> ``` ### Slider (`slider`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Slider.ejs` - **Status:** stable (since 2026-05) - **Description:** Accessible carousel. Includes role="region" + aria-roledescription="carousel" and per-slide aria labels. Supports autoplay, arrow keys, and dot navigation. **Variants:** *Default* ```ejs <%- include('modules/ui/Slider', { slides: [ "
Slide 1
", "
Slide 2
", "
Slide 3
", ], }) %> ``` *Auto-play* ```ejs <%- include('modules/ui/Slider', { slides: [ /* ... */ ], autoPlay: true, autoPlayInterval: 2000, }) %> ``` *No arrows / no loop* ```ejs <%- include('modules/ui/Slider', { slides: [ /* ... */ ], showArrows: false, loop: false, }) %> ``` ### StatCard (`stat-card`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/StatCard.ejs` - **Status:** stable (since 2026-05) - **Description:** Compact metric display card with value, label, and optional accent color. - **Design tokens:** --border, --primary, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Variants* ```ejs <%- include('modules/ui/StatCard', { label: 'Total Users', value: 1284 }) %> <%- include('modules/ui/StatCard', { label: 'Active', value: 947, accent: 'text-success' }) %> <%- include('modules/ui/StatCard', { label: 'Transferred', value: 38, accent: 'text-info' }) %> <%- include('modules/ui/StatCard', { label: 'Cancelled', value: 12, accent: 'text-error' }) %> ``` *Default (no accent)* ```ejs <%- include('modules/ui/StatCard', { label: 'Sessions', value: 1842 }) %> <%- include('modules/ui/StatCard', { label: 'Bounce rate', value: '24%' }) %> ``` ### TabButton (`tab-button`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/TabButton.ejs` - **Status:** stable (since 2026-05) - **Description:** Pill-style tab button with active/inactive coloring and an optional count badge. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --surface-overlay, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *With counts* ```ejs <%- include('modules/ui/TabButton', { active: true, children: 'All', count: 42 }) %> <%- include('modules/ui/TabButton', { active: false, children: 'Active', count: 18 }) %> <%- include('modules/ui/TabButton', { active: false, children: 'Archived', count: 24 }) %> ``` *Without count* ```ejs <%- include('modules/ui/TabButton', { active: true, children: 'Selected' }) %> <%- include('modules/ui/TabButton', { active: false, children: 'Default' }) %> ``` ### ViewToggle (`view-toggle`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/ViewToggle.ejs` - **Status:** stable (since 2026-05) - **Description:** Horizontal / vertical view toggle control; two-state icon selector. Emits a viewtoggle:change CustomEvent. - **Design tokens:** --border, --border-focus, --primary, --primary-fg, --secondary, --surface-raised, --text-primary, --text-secondary **Variants:** *Default (EN labels)* ```ejs <%- include('modules/ui/ViewToggle', { value: 'horizontal' }) %> ``` *Custom labels (TR)* ```ejs <%- include('modules/ui/ViewToggle', { value: 'vertical', ariaLabel: 'Görünüm seçenekleri', labels: { horizontal: 'Yatay', vertical: 'Dikey' }, }) %> ``` ### EmptyState (`empty-state`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/EmptyState.ejs` - **Status:** stable (since 2026-05) - **Description:** Empty-state message shown when there is no data. Supports icon, title, description, and action slots. - **Design tokens:** --primary, --secondary, --surface-sunken, --text-disabled, --text-primary, --text-secondary **Variants:** *Title only* ```ejs <%- include('modules/ui/EmptyState', { title: 'No results' }) %> ``` *With icon and description* ```ejs <%- include('modules/ui/EmptyState', { title: 'No items yet', description: 'Get started by creating your first item.', icon: '' }) %> ``` *With primary action* ```ejs <%- include('modules/ui/EmptyState', { title: 'Your inbox is empty', description: 'New messages will appear here as soon as they arrive.', icon: '', action: '' }) %> ``` *Search empty* ```ejs <%- include('modules/ui/EmptyState', { title: 'No matches found', description: 'Try adjusting your search or filter.', icon: '', action: '' }) %> ``` ### Tooltip (`tooltip`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Tooltip.ejs` - **Status:** stable (since 2026-05) - **Description:** Short hint shown on hover and focus. Accessible via role="tooltip" + aria-describedby. Supports 4 placements, 3 themes, optional arrow, and configurable delay. - **Design tokens:** --border, --primary, --surface-overlay, --text-primary **Variants:** *Placements* ```ejs <%- include('modules/ui/Tooltip', { content: 'Top tooltip', placement: 'top', children: '' }) %> <%- include('modules/ui/Tooltip', { content: 'Bottom tooltip', placement: 'bottom', children: '' }) %> <%- include('modules/ui/Tooltip', { content: 'Right tooltip', placement: 'right', children: '' }) %> ``` *Themes* ```ejs <%- include('modules/ui/Tooltip', { content: 'Default theme', theme: 'default', children: '' }) %> <%- include('modules/ui/Tooltip', { content: 'Dark theme', theme: 'dark', children: '' }) %> <%- include('modules/ui/Tooltip', { content: 'Light theme', theme: 'light', children: '' }) %> ``` *With arrow* ```ejs <%- include('modules/ui/Tooltip', { content: 'Has an arrow', placement: 'top', arrow: true, children: '' }) %> ``` *On an icon button* ```ejs <%- include('modules/ui/Tooltip', { content: 'Open settings', placement: 'top', theme: 'dark', children: '' }) %> ``` ### DropdownMenu (`dropdown-menu`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/DropdownMenu.ejs` - **Status:** stable (since 2026-05) - **Description:** Accessible dropdown using role="menu" + role="menuitem". Closes on Escape and outside click. Supports left/right alignment, icons, separators, danger and disabled items, and arrow-key navigation. - **Design tokens:** --border, --error, --error-subtle, --primary, --surface-overlay, --surface-raised, --text-primary **Variants:** *Default (left aligned)* ```ejs <%- include('modules/ui/DropdownMenu', { id: 'actions-menu', trigger: '', items: [ { label: 'View', icon: '' }, { label: 'Edit', icon: '' }, { label: 'Duplicate', icon: '' } ] }) %> ``` *With separator & danger* ```ejs <%- include('modules/ui/DropdownMenu', { id: 'row-menu', align: 'right', trigger: '', items: [ { label: 'Open', icon: '...' }, { label: 'Rename', icon: '...' }, { label: 'Archive', icon: '...', disabled: true }, { type: 'separator' }, { label: 'Delete', icon: '...', danger: true } ] }) %> ``` *With header* ```ejs <%- include('modules/ui/DropdownMenu', { id: 'account-menu', trigger: '', header: '
...
', items: [ { label: 'Profile', icon: '...' }, { label: 'Settings', icon: '...' }, { type: 'separator' }, { label: 'Sign out', icon: '...', danger: true } ] }) %> ``` ### Popover (`popover`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Popover.ejs` - **Status:** stable (since 2026-05) - **Description:** Anchor-based contextual panel. Closes on outside click and Escape key. Supports top/bottom/left/right placement. **Variants:** *Info popover (bottom)* ```ejs <%- include('modules/ui/Popover', { id: 'pro-info', trigger: '', placement: 'bottom', children: '
...
' }) %> ``` *Inline form (right)* ```ejs <%- include('modules/ui/Popover', { id: 'note-popover', trigger: '', placement: 'right', children: '
...
' }) %> ``` *List menu (top)* ```ejs <%- include('modules/ui/Popover', { id: 'account-popover', trigger: '', placement: 'top', children: '
    ...
' }) %> ``` ### AdvancedDataTable (`advanced-data-table`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Table/partials/advanced.ejs` - **Status:** beta (since 2026-05) - **Description:** Enhanced table with row selection (with indeterminate header), expandable rows, and optional sticky header. **Variants:** *Basic (no extras)* ```ejs <%- include('modules/ui/AdvancedDataTable', { columns: [ { key: 'name', header: 'Name' }, { key: 'role', header: 'Role' }, { key: 'status', header: 'Status' } ], rows: [ { name: 'Jane Doe', role: 'Designer', status: 'Active' }, { name: 'Carlos Mendes', role: 'Engineer', status: 'Active' }, { name: 'Aisha Khan', role: 'PM', status: 'Invited' } ] }) %> ``` *Selectable rows* ```ejs <%- include('modules/ui/AdvancedDataTable', { selectable: true, columns: [...], rows: [...] }) %> ``` *Expandable rows* ```ejs <%- include('modules/ui/AdvancedDataTable', { columns: [...], rows: [ { name: 'Jane Doe', role: 'Designer', status: 'Active', _expanded: '
Joined in 2023. Platform working group.
' }, { name: 'Carlos Mendes', role: 'Engineer', status: 'Active' } ] }) %> ``` *Selectable + sticky header* ```ejs <%- include('modules/ui/AdvancedDataTable', { selectable: true, stickyHeader: true, columns: [...], rows: [...] }) %> ``` ### ServerDataTable (`server-data-table`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/Table/partials/server.ejs` - **Status:** beta (since 2026-05) - **Description:** Server-side paginated table. Reads page/totalPages/total/pageSize from the Express route; supports title, toolbar, clickable rows, and empty/loading states. **Variants:** *With title & pagination* ```ejs <%- include('modules/ui/ServerDataTable', { title: 'Orders', subtitle: total + ' total', columns: [ { key: 'orderId', header: 'Order' }, { key: 'customer', header: 'Customer' }, { key: 'status', header: 'Status' }, { key: 'total', header: 'Total', align: 'right' }, ], rows: orders, page: page, totalPages: totalPages, total: total, pageSize: 20, }) %> <%-- In your Express route: --%> <% // const page = parseInt(req.query.page) || 1; // const { orders, total } = await OrderService.list({ page, pageSize: 20 }); // res.render('orders/index', { orders, total, page, totalPages: Math.ceil(total / 20) }); %> ``` *Clickable rows + header action* ```ejs <%- include('modules/ui/ServerDataTable', { title: 'Orders', headerRight: '+ New order', columns: [...], rows: orders, page: page, totalPages: totalPages, total: total, pageSize: 20, getRowHref: function(row) { return '/orders/' + row.orderId.replace('#', ''); } }) %> ``` *Loading* ```ejs <%- include('modules/ui/ServerDataTable', { title: 'Orders', columns: [...], rows: [], loading: true }) %> ``` *Empty state* ```ejs <%- include('modules/ui/ServerDataTable', { title: 'Orders', columns: [...], rows: [], page: 1, totalPages: 1, total: 0, pageSize: 20, emptyMessage: 'No orders yet. Your first sale will appear here.' }) %> ``` ### TreeView (`tree-view`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/TreeView/TreeView.ejs` - **Status:** stable (since 2026-05) - **Description:** Collapsible tree with keyboard navigation, selection, and aria-tree roles. - **Design tokens:** --border, --border-focus, --secondary, --surface-overlay, --text-secondary **Variants:** *File tree* ```ejs <%- include('modules/ui/TreeView', { label: 'Files', selectedId: selectedId, nodes: [ { id: 'src', label: 'src', children: [ { id: 'components', label: 'components', children: [ { id: 'Button', label: 'Button.ejs' }, { id: 'Card', label: 'Card.ejs' }, ]}, { id: 'routes', label: 'routes', children: [ { id: 'index', label: 'index.ts' }, { id: 'users', label: 'users.ts' }, ]}, ]}, { id: 'package', label: 'package.json' }, ] }) %> ``` *Navigation menu* ```ejs <%- include('modules/ui/TreeView', { label: 'Settings navigation', selectedId: 'billing', nodes: [ { id: 'account', label: 'Account', children: [ { id: 'profile', label: 'Profile' }, { id: 'password', label: 'Password' }, ]}, { id: 'workspace', label: 'Workspace', children: [ { id: 'general', label: 'General' }, { id: 'billing', label: 'Billing' }, ]}, { id: 'integrations', label: 'Integrations' }, ] }) %> ``` *Flat list* ```ejs <%- include('modules/ui/TreeView', { label: 'Language selector', selectedId: 'ts', nodes: [ { id: 'ts', label: 'TypeScript' }, { id: 'js', label: 'JavaScript' }, { id: 'py', label: 'Python' }, { id: 'go', label: 'Go' }, ] }) %> ``` *Multi-select + type-ahead* ```ejs <%- include('modules/ui/TreeView', { label: 'Project files', selectionMode: 'multi', selectedIds: ['Card'], nodes: [ { id: 'docs', label: 'Documents', children: [ { id: 'spec', label: 'spec.md' }, { id: 'roadmap', label: 'roadmap.md' }, ]}, { id: 'src', label: 'src', children: [ { id: 'Button', label: 'Button.ejs' }, { id: 'Card', label: 'Card.ejs' }, { id: 'Drawer', label: 'Drawer.ejs' }, { id: 'TreeView', label: 'TreeView.ejs' }, ]}, ] }) %> <%# Try it: % - Cmd/Ctrl-click → toggle individual rows % - Shift-click → range-select between anchor and clicked row % - Type "tre" → focus jumps to "TreeView.ejs" % - Cmd/Ctrl+A → select all visible rows % - Arrow keys / Home / End / Space / Enter — full keyboard nav. %> ``` ### ContentScoreBar (`content-score-bar`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/ContentScoreBar.ejs` - **Status:** stable (since 2026-05) - **Description:** Rule-based content quality score with Good ≥70 / Fair ≥40 / Poor <40 tier system. Each rule shown as a chip with passed/total count. role="progressbar" + aria-valuenow. - **Design tokens:** --border, --error, --error-fg, --error-subtle, --secondary, --success, --success-fg, --success-subtle, --surface-sunken, --text-disabled, --text-secondary, --warning, --warning-fg, --warning-subtle **Variants:** *Live evaluation* ```ejs <%# Evaluate rules in your Express route, then pass score + results. %> <% // const value = req.body.content || ''; // const rules = [ // { label: 'Min 20 chars', check: function (v) { return v.length >= 20; }, points: 20 }, // { label: 'Has keyword', check: function (v) { return /react/i.test(v); }, points: 20, hint: 'Include "React"' }, // // ... // ]; // let earned = 0, total = 0; // const results = rules.map(function (r) { // const pass = r.check(value); if (pass) earned += r.points; total += r.points; // return { label: r.label, pass: pass, hint: r.hint }; // }); // const score = total > 0 ? Math.round((earned / total) * 100) : 0; %> <%- include('modules/ui/ContentScoreBar', { label: 'Quality score', score: score, results: results }) %> ``` *All tiers* ```ejs <%# Good tier (score ≥ 70) %> <%- include('modules/ui/ContentScoreBar', { score: 100, label: 'Good (100%)', results: allPassRules }) %> <%# Fair tier (40 ≤ score < 70) %> <%- include('modules/ui/ContentScoreBar', { score: 60, label: 'Fair (60%)', results: halfPassRules }) %> <%# Poor tier (score < 40) %> <%- include('modules/ui/ContentScoreBar', { score: 20, label: 'Poor (20%)', results: onePassRules }) %> ``` *Password strength* ```ejs <%# Server-side password rule evaluation (mirror of NextJS preview). %> <% // const rules = [ // { label: 'Min 8 chars', check: function (v) { return v.length >= 8; }, points: 25 }, // { label: 'Uppercase', check: function (v) { return /[A-Z]/.test(v); }, points: 25 }, // { label: 'Number', check: function (v) { return /\d/.test(v); }, points: 25 }, // { label: 'Special char', check: function (v) { return /[^A-Za-z0-9]/.test(v); }, points: 25 }, // ]; %> <%- include('modules/ui/ContentScoreBar', { label: 'Password strength', score: score, results: results }) %> ``` ### PageHeader (`page-header`) - **Layer:** ui - **Category:** Organism - **File:** `modules/ui/PageHeader.ejs` - **Status:** stable (since 2026-05) - **Description:** Page title + subtitle + optional badge + action buttons. Supports 5 button variants (primary/secondary/outline/danger/ghost); rendered as a link with href or as a button. - **Design tokens:** --border, --border-focus, --error, --primary, --primary-fg, --primary-hover, --secondary, --secondary-fg, --secondary-hover, --surface-overlay, --text-inverse, --text-primary, --text-secondary **Variants:** *With actions* ```ejs <%- include('modules/ui/PageHeader', { title: 'Users', subtitle: 'Manage your team members and their permissions.', badge: '48 members', actions: [ { label: 'Export', variant: 'outline' }, { label: '+ Invite user', variant: 'primary', href: '/users/invite' }, ] }) %> ``` *Danger action* ```ejs <%- include('modules/ui/PageHeader', { title: 'Danger Zone', subtitle: 'Irreversible actions.', actions: [ { label: 'Archive', variant: 'outline' }, { label: 'Delete project', variant: 'danger' }, ] }) %> ``` *Minimal* ```ejs <%- include('modules/ui/PageHeader', { title: 'Settings', subtitle: 'Configure your workspace preferences.' }) %> ``` ## Themes | ID | Title | Route | Status | Since | |---|---|---|---|---| | `theme-common` | Common Theme | `/theme/common` | stable | 2025-04 | | `theme-api-doc` | API Doc Theme | `/theme/api-doc` | stable | 2025-05 | | `theme-email` | Email Templates | `/theme/common/email` | stable | 2025-05 | | `theme-modem` | Modem Theme | `/theme/modem` | stable | 2025-05 | | `theme-invoice` | Invoice Theme | `/theme/invoice` | stable | 2025-05 | | `theme-ups` | UPS Theme | `/theme/ups` | stable | 2025-05 |