文档

按钮

Status & badges

Active Pending Failed Info Tag Removable

Alerts

Saved! Changes have been published.
Heads up. API quota at 87%.
Payment failed. Retry to continue.

Cards

Plain card
A simple card with header and body. The most common building block.
With subtitle
Plus an action
Header has room for actions on the right.

Form inputs

Advanced form controls

表格

NamePlan状态MRR
SK
Sarah K.
ProActive$199
MR
Michael R.
BusinessActive$499
EW
Emily W.
StarterPending$49

Tabs

Progress

Storage62 / 100 GB
API quota87,400 / 100,000

Stat tiles

Active users
8,432↑ 14%
vs last week
Churn
2.4%↓ 0.4pp
improving

Timeline

Just now
Deployment succeeded
v4.0.2 in production · 28s
14 min ago
PR #248 merged
3 hours ago
Review requested

Accordion

First panel
Content for the first panel goes here.
Second panel
Click the chevron to expand.

Empty state

No messages yet
When you receive messages, they'll show up here.

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.

CustomerPlan状态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.

↑ Click "Save"

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>'
    });