Files
Maxim Dolgolyov e4ec9f8823 feat(design-system): comprehensive showcase page at /design-system
Standalone discoverable hub of all LearnSpace design tokens, components,
patterns, motion, accessibility primitives, and icons.

Sections:
- Foundations: colors (WCAG ratios), typography, spacing, radii,
  shadows, blur
- Components: buttons, inputs, badges, chips, cards, modal, toast,
  skeleton, empty-states, avatars (all with state variants)
- Patterns: stat-card, data-table, search-bar, sidebar nav, tabs, hero,
  bento, hover-row-actions
- Motion: transitions, shimmer, prefers-reduced-motion toggle
- Accessibility: focus rings, touch targets, live contrast checker
- Icons: top 50 Lucide names with click-to-copy
- Anti-patterns: 4x don't vs do examples

Interactive: click-to-copy on swatches/icons/classes, search filter
across sections, code snippets via <details> blocks per component,
LS.modal/toast live triggers with standalone fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:58:23 +03:00

1574 lines
80 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LearnSpace Design System</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/ls.css">
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
/* ── DS page shell ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Manrope', sans-serif;
background: var(--bg);
background-image: radial-gradient(circle, rgba(15,23,42,0.04) 1px, transparent 1px);
background-size: 22px 22px;
color: var(--text);
min-height: 100vh;
}
/* ── Top header ── */
.ds-header {
position: sticky; top: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 0 24px;
height: 56px;
background: rgba(238,242,255,0.92);
backdrop-filter: blur(20px);
border-bottom: 1.5px solid var(--border);
}
.ds-logo {
font-family: 'Unbounded', sans-serif;
font-size: 0.92rem; font-weight: 800;
color: var(--text); white-space: nowrap;
text-decoration: none;
}
.ds-logo span { background: var(--grad-1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.ds-tag { font-size: 0.72rem; font-weight: 600; color: var(--text-3); background: rgba(155,93,229,0.1); border: 1px solid rgba(155,93,229,0.2); border-radius: var(--r-pill); padding: 3px 10px; white-space: nowrap; }
.ds-search {
flex: 1; max-width: 320px;
padding: 7px 14px;
border: 1.5px solid var(--border-h); border-radius: var(--r-pill);
background: rgba(255,255,255,0.7);
font-family: 'Manrope', sans-serif; font-size: 0.84rem; color: var(--text);
transition: border-color var(--tr), box-shadow var(--tr);
}
.ds-search:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px rgba(155,93,229,0.15); }
/* ── Layout ── */
.ds-layout { display: flex; min-height: calc(100vh - 56px); }
/* ── Sidebar ── */
.ds-nav {
width: 220px; flex-shrink: 0;
position: sticky; top: 56px;
height: calc(100vh - 56px);
overflow-y: auto; overflow-x: hidden;
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.3) transparent;
background: rgba(238,242,255,0.94);
border-right: 1.5px solid var(--border);
padding: 16px 10px;
}
.ds-nav-group { font-size: 0.63rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3); padding: 8px 10px 4px; margin-top: 8px; }
.ds-nav-group:first-child { margin-top: 0; }
.ds-nav-link {
display: flex; align-items: center; gap: 8px;
padding: 7px 10px; border-radius: 10px;
font-size: 0.8rem; font-weight: 600; color: var(--text-2);
text-decoration: none; cursor: pointer; border: none; background: transparent;
width: 100%; text-align: left; font-family: 'Manrope', sans-serif;
transition: all var(--tr);
}
.ds-nav-link:hover { background: rgba(155,93,229,0.08); color: var(--text); }
.ds-nav-link.active { background: rgba(155,93,229,0.12); color: var(--violet); font-weight: 700; }
/* ── Main ── */
.ds-main { flex: 1; min-width: 0; padding: 32px 40px 80px; }
/* ── Section ── */
.ds-section { margin-bottom: 64px; }
.ds-section.hidden { display: none; }
.ds-section-title {
font-family: 'Unbounded', sans-serif;
font-size: 1.4rem; font-weight: 800;
color: var(--text); margin-bottom: 6px;
scroll-margin-top: 72px;
}
.ds-section-desc { font-size: 0.88rem; color: var(--text-2); margin-bottom: 24px; line-height: 1.6; }
.ds-divider { height: 1.5px; background: var(--border); margin: 28px 0; }
/* ── Demo card ── */
.ds-card {
background: rgba(255,255,255,0.85);
border: 1.5px solid var(--border);
border-radius: var(--r-lg);
padding: 24px;
box-shadow: var(--shadow);
}
.ds-card-label { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); margin-bottom: 16px; }
/* ── Code block ── */
details.ds-code { margin-top: 12px; }
details.ds-code summary {
font-size: 0.76rem; font-weight: 700; color: var(--violet);
cursor: pointer; padding: 6px 0; user-select: none;
list-style: none; display: flex; align-items: center; gap: 6px;
}
details.ds-code summary::before { content: ''; display: inline-block; width: 0; height: 0; border: 4px solid transparent; border-left: 6px solid var(--violet); transition: transform 0.15s; }
details.ds-code[open] summary::before { transform: rotate(90deg); }
details.ds-code pre {
background: #0f172a; color: #e2e8f0;
border-radius: var(--r-md); padding: 16px;
font-size: 0.78rem; line-height: 1.6; overflow-x: auto;
font-family: 'Courier New', monospace; margin-top: 8px;
position: relative;
}
.ds-copy-btn {
position: absolute; top: 8px; right: 8px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
color: #94a3b8; border-radius: 6px; padding: 3px 8px;
font-size: 0.7rem; cursor: pointer; font-family: 'Manrope', sans-serif;
transition: all 0.15s;
}
.ds-copy-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
/* ── Row / grid helpers ── */
.ds-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; }
.ds-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
/* ── Color swatch ── */
.swatch {
border-radius: var(--r-md); overflow: hidden;
border: 1.5px solid var(--border);
cursor: pointer; transition: transform 0.15s, box-shadow 0.15s;
}
.swatch:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.swatch-color { height: 72px; width: 100%; }
.swatch-info { padding: 8px 10px; background: #fff; }
.swatch-var { font-size: 0.72rem; font-weight: 700; color: var(--text); font-family: 'Courier New', monospace; }
.swatch-hex { font-size: 0.68rem; color: var(--text-3); margin-top: 2px; }
/* ── Spacing ruler ── */
.space-row { display: flex; align-items: center; gap: 12px; margin: 6px 0; }
.space-bar { height: 24px; background: var(--grad-1); border-radius: 4px; transition: width 0.3s; }
.space-label { font-size: 0.75rem; font-weight: 700; color: var(--text-2); min-width: 80px; font-family: 'Courier New', monospace; }
.space-px { font-size: 0.72rem; color: var(--text-3); }
/* ── Radius demo ── */
.radius-box { width: 80px; height: 80px; background: var(--grad-1); display: flex; align-items: center; justify-content: center; flex-direction: column; color: #fff; }
.radius-label { font-size: 0.62rem; font-weight: 700; margin-top: 4px; }
/* ── Shadow demo ── */
.shadow-card { background: #fff; border-radius: var(--r-lg); padding: 24px; text-align: center; font-size: 0.8rem; font-weight: 600; color: var(--text-2); min-width: 140px; }
/* ── Blur demo ── */
.blur-demo { position: relative; border-radius: var(--r-lg); overflow: hidden; height: 120px; background: linear-gradient(135deg, #9B5DE5, #06D6E0, #06D664); display: flex; align-items: center; justify-content: center; }
.blur-panel { padding: 16px 24px; border-radius: var(--r-md); font-size: 0.85rem; font-weight: 700; color: var(--text); }
.blur-panel.frosted { background: rgba(238,242,255,0.55); backdrop-filter: blur(20px); border: 1.5px solid rgba(255,255,255,0.4); }
.blur-panel.plain { background: rgba(238,242,255,0.95); }
/* ── Button demo states ── */
.btn-states { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.touch-target-wrap { position: relative; display: inline-flex; }
.touch-target-box { position: absolute; inset: -2px; border: 1.5px dashed rgba(155,93,229,0.4); border-radius: 4px; pointer-events: none; }
/* ── Form demo ── */
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-label { font-size: 0.78rem; font-weight: 700; color: var(--text-2); }
.form-error { font-size: 0.74rem; color: var(--danger); margin-top: 2px; }
.form-input.error { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(241,91,181,0.12); }
/* ── Chip ── */
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 12px; border-radius: var(--r-pill);
font-size: 0.78rem; font-weight: 700;
background: rgba(155,93,229,0.1); color: var(--violet);
border: 1.5px solid rgba(155,93,229,0.2); cursor: pointer;
transition: all var(--tr);
}
.chip:hover, .chip.active { background: rgba(155,93,229,0.18); border-color: var(--violet); }
.chip-x { background: none; border: none; cursor: pointer; color: inherit; padding: 0; display: flex; line-height: 1; }
.chip-kpi { background: rgba(255,255,255,0.9); border: 1.5px solid var(--border); border-radius: var(--r-pill); padding: 5px 14px 5px 10px; display: inline-flex; align-items: center; gap: 8px; }
.chip-kpi-icon { width: 28px; height: 28px; border-radius: 8px; background: var(--grad-1); display: flex; align-items: center; justify-content: center; }
.chip-kpi-label { font-size: 0.72rem; color: var(--text-3); font-weight: 600; }
.chip-kpi-value { font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 800; color: var(--text); }
/* ── Cards demo ── */
.ds-surface-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 20px; box-shadow: var(--shadow); }
.ds-glass-card { background: rgba(255,255,255,0.35); backdrop-filter: var(--blur); border: 1.5px solid rgba(255,255,255,0.5); border-radius: var(--r-lg); padding: 20px; }
.ds-bar-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 20px; position: relative; overflow: hidden; box-shadow: var(--shadow); }
.ds-bar-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--grad-1); }
.ds-hero-card { background: linear-gradient(135deg, #0f0c29, #1a1547); border-radius: var(--r-lg); padding: 28px; color: #fff; }
.ds-hero-kpi { font-family: 'Unbounded', sans-serif; font-size: 2.2rem; font-weight: 800; margin: 8px 0 4px; }
/* ── Avatar ── */
.avatar-pill {
width: 36px; height: 36px; border-radius: 10px;
display: inline-flex; align-items: center; justify-content: center;
font-family: 'Unbounded', sans-serif; font-size: 0.62rem; font-weight: 800; color: #fff;
flex-shrink: 0;
}
/* ── Stat card ── */
.stat-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 18px 20px; box-shadow: var(--shadow); position: relative; overflow: hidden; }
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
.stat-card.vc::before { background: var(--grad-1); }
.stat-card.gc::before { background: linear-gradient(135deg, var(--green), var(--cyan)); }
.stat-card.ac::before { background: linear-gradient(135deg, var(--amber), var(--pink)); }
.stat-val { font-family: 'Unbounded', sans-serif; font-size: 1.8rem; font-weight: 800; color: var(--text); margin: 6px 0 2px; }
.stat-label { font-size: 0.78rem; font-weight: 600; color: var(--text-3); }
.stat-delta { font-size: 0.72rem; font-weight: 700; color: var(--success); }
/* ── Data table ── */
.ds-table-wrap { overflow-x: auto; border-radius: var(--r-lg); border: 1.5px solid var(--border); }
.ds-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
.ds-table thead th { background: rgba(155,93,229,0.06); padding: 11px 14px; text-align: left; font-weight: 700; color: var(--text-2); font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.05em; position: sticky; top: 0; }
.ds-table tbody tr { border-top: 1px solid var(--border); transition: background 0.12s; }
.ds-table tbody tr:hover { background: rgba(155,93,229,0.04); }
.ds-table tbody td { padding: 11px 14px; color: var(--text); }
.ds-table .row-actions { display: none; gap: 6px; }
.ds-table tbody tr:hover .row-actions { display: flex; }
/* ── Tabs ── */
.ds-tabs { display: flex; gap: 4px; background: rgba(155,93,229,0.07); border-radius: var(--r-pill); padding: 4px; width: fit-content; }
.ds-tab { padding: 7px 18px; border-radius: var(--r-pill); font-size: 0.82rem; font-weight: 700; color: var(--text-2); cursor: pointer; border: none; background: transparent; font-family: 'Manrope', sans-serif; transition: all 0.18s; }
.ds-tab:hover { color: var(--text); }
.ds-tab.active { background: #fff; color: var(--violet); box-shadow: 0 1px 6px rgba(15,23,42,0.10); }
/* ── Search bar ── */
.search-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.search-wrap { position: relative; flex: 1; min-width: 200px; }
.search-wrap svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-3); pointer-events: none; }
.search-wrap input { padding-left: 36px; }
/* ── Bento grid ── */
.bento { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: auto auto; gap: 14px; }
.bento-cell { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 20px; box-shadow: var(--shadow); min-height: 100px; }
.bento-cell.wide { grid-column: span 2; }
.bento-cell.tall { grid-row: span 2; }
/* ── Motion demo cards ── */
.motion-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 20px; text-align: center; cursor: pointer; transition: transform 0.22s var(--ease-spring), box-shadow 0.22s ease; }
.motion-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: var(--shadow-h); }
/* ── A11y focus demo ── */
.focus-demo-btn { padding: 10px 20px; border: 2px solid var(--border); border-radius: var(--r-md); background: var(--surface); font-family: 'Manrope', sans-serif; font-size: 0.86rem; font-weight: 600; cursor: pointer; color: var(--text); transition: all var(--tr); }
.focus-demo-btn:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
/* ── Icon grid ── */
.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
.icon-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 8px; border-radius: var(--r-md); border: 1.5px solid var(--border); background: var(--surface); cursor: pointer; transition: all 0.15s; font-size: 0.64rem; font-weight: 600; color: var(--text-2); text-align: center; word-break: break-all; }
.icon-cell:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.06); }
/* ── Anti-pattern ── */
.ap-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.ap-card { border-radius: var(--r-md); padding: 16px; font-size: 0.82rem; }
.ap-bad { background: rgba(241,91,181,0.07); border: 1.5px solid rgba(241,91,181,0.25); }
.ap-good { background: rgba(6,214,100,0.07); border: 1.5px solid rgba(6,214,100,0.25); }
.ap-label { font-weight: 800; font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; margin-bottom: 8px; }
.ap-bad .ap-label { color: var(--danger); }
.ap-good .ap-label { color: var(--success); }
.ap-code { font-family: 'Courier New', monospace; font-size: 0.78rem; line-height: 1.5; }
/* ── Contrast checker ── */
.contrast-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; }
.contrast-box { width: 80px; height: 48px; border-radius: var(--r-md); border: 1.5px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 0.82rem; font-weight: 700; }
.wcag-badge { font-size: 0.68rem; font-weight: 800; padding: 2px 6px; border-radius: 4px; }
.wcag-pass { background: rgba(6,214,100,0.15); color: #059950; }
.wcag-fail { background: rgba(241,91,181,0.15); color: #c0306a; }
/* ── Hero header demo ── */
.hero-header { padding: 24px 28px; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow); }
.hero-greeting { font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800; color: var(--text); margin-bottom: 16px; }
/* ── Reduced motion toggle ── */
.rm-toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.82rem; font-weight: 600; color: var(--text-2); }
.rm-toggle input { width: 36px; height: 20px; cursor: pointer; }
/* ── Copy toast (inline) ── */
#ds-copy-flash { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%); background: #0f172a; color: #fff; border-radius: var(--r-pill); padding: 8px 18px; font-size: 0.8rem; font-weight: 600; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 9999; }
#ds-copy-flash.show { opacity: 1; }
/* ── Responsive ── */
@media (max-width: 900px) {
.ds-nav { display: none; }
.ds-main { padding: 24px 20px 60px; }
.bento { grid-template-columns: 1fr 1fr; }
.bento-cell.wide { grid-column: span 2; }
.ap-grid { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
.bento { grid-template-columns: 1fr; }
.bento-cell.wide { grid-column: span 1; }
}
</style>
</head>
<body>
<!-- Copy flash -->
<div id="ds-copy-flash">Copied!</div>
<!-- Header -->
<header class="ds-header">
<a class="ds-logo" href="/design-system">Learn<span>Space</span></a>
<span class="ds-tag">Design System v1.0 · light theme</span>
<input class="ds-search" type="search" placeholder="Filter sections..." id="ds-search" aria-label="Filter design system sections">
</header>
<div class="ds-layout">
<!-- Sidebar nav -->
<nav class="ds-nav" aria-label="Design system sections">
<div class="ds-nav-group">Foundations</div>
<a class="ds-nav-link" href="#colors">Colors</a>
<a class="ds-nav-link" href="#typography">Typography</a>
<a class="ds-nav-link" href="#spacing">Spacing</a>
<a class="ds-nav-link" href="#radii">Radii</a>
<a class="ds-nav-link" href="#shadows">Shadows</a>
<a class="ds-nav-link" href="#blur">Blur</a>
<div class="ds-nav-group">Components</div>
<a class="ds-nav-link" href="#buttons">Buttons</a>
<a class="ds-nav-link" href="#inputs">Form Inputs</a>
<a class="ds-nav-link" href="#badges">Badges</a>
<a class="ds-nav-link" href="#chips">Chips</a>
<a class="ds-nav-link" href="#cards">Cards</a>
<a class="ds-nav-link" href="#modal">Modal</a>
<a class="ds-nav-link" href="#toast">Toast</a>
<a class="ds-nav-link" href="#skeleton">Skeleton</a>
<a class="ds-nav-link" href="#empty-state">Empty State</a>
<a class="ds-nav-link" href="#avatar">Avatar</a>
<div class="ds-nav-group">Patterns</div>
<a class="ds-nav-link" href="#stat-card">Stat Card</a>
<a class="ds-nav-link" href="#data-table">Data Table</a>
<a class="ds-nav-link" href="#search-bar">Search + Filter</a>
<a class="ds-nav-link" href="#sidebar-nav">Sidebar Nav</a>
<a class="ds-nav-link" href="#tabs">Tabs</a>
<a class="ds-nav-link" href="#hero-header">Hero Header</a>
<a class="ds-nav-link" href="#bento">Bento Grid</a>
<a class="ds-nav-link" href="#hover-row">Hover Row Actions</a>
<div class="ds-nav-group">Motion</div>
<a class="ds-nav-link" href="#motion">Transitions</a>
<div class="ds-nav-group">Accessibility</div>
<a class="ds-nav-link" href="#a11y">Focus &amp; Targets</a>
<a class="ds-nav-link" href="#contrast">Contrast Checker</a>
<div class="ds-nav-group">Reference</div>
<a class="ds-nav-link" href="#icons">Icons (Lucide 50)</a>
<a class="ds-nav-link" href="#anti-patterns">Anti-Patterns</a>
</nav>
<!-- Main content -->
<main class="ds-main">
<!-- ══ COLORS ══ -->
<section class="ds-section" id="colors" data-title="Colors">
<h2 class="ds-section-title">Colors</h2>
<p class="ds-section-desc">Brand palette, semantic aliases, and text hierarchy. Click any swatch to copy the CSS variable.</p>
<div class="ds-card">
<div class="ds-card-label">Brand</div>
<div class="ds-grid" id="brand-swatches"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">Semantic Aliases</div>
<div class="ds-row" id="semantic-swatches"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">Text &amp; Surface</div>
<div class="ds-row" id="text-swatches"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">WCAG Contrast Preview</div>
<div id="contrast-preview"></div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>/* Brand */
--violet: #9B5DE5;
--cyan: #06D6E0;
--green: #06D664;
--pink: #F15BB5;
--amber: #FFB347;
/* Semantic */
--success: var(--green);
--warning: var(--amber);
--danger: var(--pink);
--info: var(--cyan);</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ TYPOGRAPHY ══ -->
<section class="ds-section" id="typography" data-title="Typography">
<h2 class="ds-section-title">Typography</h2>
<p class="ds-section-desc">Type scale, font families, and weight ladder. Unbounded: display/headings/KPI numbers. Manrope: everything else.</p>
<div class="ds-card">
<div class="ds-card-label">Type Scale</div>
<div id="type-scale"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">Font Families</div>
<div class="ds-row" style="gap:32px">
<div>
<div style="font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin-bottom:8px">Manrope — body</div>
<div style="font-family:'Manrope',sans-serif;font-size:1.1rem">The quick brown fox</div>
<div style="font-family:'Manrope',sans-serif;font-size:0.82rem;color:var(--text-2);margin-top:4px">Body text, labels, inputs</div>
</div>
<div>
<div style="font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin-bottom:8px">Unbounded — display</div>
<div style="font-family:'Unbounded',sans-serif;font-size:1.1rem;font-weight:800">The quick brown fox</div>
<div style="font-family:'Manrope',sans-serif;font-size:0.82rem;color:var(--text-2);margin-top:4px">Headings, KPI numbers, logo</div>
</div>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Weight Ladder</div>
<div id="weight-scale"></div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--text-xs: 0.72rem; /* 11.5px */
--text-sm: 0.82rem; /* 13px */
--text-base: 0.92rem; /* 14.7px */
--text-md: 1.02rem; /* 16.3px */
--text-lg: 1.18rem; /* 18.9px */
--text-xl: 1.5rem; /* 24px */
--text-2xl: 2rem; /* 32px */
--text-3xl: 2.6rem; /* 41.6px */
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--fw-extrabold: 800;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ SPACING ══ -->
<section class="ds-section" id="spacing" data-title="Spacing">
<h2 class="ds-section-title">Spacing</h2>
<p class="ds-section-desc">4px base scale. Use tokens instead of hardcoded values for consistent rhythm.</p>
<div class="ds-card">
<div id="spacing-ruler"></div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ RADII ══ -->
<section class="ds-section" id="radii" data-title="Radii">
<h2 class="ds-section-title">Radii</h2>
<p class="ds-section-desc">Border-radius ladder from sharp to pill. Click to copy.</p>
<div class="ds-card">
<div class="ds-row" id="radii-demo"></div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--r-xs: 4px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 20px;
--r-xl: 24px;
--r-pill: 999px;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ SHADOWS ══ -->
<section class="ds-section" id="shadows" data-title="Shadows">
<h2 class="ds-section-title">Shadows</h2>
<p class="ds-section-desc">Two-layer shadows: crisp ambient + lifted. Use <code>--shadow-h</code> on hover.</p>
<div class="ds-row">
<div class="shadow-card" style="border:1.5px solid var(--border)">No shadow</div>
<div class="shadow-card" style="box-shadow:var(--shadow)">--shadow</div>
<div class="shadow-card" style="box-shadow:var(--shadow-h)">--shadow-h</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--shadow: 0 2px 8px rgba(15,23,42,0.08), 0 8px 40px rgba(15,23,42,0.10);
--shadow-h: 0 4px 16px rgba(15,23,42,0.12), 0 16px 56px rgba(15,23,42,0.13);</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ BLUR ══ -->
<section class="ds-section" id="blur" data-title="Blur">
<h2 class="ds-section-title">Blur</h2>
<p class="ds-section-desc"><code>--blur: blur(20px)</code> — used with a semi-transparent background for glass surfaces.</p>
<div class="ds-row">
<div class="blur-demo" style="flex:1">
<div class="blur-panel plain">No blur</div>
</div>
<div class="blur-demo" style="flex:1">
<div class="blur-panel frosted">Frosted glass</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--blur: blur(20px);
/* Usage */
.glass-panel {
background: rgba(238,242,255,0.55);
backdrop-filter: var(--blur);
border: 1.5px solid rgba(255,255,255,0.4);
}</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ BUTTONS ══ -->
<section class="ds-section" id="buttons" data-title="Buttons">
<h2 class="ds-section-title">Buttons</h2>
<p class="ds-section-desc">All buttons meet WCAG 2.5.5 44px touch target. Primary has shimmer on hover.</p>
<div class="ds-card">
<div class="ds-card-label">Variants</div>
<div class="btn-states">
<div class="touch-target-wrap">
<button class="btn-primary">Primary</button>
<div class="touch-target-box"></div>
</div>
<div class="touch-target-wrap">
<button class="btn-ghost">Ghost</button>
<div class="touch-target-box"></div>
</div>
<div class="touch-target-wrap">
<button class="btn-danger">Danger</button>
<div class="touch-target-box"></div>
</div>
<button class="icon-btn" aria-label="Settings">
<i data-lucide="settings" style="width:18px;height:18px"></i>
</button>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Disabled state</div>
<div class="btn-states">
<button class="btn-primary" disabled style="opacity:0.45;cursor:not-allowed">Primary disabled</button>
<button class="btn-ghost" disabled style="opacity:0.45;cursor:not-allowed">Ghost disabled</button>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Loading state</div>
<div class="btn-states">
<button class="btn-primary" style="display:inline-flex;align-items:center;gap:8px">
<div style="width:14px;height:14px;border:2px solid rgba(255,255,255,0.4);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite"></div>
Loading...
</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;button class="btn-primary"&gt;Primary&lt;/button&gt;
&lt;button class="btn-ghost"&gt;Ghost&lt;/button&gt;
&lt;button class="btn-danger"&gt;Danger&lt;/button&gt;
&lt;button class="icon-btn" aria-label="Settings"&gt;
&lt;i data-lucide="settings"&gt;&lt;/i&gt;
&lt;/button&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ INPUTS ══ -->
<section class="ds-section" id="inputs" data-title="Form Inputs">
<h2 class="ds-section-title">Form Inputs</h2>
<p class="ds-section-desc">All inputs use <code>.form-input</code>. Focus ring uses violet at 15% opacity.</p>
<div class="ds-card">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px">
<div class="form-group">
<label class="form-label">Text input (default)</label>
<input class="form-input" type="text" placeholder="Enter value...">
</div>
<div class="form-group">
<label class="form-label">Text input (error)</label>
<input class="form-input error" type="text" value="Invalid value">
<span class="form-error">This field is required</span>
</div>
<div class="form-group">
<label class="form-label">Number</label>
<input class="form-input" type="number" placeholder="0">
</div>
<div class="form-group">
<label class="form-label">Select</label>
<select class="form-input">
<option>Option A</option>
<option>Option B</option>
<option>Option C</option>
</select>
</div>
<div class="form-group" style="grid-column:span 2">
<label class="form-label">Textarea</label>
<textarea class="form-input" rows="3" placeholder="Write something..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Disabled</label>
<input class="form-input" type="text" value="Disabled input" disabled style="opacity:0.55;cursor:not-allowed">
</div>
<div class="form-group">
<label class="form-label">File upload</label>
<input class="form-input" type="file">
</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;input class="form-input" type="text" placeholder="Enter value..."&gt;
&lt;input class="form-input error" type="text"&gt;
&lt;select class="form-input"&gt;...&lt;/select&gt;
&lt;textarea class="form-input" rows="3"&gt;&lt;/textarea&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ BADGES ══ -->
<section class="ds-section" id="badges" data-title="Badges">
<h2 class="ds-section-title">Badges</h2>
<p class="ds-section-desc">5 brand-color variants, pill-shaped, suitable for status labels.</p>
<div class="ds-card">
<div class="ds-card-label">Variants</div>
<div class="ds-row">
<span class="badge badge-violet">Учитель</span>
<span class="badge badge-cyan">Активен</span>
<span class="badge badge-green">Завершено</span>
<span class="badge badge-pink">Новый</span>
<span class="badge badge-amber">Ученик</span>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Sizes</div>
<div class="ds-row" style="align-items:center">
<span class="badge badge-violet" style="font-size:0.62rem;padding:1px 7px">Micro</span>
<span class="badge badge-violet">Regular</span>
<span class="badge badge-violet" style="font-size:0.82rem;padding:4px 12px">Large</span>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;span class="badge badge-violet"&gt;Учитель&lt;/span&gt;
&lt;span class="badge badge-cyan"&gt;Активен&lt;/span&gt;
&lt;span class="badge badge-green"&gt;Завершено&lt;/span&gt;
&lt;span class="badge badge-pink"&gt;Новый&lt;/span&gt;
&lt;span class="badge badge-amber"&gt;Ученик&lt;/span&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ CHIPS ══ -->
<section class="ds-section" id="chips" data-title="Chips">
<h2 class="ds-section-title">Chips</h2>
<p class="ds-section-desc">Interactive filter chips, KPI chips, and status chips.</p>
<div class="ds-card">
<div class="ds-card-label">KPI chip</div>
<div class="ds-row">
<div class="chip-kpi">
<div class="chip-kpi-icon"><i data-lucide="users" style="width:14px;height:14px;stroke:#fff;fill:none"></i></div>
<div><div class="chip-kpi-label">Students</div><div class="chip-kpi-value">128</div></div>
</div>
<div class="chip-kpi">
<div class="chip-kpi-icon"><i data-lucide="trophy" style="width:14px;height:14px;stroke:#fff;fill:none"></i></div>
<div><div class="chip-kpi-label">Top score</div><div class="chip-kpi-value">96%</div></div>
</div>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Filter chips</div>
<div class="ds-row">
<button class="chip active">All subjects</button>
<button class="chip">Mathematics</button>
<button class="chip">Physics <span class="chip-x" onclick="this.closest('.chip').remove()" aria-label="Remove"><i data-lucide="x" style="width:12px;height:12px;stroke:currentColor;fill:none"></i></span></button>
<button class="chip">Chemistry</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;button class="chip active"&gt;All subjects&lt;/button&gt;
&lt;button class="chip"&gt;Mathematics
&lt;span class="chip-x" aria-label="Remove"&gt;...&lt;/span&gt;
&lt;/button&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ CARDS ══ -->
<section class="ds-section" id="cards" data-title="Cards">
<h2 class="ds-section-title">Cards</h2>
<p class="ds-section-desc">Surface, glass, top-bar, and hero card variants.</p>
<div class="ds-row" style="align-items:stretch">
<div class="ds-surface-card" style="flex:1">
<div style="font-weight:700;margin-bottom:6px">Surface card</div>
<div style="font-size:0.82rem;color:var(--text-2)">Uses <code>var(--surface)</code> background with shadow.</div>
</div>
<div style="flex:1;background:linear-gradient(135deg,var(--violet),var(--cyan));border-radius:var(--r-lg);padding:3px">
<div class="ds-glass-card" style="height:100%">
<div style="font-weight:700;margin-bottom:6px">Glass card</div>
<div style="font-size:0.82rem;color:var(--text-2)">Frosted backdrop-filter on gradient bg.</div>
</div>
</div>
<div class="ds-bar-card" style="flex:1">
<div style="font-weight:700;margin-bottom:6px">Top-bar card</div>
<div style="font-size:0.82rem;color:var(--text-2)"><code>::before</code> colored bar at top.</div>
</div>
</div>
<div style="margin-top:14px">
<div class="ds-hero-card">
<div style="font-size:0.78rem;color:rgba(255,255,255,0.6);font-weight:600">Sessions this week</div>
<div class="ds-hero-kpi">1,248</div>
<div style="font-size:0.8rem;color:rgba(255,255,255,0.7)">+12% vs last week</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="ds-surface-card"&gt;...&lt;/div&gt;
&lt;!-- glass: wrap in gradient div, then --&gt;
&lt;div style="backdrop-filter:var(--blur);background:rgba(255,255,255,0.35)"&gt;...&lt;/div&gt;
&lt;!-- top-bar: ::before height:3px; background: gradient --&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ MODAL ══ -->
<section class="ds-section" id="modal" data-title="Modal">
<h2 class="ds-section-title">Modal</h2>
<p class="ds-section-desc">Uses <code>LS.modal()</code> from api.js. Focus-trapped, dismissible via Escape or overlay click.</p>
<div class="ds-card">
<div class="btn-states">
<button class="btn-primary" onclick="openDemoModal('sm')">Open sm modal</button>
<button class="btn-ghost" onclick="openDemoModal('md')">Open md modal</button>
<button class="btn-ghost" onclick="openDemoModal('lg')">Open lg modal</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>const m = LS.modal({
title: 'Modal title',
content: '&lt;p&gt;Body content here&lt;/p&gt;',
size: 'md', // 'sm' | 'md' | 'lg'
actions: [
{ label: 'Cancel', onClick: () =&gt; m.close() },
{ label: 'Confirm', primary: true, onClick: async () =&gt; { ... } },
],
});</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ TOAST ══ -->
<section class="ds-section" id="toast" data-title="Toast">
<h2 class="ds-section-title">Toast</h2>
<p class="ds-section-desc">4 variants via <code>LS.toast(message, type)</code>. Auto-dismiss in 3.5s.</p>
<div class="ds-card">
<div class="btn-states">
<button class="btn-primary" onclick="dsToast('Action completed successfully!', 'success')">Success</button>
<button class="btn-ghost" onclick="dsToast('Please review your input.', 'warn')">Warning</button>
<button class="btn-danger" onclick="dsToast('An error occurred. Try again.', 'error')">Error</button>
<button class="btn-ghost" onclick="dsToast('Your session was saved.', 'info')">Info</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>LS.toast('Action completed!', 'success');
LS.toast('Review your input.', 'warn');
LS.toast('An error occurred.', 'error');
LS.toast('Session saved.', 'info');</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ SKELETON ══ -->
<section class="ds-section" id="skeleton" data-title="Skeleton">
<h2 class="ds-section-title">Skeleton</h2>
<p class="ds-section-desc">Shimmer placeholders for loading states. Use <code>.ls-skeleton</code> as base.</p>
<div class="ds-card">
<div class="ds-card-label">Line skeleton</div>
<div style="max-width:360px">
<div class="ls-skeleton ls-skeleton-line" style="width:80%"></div>
<div class="ls-skeleton ls-skeleton-line" style="width:60%"></div>
<div class="ls-skeleton ls-skeleton-line" style="width:90%"></div>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Card skeleton</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;max-width:480px">
<div class="ls-skeleton" style="height:80px;border-radius:var(--r-lg)"></div>
<div class="ls-skeleton" style="height:80px;border-radius:var(--r-lg)"></div>
<div class="ls-skeleton" style="height:80px;border-radius:var(--r-lg)"></div>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Row skeleton (violet shimmer variant)</div>
<div class="ls-skeleton-row ls-sk" style="border-radius:var(--r-md)">
<div class="ls-sk" style="width:36px;height:36px;border-radius:10px;flex-shrink:0"></div>
<div style="flex:1"><div class="ls-sk ls-skeleton-line" style="width:60%"></div><div class="ls-sk ls-skeleton-line" style="width:40%"></div></div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;!-- Neutral shimmer --&gt;
&lt;div class="ls-skeleton ls-skeleton-line"&gt;&lt;/div&gt;
&lt;!-- Violet shimmer (existing) --&gt;
&lt;div class="ls-sk" style="height:36px"&gt;&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ EMPTY STATE ══ -->
<section class="ds-section" id="empty-state" data-title="Empty State">
<h2 class="ds-section-title">Empty State</h2>
<p class="ds-section-desc">Zero-data, error, and no-results variants using <code>.rich-empty</code>.</p>
<div class="ds-row">
<div class="rich-empty" style="flex:1">
<div class="rich-empty-svg"><i data-lucide="inbox" style="width:44px;height:44px;stroke:var(--violet);fill:none;stroke-width:1.5"></i></div>
<div class="rich-empty-title">No data yet</div>
<div class="rich-empty-sub">Create your first item to get started.</div>
<button class="rich-empty-btn">Create item</button>
</div>
<div class="rich-empty" style="flex:1">
<div class="rich-empty-svg"><i data-lucide="alert-triangle" style="width:44px;height:44px;stroke:var(--amber);fill:none;stroke-width:1.5"></i></div>
<div class="rich-empty-title">Something went wrong</div>
<div class="rich-empty-sub">We couldn't load the data. Check your connection.</div>
<button class="rich-empty-btn" style="background:var(--amber)">Retry</button>
</div>
<div class="rich-empty" style="flex:1">
<div class="rich-empty-svg"><i data-lucide="search-x" style="width:44px;height:44px;stroke:var(--text-3);fill:none;stroke-width:1.5"></i></div>
<div class="rich-empty-title">No results</div>
<div class="rich-empty-sub">Try adjusting your search or filters.</div>
<button class="rich-empty-btn" style="background:rgba(155,93,229,0.12);color:var(--violet)" onclick="this.closest('section').querySelectorAll('input').forEach(i=>i.value='')">Clear filter</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="rich-empty"&gt;
&lt;div class="rich-empty-svg"&gt;&lt;!-- Lucide icon --&gt;&lt;/div&gt;
&lt;div class="rich-empty-title"&gt;No data yet&lt;/div&gt;
&lt;div class="rich-empty-sub"&gt;Description text&lt;/div&gt;
&lt;button class="rich-empty-btn"&gt;Action&lt;/button&gt;
&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ AVATAR ══ -->
<section class="ds-section" id="avatar" data-title="Avatar">
<h2 class="ds-section-title">Avatar</h2>
<p class="ds-section-desc">Initials-based pills with HSL color derived from name hash. Role-colored variants.</p>
<div class="ds-card">
<div class="ds-card-label">Name-hashed colors</div>
<div class="ds-row" id="avatar-demo"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">Role variants</div>
<div class="ds-row">
<div style="display:flex;align-items:center;gap:10px">
<div class="avatar-pill" style="background:var(--grad-1)">AD</div>
<div><div style="font-size:0.82rem;font-weight:700">Admin</div><div style="font-size:0.72rem;color:var(--text-3)">Gradient</div></div>
</div>
<div style="display:flex;align-items:center;gap:10px">
<div class="avatar-pill" style="background:linear-gradient(135deg,var(--cyan),var(--green))">TR</div>
<div><div style="font-size:0.82rem;font-weight:700">Teacher</div><div style="font-size:0.72rem;color:var(--text-3)">Cyan-green</div></div>
</div>
<div style="display:flex;align-items:center;gap:10px">
<div class="avatar-pill" style="background:rgba(155,93,229,0.15);color:var(--violet)">ST</div>
<div><div style="font-size:0.82rem;font-weight:700">Student</div><div style="font-size:0.72rem;color:var(--text-3)">Subtle violet</div></div>
</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>function avatarColor(name) {
let h = 0;
for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
return `hsl(${h}, 60%, 48%)`;
}
// initials
const initials = name.split(' ').slice(0,2).map(w=>w[0]).join('').toUpperCase();</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ STAT CARD ══ -->
<section class="ds-section" id="stat-card" data-title="Stat Card">
<h2 class="ds-section-title">Stat Card</h2>
<p class="ds-section-desc">KPI cards with colored top bar and optional delta indicator.</p>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px">
<div class="stat-card vc">
<div class="stat-label">Total sessions</div>
<div class="stat-val">1,248</div>
<div class="stat-delta">+12% this week</div>
</div>
<div class="stat-card gc">
<div class="stat-label">Avg. score</div>
<div class="stat-val">78%</div>
<div class="stat-delta" style="color:var(--cyan)">Stable</div>
</div>
<div class="stat-card ac">
<div class="stat-label">Active classes</div>
<div class="stat-val">24</div>
<div class="stat-delta" style="color:var(--amber)">+3 new</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="stat-card vc"&gt;
&lt;div class="stat-label"&gt;Total sessions&lt;/div&gt;
&lt;div class="stat-val"&gt;1,248&lt;/div&gt;
&lt;div class="stat-delta"&gt;+12% this week&lt;/div&gt;
&lt;/div&gt;
/* .vc = violet-cyan bar, .gc = green-cyan, .ac = amber-pink */</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ DATA TABLE ══ -->
<section class="ds-section" id="data-table" data-title="Data Table">
<h2 class="ds-section-title">Data Table</h2>
<p class="ds-section-desc">Sticky header, hover highlight, row actions revealed on hover.</p>
<div class="ds-table-wrap">
<table class="ds-table">
<thead>
<tr>
<th>Student</th><th>Subject</th><th>Score</th><th>Date</th><th>Status</th><th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Anna K.</td><td>Mathematics</td><td>92%</td><td>May 20</td>
<td><span class="badge badge-green">Passed</span></td>
<td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button></div></td>
</tr>
<tr>
<td>Dmitri P.</td><td>Physics</td><td>74%</td><td>May 19</td>
<td><span class="badge badge-amber">Review</span></td>
<td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button></div></td>
</tr>
<tr>
<td>Maria S.</td><td>Chemistry</td><td>88%</td><td>May 18</td>
<td><span class="badge badge-violet">Graded</span></td>
<td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button></div></td>
</tr>
<tr>
<td>Ivan B.</td><td>History</td><td>61%</td><td>May 17</td>
<td><span class="badge badge-pink">Failed</span></td>
<td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button></div></td>
</tr>
<tr>
<td>Elena V.</td><td>Biology</td><td>95%</td><td>May 16</td>
<td><span class="badge badge-cyan">Top score</span></td>
<td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button></div></td>
</tr>
</tbody>
</table>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="ds-table-wrap"&gt;
&lt;table class="ds-table"&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Name&lt;/th&gt;...&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anna K.&lt;/td&gt;...
&lt;td&gt;&lt;div class="row-actions"&gt;...&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ SEARCH BAR ══ -->
<section class="ds-section" id="search-bar" data-title="Search + Filter Bar">
<h2 class="ds-section-title">Search + Filter Bar</h2>
<p class="ds-section-desc">Search input paired with filter chips pattern.</p>
<div class="ds-card">
<div class="search-bar">
<div class="search-wrap">
<i data-lucide="search" style="width:16px;height:16px"></i>
<input class="form-input" type="search" placeholder="Search students...">
</div>
<button class="chip active" id="sf-all" onclick="setFilter(this)">All</button>
<button class="chip" onclick="setFilter(this)">Active</button>
<button class="chip" onclick="setFilter(this)">Inactive</button>
<button class="chip" onclick="setFilter(this)">Pending</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="search-bar"&gt;
&lt;div class="search-wrap"&gt;
&lt;!-- Lucide search icon --&gt;
&lt;input class="form-input" type="search" placeholder="Search..."&gt;
&lt;/div&gt;
&lt;button class="chip active"&gt;All&lt;/button&gt;
&lt;button class="chip"&gt;Active&lt;/button&gt;
&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ SIDEBAR NAV ══ -->
<section class="ds-section" id="sidebar-nav" data-title="Sidebar Nav">
<h2 class="ds-section-title">Sidebar Nav</h2>
<p class="ds-section-desc"><code>.sb-link</code> with active state indicator. Collapses to icon-only at 62px.</p>
<div class="ds-card" style="max-width:240px;padding:12px">
<a class="sb-link active" href="#">
<span class="sb-icon"><i data-lucide="layout-dashboard" style="width:18px;height:18px"></i></span>
<span class="sb-lbl">Dashboard</span>
</a>
<a class="sb-link" href="#">
<span class="sb-icon"><i data-lucide="users" style="width:18px;height:18px"></i></span>
<span class="sb-lbl">Students</span>
<span class="sb-badge">3</span>
</a>
<a class="sb-link" href="#">
<span class="sb-icon"><i data-lucide="book-open" style="width:18px;height:18px"></i></span>
<span class="sb-lbl">Library</span>
</a>
<a class="sb-link" href="#">
<span class="sb-icon"><i data-lucide="settings" style="width:18px;height:18px"></i></span>
<span class="sb-lbl">Settings</span>
</a>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;a class="sb-link active" href="/dashboard"&gt;
&lt;span class="sb-icon"&gt;&lt;i data-lucide="layout-dashboard"&gt;&lt;/i&gt;&lt;/span&gt;
&lt;span class="sb-lbl"&gt;Dashboard&lt;/span&gt;
&lt;/a&gt;
&lt;a class="sb-link" href="/students"&gt;
&lt;span class="sb-icon"&gt;&lt;i data-lucide="users"&gt;&lt;/i&gt;&lt;/span&gt;
&lt;span class="sb-lbl"&gt;Students&lt;/span&gt;
&lt;span class="sb-badge"&gt;3&lt;/span&gt;
&lt;/a&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ TABS ══ -->
<section class="ds-section" id="tabs" data-title="Tabs">
<h2 class="ds-section-title">Tabs</h2>
<p class="ds-section-desc">Pill-tabs with white active indicator. Wraps in a muted violet track.</p>
<div class="ds-card">
<div class="ds-tabs">
<button class="ds-tab active" onclick="switchTab(this)">Overview</button>
<button class="ds-tab" onclick="switchTab(this)">Sessions</button>
<button class="ds-tab" onclick="switchTab(this)">Students</button>
<button class="ds-tab" onclick="switchTab(this)">Settings</button>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="ds-tabs"&gt;
&lt;button class="ds-tab active"&gt;Overview&lt;/button&gt;
&lt;button class="ds-tab"&gt;Sessions&lt;/button&gt;
&lt;button class="ds-tab"&gt;Students&lt;/button&gt;
&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ HERO HEADER ══ -->
<section class="ds-section" id="hero-header" data-title="Hero Header">
<h2 class="ds-section-title">Hero Header</h2>
<p class="ds-section-desc">Greeting + KPI chip row used at the top of dashboard pages.</p>
<div class="hero-header">
<div class="hero-greeting">Good morning, Maxim</div>
<div class="ds-row">
<div class="chip-kpi">
<div class="chip-kpi-icon"><i data-lucide="book-open" style="width:14px;height:14px;stroke:#fff;fill:none"></i></div>
<div><div class="chip-kpi-label">Subjects</div><div class="chip-kpi-value">8</div></div>
</div>
<div class="chip-kpi">
<div class="chip-kpi-icon"><i data-lucide="users" style="width:14px;height:14px;stroke:#fff;fill:none"></i></div>
<div><div class="chip-kpi-label">Students</div><div class="chip-kpi-value">128</div></div>
</div>
<div class="chip-kpi">
<div class="chip-kpi-icon"><i data-lucide="award" style="width:14px;height:14px;stroke:#fff;fill:none"></i></div>
<div><div class="chip-kpi-label">Avg. score</div><div class="chip-kpi-value">78%</div></div>
</div>
</div>
</div>
</section>
<!-- ══ BENTO ══ -->
<section class="ds-section" id="bento" data-title="Bento Grid">
<h2 class="ds-section-title">Bento Grid</h2>
<p class="ds-section-desc">3-column masonry-like grid with spanning cells for dashboard layouts.</p>
<div class="bento">
<div class="bento-cell wide">
<div style="font-weight:700;margin-bottom:4px">Wide cell (span 2)</div>
<div style="font-size:0.8rem;color:var(--text-2)">grid-column: span 2</div>
</div>
<div class="bento-cell tall" style="background:linear-gradient(135deg,rgba(155,93,229,0.08),rgba(6,214,224,0.08))">
<div style="font-weight:700;margin-bottom:4px">Tall cell (span 2 rows)</div>
<div style="font-size:0.8rem;color:var(--text-2)">grid-row: span 2</div>
</div>
<div class="bento-cell"><div style="font-weight:600;font-size:0.84rem">Cell A</div></div>
<div class="bento-cell"><div style="font-weight:600;font-size:0.84rem">Cell B</div></div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;div class="bento"&gt;
&lt;div class="bento-cell wide"&gt;Span 2 cols&lt;/div&gt;
&lt;div class="bento-cell tall"&gt;Span 2 rows&lt;/div&gt;
&lt;div class="bento-cell"&gt;Normal&lt;/div&gt;
&lt;div class="bento-cell"&gt;Normal&lt;/div&gt;
&lt;/div&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ HOVER ROW ACTIONS ══ -->
<section class="ds-section" id="hover-row" data-title="Hover Row Actions">
<h2 class="ds-section-title">Hover Row Actions</h2>
<p class="ds-section-desc">Action buttons hidden by default, revealed on row hover via <code>display:flex</code> toggle.</p>
<div class="ds-table-wrap">
<table class="ds-table">
<thead><tr><th>Name</th><th>Role</th><th>Last active</th><th></th></tr></thead>
<tbody>
<tr><td>Anna K.</td><td><span class="badge badge-cyan">Teacher</span></td><td>2 min ago</td><td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Delete"><i data-lucide="trash-2" style="width:16px;height:16px"></i></button></div></td></tr>
<tr><td>Dmitri P.</td><td><span class="badge badge-violet">Student</span></td><td>1 hour ago</td><td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Delete"><i data-lucide="trash-2" style="width:16px;height:16px"></i></button></div></td></tr>
<tr><td>Maria S.</td><td><span class="badge badge-green">Admin</span></td><td>Just now</td><td><div class="row-actions"><button class="icon-btn" aria-label="View"><i data-lucide="eye" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Edit"><i data-lucide="edit" style="width:16px;height:16px"></i></button><button class="icon-btn" aria-label="Delete"><i data-lucide="trash-2" style="width:16px;height:16px"></i></button></div></td></tr>
</tbody>
</table>
</div>
<p style="font-size:0.78rem;color:var(--text-3);margin-top:8px">Hover a row to see action buttons.</p>
</section>
<!-- ══ MOTION ══ -->
<section class="ds-section" id="motion" data-title="Motion">
<h2 class="ds-section-title">Motion</h2>
<p class="ds-section-desc">Spring and ease-out transitions. Hover cards to see. Toggle prefers-reduced-motion simulation.</p>
<div style="margin-bottom:16px">
<label class="rm-toggle">
<input type="checkbox" id="rm-toggle" onchange="toggleReducedMotion(this.checked)">
Simulate prefers-reduced-motion
</label>
</div>
<div class="ds-row">
<div class="motion-card" style="flex:1;min-width:140px">
<i data-lucide="zap" style="width:28px;height:28px;stroke:var(--violet);fill:none;stroke-width:1.5;margin-bottom:8px"></i>
<div style="font-weight:700;margin-bottom:4px">Spring</div>
<div style="font-size:0.78rem;color:var(--text-2)">--ease-spring<br>0.22s</div>
</div>
<div class="motion-card" style="flex:1;min-width:140px;transition-timing-function:var(--ease-out)">
<i data-lucide="wind" style="width:28px;height:28px;stroke:var(--cyan);fill:none;stroke-width:1.5;margin-bottom:8px"></i>
<div style="font-weight:700;margin-bottom:4px">Ease-out</div>
<div style="font-size:0.78rem;color:var(--text-2)">--ease-out<br>0.22s</div>
</div>
<div class="motion-card" style="flex:1;min-width:140px;transition-duration:var(--duration-slow)">
<i data-lucide="feather" style="width:28px;height:28px;stroke:var(--green);fill:none;stroke-width:1.5;margin-bottom:8px"></i>
<div style="font-weight:700;margin-bottom:4px">Slow</div>
<div style="font-size:0.78rem;color:var(--text-2)">--duration-slow<br>0.40s</div>
</div>
<div style="flex:1;min-width:140px">
<div class="ls-sk" style="height:80px;border-radius:var(--r-lg);display:flex;align-items:center;justify-content:center">
<span style="font-size:0.78rem;font-weight:700;color:var(--violet)">Shimmer</span>
</div>
<div style="font-size:0.72rem;color:var(--text-3);margin-top:6px;text-align:center">.ls-sk continuous</div>
</div>
</div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 0.12s;
--duration-base: 0.22s;
--duration-slow: 0.40s;
/* Usage */
.card { transition: transform var(--duration-base) var(--ease-spring); }
.card:hover { transform: translateY(-6px) scale(1.02); }
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ ACCESSIBILITY ══ -->
<section class="ds-section" id="a11y" data-title="Accessibility">
<h2 class="ds-section-title">Accessibility</h2>
<p class="ds-section-desc">Focus rings use <code>:focus-visible</code>. All interactive elements meet 44px touch target (WCAG 2.5.5).</p>
<div class="ds-card">
<div class="ds-card-label">Focus ring (Tab through these)</div>
<div class="ds-row">
<button class="focus-demo-btn">Button A</button>
<button class="focus-demo-btn">Button B</button>
<a class="focus-demo-btn" href="#a11y" style="text-decoration:none">Link C</a>
</div>
<div class="ds-divider"></div>
<div class="ds-card-label">Touch target 44x44px (dashed outline = target area)</div>
<div class="ds-row">
<div class="touch-target-wrap">
<button class="btn-primary">Primary</button>
<div class="touch-target-box"></div>
</div>
<div class="touch-target-wrap">
<button class="icon-btn" aria-label="Settings"><i data-lucide="settings" style="width:18px;height:18px"></i></button>
<div class="touch-target-box"></div>
</div>
<div class="touch-target-wrap">
<a class="btn-nav" href="#a11y">Nav link</a>
<div class="touch-target-box"></div>
</div>
</div>
</div>
</section>
<!-- ══ CONTRAST ══ -->
<section class="ds-section" id="contrast" data-title="Contrast Checker">
<h2 class="ds-section-title">Contrast Checker</h2>
<p class="ds-section-desc">Compute WCAG 2.1 contrast ratio live. AA requires 4.5:1 (normal text), 3:1 (large text).</p>
<div class="ds-card">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
<div class="form-group">
<label class="form-label">Text color (hex)</label>
<input class="form-input" type="color" id="cc-fg" value="#0F172A" oninput="updateContrast()">
</div>
<div class="form-group">
<label class="form-label">Background color (hex)</label>
<input class="form-input" type="color" id="cc-bg" value="#EEF2FF" oninput="updateContrast()">
</div>
</div>
<div id="cc-preview" style="border-radius:var(--r-md);padding:20px;font-size:1rem;font-weight:600;margin-bottom:12px;border:1.5px solid var(--border)">
Sample text on background
</div>
<div id="cc-result" style="display:flex;gap:16px;flex-wrap:wrap"></div>
<div class="ds-divider"></div>
<div class="ds-card-label">Design system contrast checks</div>
<div id="contrast-checks" style="display:flex;flex-direction:column;gap:8px"></div>
</div>
</section>
<!-- ══ ICONS ══ -->
<section class="ds-section" id="icons" data-title="Icons">
<h2 class="ds-section-title">Icons (Lucide)</h2>
<p class="ds-section-desc">Top 50 Lucide icons used in LearnSpace. Click to copy <code>&lt;i data-lucide="..."&gt;</code> tag.</p>
<div class="icon-grid" id="icon-grid"></div>
<details class="ds-code"><summary>Code snippet</summary><pre><code>&lt;!-- Include once in &lt;head&gt; --&gt;
&lt;script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"&gt;&lt;/script&gt;
&lt;!-- Use icon --&gt;
&lt;i data-lucide="users" style="width:18px;height:18px"&gt;&lt;/i&gt;
&lt;!-- Initialize (call once after DOM) --&gt;
&lt;script&gt;lucide.createIcons();&lt;/script&gt;</code><button class="ds-copy-btn" onclick="dsCopy(this)">Copy</button></pre></details>
</section>
<!-- ══ ANTI-PATTERNS ══ -->
<section class="ds-section" id="anti-patterns" data-title="Anti-Patterns">
<h2 class="ds-section-title">Anti-Patterns</h2>
<p class="ds-section-desc">Common mistakes and their correct counterparts in LearnSpace.</p>
<div class="ap-grid">
<div class="ap-card ap-bad">
<div class="ap-label">Don't</div>
<code class="ap-code">color: #9B5DE5;</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Hardcoded hex bypasses theming</div>
</div>
<div class="ap-card ap-good">
<div class="ap-label">Do</div>
<code class="ap-code">color: var(--violet);</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Use CSS token — survives palette changes</div>
</div>
<div class="ap-card ap-bad">
<div class="ap-label">Don't</div>
<code class="ap-code">style="display:none"</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Inline style overrides specificity</div>
</div>
<div class="ap-card ap-good">
<div class="ap-label">Do</div>
<code class="ap-code">class="hidden"</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Utility class, toggled with JS</div>
</div>
<div class="ap-card ap-bad">
<div class="ap-label">Don't</div>
<code class="ap-code">style="background:#9B5DE5;color:#fff;border-radius:999px;..."</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Re-inventing existing components</div>
</div>
<div class="ap-card ap-good">
<div class="ap-label">Do</div>
<code class="ap-code">&lt;button class="btn-primary"&gt;</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Reuse design system component</div>
</div>
<div class="ap-card ap-bad">
<div class="ap-label">Don't</div>
<code class="ap-code">style="padding:12px"</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Hardcoded spacing breaks scale</div>
</div>
<div class="ap-card ap-good">
<div class="ap-label">Do</div>
<code class="ap-code">class="p-3"</code>
<div style="font-size:0.76rem;color:var(--text-2);margin-top:8px">Token-backed utility (= 12px = --space-3)</div>
</div>
</div>
</section>
<!-- Footer -->
<footer style="margin-top:80px;padding:32px 0;border-top:1.5px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
<div style="font-size:0.78rem;color:var(--text-3)">Generated 2026-05-21 · LearnSpace Design System v1.0</div>
<a href="/css/ls.css" target="_blank" style="font-size:0.78rem;font-weight:700;color:var(--violet);text-decoration:none">View ls.css source</a>
</footer>
</main>
</div><!-- ds-layout -->
<script>
// ── Lucide init ──
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
buildPage();
});
// ── Copy flash ──
function showFlash(msg) {
const el = document.getElementById('ds-copy-flash');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 1800);
}
function dsCopy(btn) {
const code = btn.closest('pre').querySelector('code');
const text = code ? code.textContent : btn.closest('pre').textContent.replace(/Copy$/, '').trim();
navigator.clipboard.writeText(text).then(() => showFlash('Copied!'));
}
function copyText(text) {
navigator.clipboard.writeText(text).then(() => showFlash('Copied: ' + text));
}
// ── LS.toast shim (works standalone too) ──
function dsToast(msg, type) {
if (window.LS && window.LS.toast) { window.LS.toast(msg, type); return; }
// standalone fallback
const wrap = (() => { let w = document.getElementById('ls-toast-wrap'); if (!w) { w = document.createElement('div'); w.id='ls-toast-wrap'; w.style.cssText='position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;'; document.body.appendChild(w); } return w; })();
const colors = { success:'linear-gradient(135deg,#00C87A,#06B96E)', warn:'linear-gradient(135deg,#FF9F1C,#E07A00)', error:'linear-gradient(135deg,#F15BB5,#E0335E)', info:'linear-gradient(135deg,#06D6E0,#9B5DE5)' };
const el = document.createElement('div');
el.style.cssText = `display:flex;align-items:center;gap:10px;padding:12px 18px;border-radius:14px;min-width:220px;max-width:360px;font-family:'Manrope',sans-serif;font-size:0.875rem;font-weight:600;color:#fff;pointer-events:auto;box-shadow:0 8px 32px rgba(15,23,42,0.22);background:${colors[type]||colors.info};`;
el.textContent = msg;
wrap.appendChild(el);
setTimeout(() => el.remove(), 3500);
}
// ── LS.modal shim ──
function openDemoModal(size) {
if (window.LS && window.LS.modal) {
const m = window.LS.modal({
title: 'Demo Modal (' + size + ')',
content: '<p style="color:var(--text-2);font-size:0.88rem;line-height:1.7">This is the <strong>LS.modal</strong> component. It supports focus trapping, Escape to close, and sm/md/lg sizes.<br><br>Tab through these buttons to see focus management:</p><div style="display:flex;gap:10px;margin-top:16px"><button class="btn-ghost">Button A</button><button class="btn-ghost">Button B</button></div>',
size: size,
actions: [
{ label: 'Cancel', onClick: () => m.close() },
{ label: 'Confirm', primary: true, onClick: () => { dsToast('Confirmed!', 'success'); m.close(); } }
]
});
return;
}
// fallback: show alert
alert('LS.modal is available when api.js is loaded with auth context.');
}
// ── Section search ──
document.getElementById('ds-search').addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
document.querySelectorAll('.ds-section').forEach(sec => {
const title = (sec.dataset.title || sec.querySelector('.ds-section-title')?.textContent || '').toLowerCase();
sec.classList.toggle('hidden', q.length > 0 && !title.includes(q));
});
});
// ── Sidebar active on scroll ──
const navLinks = document.querySelectorAll('.ds-nav-link');
const sections = document.querySelectorAll('.ds-section');
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
navLinks.forEach(l => l.classList.remove('active'));
const link = document.querySelector(`.ds-nav-link[href="#${e.target.id}"]`);
if (link) link.classList.add('active');
}
});
}, { rootMargin: '-10% 0px -80% 0px' });
sections.forEach(s => observer.observe(s));
// ── Hash update on click ──
navLinks.forEach(link => {
link.addEventListener('click', e => {
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
});
});
// ── Tabs helper ──
function switchTab(btn) {
btn.closest('.ds-tabs').querySelectorAll('.ds-tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
}
// ── Filter chips ──
function setFilter(btn) {
btn.closest('.ds-card').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
}
// ── Reduced motion toggle ──
function toggleReducedMotion(on) {
if (on) document.documentElement.style.setProperty('--ease-spring', 'none');
else document.documentElement.style.removeProperty('--ease-spring');
document.querySelectorAll('.motion-card').forEach(c => {
c.style.transition = on ? 'none' : '';
});
}
// ── Contrast checker ──
function hexToRgb(hex) {
hex = hex.replace('#','');
if (hex.length === 3) hex = hex.split('').map(c=>c+c).join('');
const n = parseInt(hex,16);
return [n>>16, (n>>8)&255, n&255];
}
function linearize(c) { const s=c/255; return s<=0.04045 ? s/12.92 : Math.pow((s+0.055)/1.055,2.4); }
function luminance([r,g,b]) { return 0.2126*linearize(r) + 0.7152*linearize(g) + 0.0722*linearize(b); }
function contrastRatio(hex1, hex2) {
const l1 = luminance(hexToRgb(hex1)), l2 = luminance(hexToRgb(hex2));
const lighter = Math.max(l1,l2), darker = Math.min(l1,l2);
return (lighter+0.05)/(darker+0.05);
}
function wcagLevel(ratio, large=false) {
if (large) return ratio>=3 ? 'AA' : 'Fail';
return ratio>=7 ? 'AAA' : ratio>=4.5 ? 'AA' : ratio>=3 ? 'AA Large' : 'Fail';
}
function updateContrast() {
const fg = document.getElementById('cc-fg').value;
const bg = document.getElementById('cc-bg').value;
const ratio = contrastRatio(fg, bg);
const preview = document.getElementById('cc-preview');
preview.style.color = fg; preview.style.background = bg;
const level = wcagLevel(ratio);
const pass = level.startsWith('AA') || level==='AAA';
document.getElementById('cc-result').innerHTML = `
<div style="font-size:1.1rem;font-weight:800;color:var(--text)">${ratio.toFixed(2)}:1</div>
<div><span class="wcag-badge ${pass?'wcag-pass':'wcag-fail'}">${level}</span></div>
<div style="font-size:0.78rem;color:var(--text-2)">${pass?'Passes WCAG 2.1':'Does not meet WCAG 2.1'}</div>
`;
}
function buildContrastChecks() {
const checks = [
{ label:'--text on --bg', fg:'#0F172A', bg:'#EEF2FF' },
{ label:'--text-2 on --bg', fg:'#3D4F6B', bg:'#EEF2FF' },
{ label:'--text-3 on --bg', fg:'#56687A', bg:'#EEF2FF' },
{ label:'--text on --surface', fg:'#0F172A', bg:'#FFFFFF' },
{ label:'white on --violet', fg:'#FFFFFF', bg:'#9B5DE5' },
];
const el = document.getElementById('contrast-checks');
el.innerHTML = checks.map(c => {
const r = contrastRatio(c.fg, c.bg);
const level = wcagLevel(r);
const pass = level.startsWith('AA') || level==='AAA';
return `<div style="display:flex;align-items:center;gap:12px;font-size:0.82rem">
<div style="width:32px;height:20px;background:${c.bg};border:1px solid var(--border);border-radius:4px;display:flex;align-items:center;justify-content:center"><span style="color:${c.fg};font-size:0.6rem;font-weight:800">Aa</span></div>
<span style="flex:1;color:var(--text-2)">${c.label}</span>
<span style="font-weight:700">${r.toFixed(2)}:1</span>
<span class="wcag-badge ${pass?'wcag-pass':'wcag-fail'}">${level}</span>
</div>`;
}).join('');
}
// ── Avatar color ──
function avatarColor(name) {
let h = 0;
for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
return `hsl(${h}, 58%, 44%)`;
}
function initials(name) { return name.split(' ').slice(0,2).map(w=>w[0]||'').join('').toUpperCase(); }
// ── Build page ──
function buildPage() {
// Brand swatches
const brands = [
{ var:'--violet', hex:'#9B5DE5', name:'Violet' },
{ var:'--cyan', hex:'#06D6E0', name:'Cyan' },
{ var:'--green', hex:'#06D664', name:'Green' },
{ var:'--pink', hex:'#F15BB5', name:'Pink' },
{ var:'--amber', hex:'#FFB347', name:'Amber' },
];
document.getElementById('brand-swatches').innerHTML = brands.map(s => `
<div class="swatch" onclick="copyText('var(${s.var})')" title="Click to copy">
<div class="swatch-color" style="background:${s.hex}"></div>
<div class="swatch-info">
<div class="swatch-var">var(${s.var})</div>
<div class="swatch-hex">${s.hex} · ${s.name}</div>
</div>
</div>`).join('');
// Semantic swatches
const semantics = [
{ var:'--success', hex:'#06D664', alias:'--green' },
{ var:'--warning', hex:'#FFB347', alias:'--amber' },
{ var:'--danger', hex:'#F15BB5', alias:'--pink' },
{ var:'--info', hex:'#06D6E0', alias:'--cyan' },
];
document.getElementById('semantic-swatches').innerHTML = semantics.map(s => `
<div style="display:flex;align-items:center;gap:10px;cursor:pointer" onclick="copyText('var(${s.var})')" title="Click to copy">
<div style="width:32px;height:32px;background:${s.hex};border-radius:8px;border:1px solid rgba(0,0,0,0.08)"></div>
<div>
<div style="font-family:'Courier New',monospace;font-size:0.76rem;font-weight:700">var(${s.var})</div>
<div style="font-size:0.68rem;color:var(--text-3)">= var(${s.alias})</div>
</div>
</div>`).join('');
// Text swatches
const texts = [
{ var:'--text', hex:'#0F172A', label:'Primary text' },
{ var:'--text-2', hex:'#3D4F6B', label:'Secondary' },
{ var:'--text-3', hex:'#56687A', label:'Muted' },
{ var:'--surface', hex:'rgba(255,255,255,0.82)', label:'Surface' },
{ var:'--bg', hex:'#EEF2FF', label:'Background' },
];
document.getElementById('text-swatches').innerHTML = texts.map(s => `
<div style="display:flex;align-items:center;gap:10px;cursor:pointer" onclick="copyText('var(${s.var})')" title="Click to copy">
<div style="width:32px;height:32px;background:${s.hex};border-radius:8px;border:1.5px solid var(--border)"></div>
<div>
<div style="font-family:'Courier New',monospace;font-size:0.76rem;font-weight:700">var(${s.var})</div>
<div style="font-size:0.68rem;color:var(--text-3)">${s.label}</div>
</div>
</div>`).join('');
// WCAG contrast preview
const contrastItems = [
{ label:'--text on --bg', fg:'#0F172A', bg:'#EEF2FF' },
{ label:'--text-3 on --bg', fg:'#56687A', bg:'#EEF2FF' },
{ label:'White on --violet', fg:'#fff', bg:'#9B5DE5' },
];
document.getElementById('contrast-preview').innerHTML = `<div style="display:flex;gap:16px;flex-wrap:wrap">` + contrastItems.map(c => {
const r = contrastRatio(c.fg, c.bg);
const pass = r>=4.5;
return `<div style="display:flex;align-items:center;gap:10px">
<div class="contrast-box" style="background:${c.bg};color:${c.fg}">${r.toFixed(1)}:1</div>
<div>
<div style="font-size:0.72rem;color:var(--text-2)">${c.label}</div>
<span class="wcag-badge ${pass?'wcag-pass':'wcag-fail'}">${pass?'AA pass':'Fail'}</span>
</div>
</div>`;
}).join('') + '</div>';
// Type scale
const typeScale = [
{ var:'--text-xs', rem:'0.72rem', label:'Extra Small · 11.5px' },
{ var:'--text-sm', rem:'0.82rem', label:'Small · 13px' },
{ var:'--text-base', rem:'0.92rem', label:'Base · 14.7px (body default)' },
{ var:'--text-md', rem:'1.02rem', label:'Medium · 16.3px' },
{ var:'--text-lg', rem:'1.18rem', label:'Large · 18.9px' },
{ var:'--text-xl', rem:'1.5rem', label:'XL · 24px' },
{ var:'--text-2xl', rem:'2rem', label:'2XL · 32px', font:'Unbounded' },
{ var:'--text-3xl', rem:'2.6rem', label:'3XL · 41.6px', font:'Unbounded' },
];
document.getElementById('type-scale').innerHTML = typeScale.map(t => `
<div style="display:flex;align-items:baseline;gap:16px;margin:6px 0;cursor:pointer" onclick="copyText('var(${t.var})')" title="Click to copy">
<span style="font-size:${t.rem};font-family:${t.font||'Manrope'},sans-serif;font-weight:${t.font?800:500};color:var(--text);line-height:1.2">Aa</span>
<span style="font-family:'Courier New',monospace;font-size:0.72rem;color:var(--violet)">var(${t.var})</span>
<span style="font-size:0.72rem;color:var(--text-3)">${t.label}</span>
</div>`).join('');
// Weight scale
const weights = [
{ var:'--fw-regular', val:400 },
{ var:'--fw-medium', val:500 },
{ var:'--fw-semibold', val:600 },
{ var:'--fw-bold', val:700 },
{ var:'--fw-extrabold', val:800 },
];
document.getElementById('weight-scale').innerHTML = weights.map(w => `
<div style="display:flex;align-items:center;gap:16px;margin:6px 0;cursor:pointer" onclick="copyText('var(${w.var})')" title="Click to copy">
<span style="font-size:1rem;font-weight:${w.val};color:var(--text);min-width:180px">The quick brown fox</span>
<span style="font-family:'Courier New',monospace;font-size:0.72rem;color:var(--violet)">var(${w.var})</span>
<span style="font-size:0.72rem;color:var(--text-3)">weight ${w.val}</span>
</div>`).join('');
// Spacing ruler
const spaces = [
{ var:'--space-1', px:4 },
{ var:'--space-2', px:8 },
{ var:'--space-3', px:12 },
{ var:'--space-4', px:16 },
{ var:'--space-5', px:20 },
{ var:'--space-6', px:24 },
{ var:'--space-8', px:32 },
{ var:'--space-10', px:40 },
{ var:'--space-12', px:48 },
{ var:'--space-16', px:64 },
];
document.getElementById('spacing-ruler').innerHTML = spaces.map(s => `
<div class="space-row" onclick="copyText('var(${s.var})')" style="cursor:pointer" title="Click to copy">
<span class="space-label">var(${s.var})</span>
<div class="space-bar" style="width:${s.px * 3}px"></div>
<span class="space-px">${s.px}px</span>
</div>`).join('');
// Radii demo
const radii = [
{ var:'--r-xs', px:4, label:'xs' },
{ var:'--r-sm', px:8, label:'sm' },
{ var:'--r-md', px:12, label:'md' },
{ var:'--r-lg', px:20, label:'lg' },
{ var:'--r-xl', px:24, label:'xl' },
{ var:'--r-pill', px:999, label:'pill' },
];
document.getElementById('radii-demo').innerHTML = radii.map(r => `
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer" onclick="copyText('var(${r.var})')" title="Click to copy">
<div class="radius-box" style="border-radius:${r.px}px">
<span style="font-size:0.6rem;font-weight:800">${r.label}</span>
<span style="font-size:0.54rem;opacity:0.8">${r.px === 999 ? '999' : r.px}px</span>
</div>
<span style="font-size:0.66rem;font-family:'Courier New',monospace;color:var(--violet)">var(${r.var})</span>
</div>`).join('');
// Avatar demo
const names = ['Anna Kovaleva', 'Dmitri Petrov', 'Maria Sidorova', 'Ivan Bazhov', 'Elena Volkov', 'Pavel Novak'];
document.getElementById('avatar-demo').innerHTML = names.map(n => `
<div style="display:flex;align-items:center;gap:8px">
<div class="avatar-pill" style="background:${avatarColor(n)}">${initials(n)}</div>
<span style="font-size:0.78rem;color:var(--text-2)">${n}</span>
</div>`).join('');
// Icons
const iconNames = ['users','file-text','settings','search','plus','x','check','edit','trash-2','eye','eye-off','lock','unlock','mail','phone','calendar','clock','home','layout-dashboard','bell','star','heart','trophy','award','coins','shopping-bag','book-open','graduation-cap','video','mic','mic-off','camera','image','paperclip','link','share-2','copy','download','upload','refresh-cw','alert-triangle','alert-circle','info','help-circle','chevron-down','chevron-right','arrow-left','arrow-right','more-horizontal','menu'];
document.getElementById('icon-grid').innerHTML = iconNames.map(name => `
<div class="icon-cell" onclick="copyText('<i data-lucide=\\"${name}\\">')" title="Copy icon tag">
<i data-lucide="${name}" style="width:20px;height:20px"></i>
<span>${name}</span>
</div>`).join('');
lucide.createIcons();
// Contrast checker
updateContrast();
buildContrastChecks();
}
</script>
</body>
</html>