e4ec9f8823
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>
1574 lines
80 KiB
HTML
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 & 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 & 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><button class="btn-primary">Primary</button>
|
|
<button class="btn-ghost">Ghost</button>
|
|
<button class="btn-danger">Danger</button>
|
|
<button class="icon-btn" aria-label="Settings">
|
|
<i data-lucide="settings"></i>
|
|
</button></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><input class="form-input" type="text" placeholder="Enter value...">
|
|
<input class="form-input error" type="text">
|
|
<select class="form-input">...</select>
|
|
<textarea class="form-input" rows="3"></textarea></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><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></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><button class="chip active">All subjects</button>
|
|
<button class="chip">Mathematics
|
|
<span class="chip-x" aria-label="Remove">...</span>
|
|
</button></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><div class="ds-surface-card">...</div>
|
|
|
|
<!-- glass: wrap in gradient div, then -->
|
|
<div style="backdrop-filter:var(--blur);background:rgba(255,255,255,0.35)">...</div>
|
|
|
|
<!-- top-bar: ::before height:3px; background: gradient --></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: '<p>Body content here</p>',
|
|
size: 'md', // 'sm' | 'md' | 'lg'
|
|
actions: [
|
|
{ label: 'Cancel', onClick: () => m.close() },
|
|
{ label: 'Confirm', primary: true, onClick: async () => { ... } },
|
|
],
|
|
});</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><!-- Neutral shimmer -->
|
|
<div class="ls-skeleton ls-skeleton-line"></div>
|
|
|
|
<!-- Violet shimmer (existing) -->
|
|
<div class="ls-sk" style="height:36px"></div></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><div class="rich-empty">
|
|
<div class="rich-empty-svg"><!-- Lucide icon --></div>
|
|
<div class="rich-empty-title">No data yet</div>
|
|
<div class="rich-empty-sub">Description text</div>
|
|
<button class="rich-empty-btn">Action</button>
|
|
</div></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><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>
|
|
/* .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><div class="ds-table-wrap">
|
|
<table class="ds-table">
|
|
<thead><tr><th>Name</th>...</tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Anna K.</td>...
|
|
<td><div class="row-actions">...</div></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div></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><div class="search-bar">
|
|
<div class="search-wrap">
|
|
<!-- Lucide search icon -->
|
|
<input class="form-input" type="search" placeholder="Search...">
|
|
</div>
|
|
<button class="chip active">All</button>
|
|
<button class="chip">Active</button>
|
|
</div></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><a class="sb-link active" href="/dashboard">
|
|
<span class="sb-icon"><i data-lucide="layout-dashboard"></i></span>
|
|
<span class="sb-lbl">Dashboard</span>
|
|
</a>
|
|
<a class="sb-link" href="/students">
|
|
<span class="sb-icon"><i data-lucide="users"></i></span>
|
|
<span class="sb-lbl">Students</span>
|
|
<span class="sb-badge">3</span>
|
|
</a></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><div class="ds-tabs">
|
|
<button class="ds-tab active">Overview</button>
|
|
<button class="ds-tab">Sessions</button>
|
|
<button class="ds-tab">Students</button>
|
|
</div></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><div class="bento">
|
|
<div class="bento-cell wide">Span 2 cols</div>
|
|
<div class="bento-cell tall">Span 2 rows</div>
|
|
<div class="bento-cell">Normal</div>
|
|
<div class="bento-cell">Normal</div>
|
|
</div></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><i data-lucide="..."></code> tag.</p>
|
|
<div class="icon-grid" id="icon-grid"></div>
|
|
<details class="ds-code"><summary>Code snippet</summary><pre><code><!-- Include once in <head> -->
|
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
|
|
<!-- Use icon -->
|
|
<i data-lucide="users" style="width:18px;height:18px"></i>
|
|
|
|
<!-- Initialize (call once after DOM) -->
|
|
<script>lucide.createIcons();</script></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"><button class="btn-primary"></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>
|