Component playground
按钮
Status & badges
Alerts
Cards
Form inputs
Advanced form controls
表格
| Name | Plan | 状态 | MRR |
|---|---|---|---|
SK Sarah K. | Pro | Active | $199 |
MR Michael R. | Business | Active | $499 |
EW Emily W. | Starter | Pending | $49 |
Tabs
Progress
Stat tiles
Timeline
Accordion
First panel
Second panel
Empty state
Skeleton table
Render placeholder rows while data loads. Width variation across columns prevents the "jelly bean" look. Fades in via the .skeleton shimmer animation defined in _components.scss.
| Customer | Plan | 状态 | MRR |
|---|---|---|---|
Skeleton tiles
Use for card grids — dashboards, file managers, product galleries. Avatar circle + headline + 2 lines is the canonical shape.
List lifecycle — interactive
Click a button below to simulate each state. This is exactly what orders.html and the inbox use under the hood.
Submit spinner
Disable the button + swap label for a spinner during the request. Re-enable + restore label when it resolves. Click below to simulate a 1.5s submit.
Banners — feedback at the page level
For things the user needs to see and may want to act on (failed payment, quota near limit). For ephemeral feedback (saved, copied, deleted) use a toast instead — see general elements.
Markup helpers — stop hand-writing scaffolds
For pages that build content from data (orders, inbox threads, kanban cards…) the boilerplate adds up. src/v4/markup.js exposes pure functions that return HTML strings — no framework, no virtual DOM. Auto-escapes user content. Drop the result into innerHTML or interpolate into a template literal.
Each helper below shows the call site (top) and the rendered output (bottom).
statTile()
import { statTile } from '/src/v4/markup.js';
document.querySelector('#stats').innerHTML = [
statTile({ label: 'Revenue', value: '$84,520', color: 'green',
change: { pct: '+18%', direction: 'up' }, subtext: '$3,218 today' }),
statTile({ label: 'Pending', value: '42', color: 'yellow',
change: { pct: '-3%', direction: 'down' }, subtext: '5 awaiting payment' })
].join('');
statusBadge()
statusBadge('Paid', 'green')
statusBadge('Pending', 'yellow')
statusBadge('Failed', 'red')
customerCell() — table cell with avatar + name
tbody.innerHTML = orders.map((o) =>
`<tr><td>${customerCell({ name: o.name, avatarColor: o.color })}</td></tr>`
).join('');
activityItem() — feeds, audit logs
const html = items.map((i) => activityItem({
initials: i.initials, avatarBg: i.bg,
bodyHtml: `<strong>${escapeHtml(i.user)}</strong> ${escapeHtml(i.text)}`,
time: i.time
})).join('');
container.innerHTML = `<ul class="activity-list">${html}</ul>`;
visitorRow() — distribution bars
const html = data.map((d) =>
visitorRow({ name: d.country, pct: d.pct, flag: d.flag })
).join('');
emptyState() — fallback for no results
container.innerHTML = emptyState({
title: 'No orders yet',
desc: "Orders will appear here once your first customer checks out.",
iconHtml: '<svg ...></svg>',
actionHtml: '<button class="btn btn-primary">Create test order</button>'
});