Initial commit: 小羚羚小程序 Axhub Make workspace.

Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-30 15:32:30 +08:00
commit a619938e0c
1227 changed files with 251728 additions and 0 deletions

View File

@@ -0,0 +1,964 @@
.dmb-page {
min-height: 100vh;
background: var(--dmb-bg, #ffffff);
color: var(--dmb-ink, #111318);
font-family: var(--dmb-font-body, var(--dmb-font-sans));
}
.dmb-layout {
width: min(1320px, calc(100vw - 48px));
margin: 0 auto;
padding: 42px 0 76px;
}
.dmb-sheet {
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
}
.dmb-sheet-head {
width: 100%;
min-height: 68px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 22px;
border-bottom: 1px solid color-mix(in srgb, var(--dmb-border, #e1e2e7) 72%, transparent);
}
.dmb-sheet-head-main {
min-width: 0;
max-width: 920px;
}
.dmb-sheet-actions {
flex: 0 0 auto;
align-self: flex-end;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
max-width: min(100%, 640px);
}
.dmb-sheet-head p {
margin: 0 0 6px;
color: var(--dmb-ink-subtle, #686c76);
font-size: 12px;
font-weight: 700;
}
.dmb-sheet-head h1 {
margin: 0;
color: var(--dmb-ink, #121319);
font-family: var(--dmb-font-display, var(--dmb-font-body, var(--dmb-font-sans)));
font-size: 54px;
line-height: 1.08;
font-weight: 800;
letter-spacing: 0;
}
.dmb-sheet-links {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin: 0;
position: relative;
isolation: isolate;
}
.dmb-sheet-links a {
position: relative;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 36px;
block-size: 36px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--dmb-ink-subtle, #64748b);
padding: 0;
box-shadow: none;
text-decoration: none;
line-height: 1;
transition: transform 160ms ease, color 160ms ease;
}
.dmb-sheet-links a:hover {
color: var(--dmb-ink, #111318);
text-decoration: none;
transform: translateY(-1px);
}
.dmb-sheet-links a:focus-visible {
color: var(--dmb-ink, #111318);
text-decoration: none;
transform: translateY(-1px);
outline: 2px solid currentColor;
outline-offset: 3px;
}
.dmb-sheet-links svg {
width: 16px;
height: 16px;
flex: 0 0 auto;
stroke: currentColor;
fill: none;
}
.dmb-sheet-links a::after {
content: attr(data-label);
position: absolute;
left: 50%;
bottom: calc(100% + 6px);
z-index: 4;
min-width: max-content;
border: 1px solid rgba(148, 163, 184, 0.22);
border-radius: 8px;
background: rgba(15, 23, 42, 0.86);
color: #ffffff;
padding: 6px 8px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 11px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
opacity: 0;
pointer-events: none;
transform: translate(-50%, 4px);
transition: opacity 140ms ease, transform 140ms ease;
}
.dmb-sheet-links a:hover::after,
.dmb-sheet-links a:focus-visible::after {
opacity: 1;
transform: translate(-50%, 0);
}
.dmb-tabs {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 4px;
border: 1px solid #e1e2e7;
border-radius: var(--dmb-radius-card, 0);
background: #f7f5ef;
}
.dmb-tabs button {
min-height: 32px;
border: 1px solid transparent;
border-radius: var(--dmb-radius-control, 0);
background: transparent;
color: #5f6370;
padding: 0 12px;
font: inherit;
font-family: var(--dmb-font-body, var(--dmb-font-sans));
font-size: 12px;
font-weight: 800;
cursor: pointer;
}
.dmb-tabs button[aria-selected="true"] {
border-color: color-mix(in srgb, var(--dmb-accent) 38%, #d9dbe2);
background: #ffffff;
color: #111318;
}
.dmb-tab-panel {
min-width: 0;
}
.dmb-preview {
width: 100%;
height: 620px;
border: 0;
border-radius: var(--dmb-radius-preview, 0);
background: color-mix(in srgb, var(--dmb-accent) 4%, #f6f7fa);
padding: 0;
overflow: hidden;
display: block;
position: relative;
cursor: zoom-in;
}
.dmb-preview::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
border: 1px solid color-mix(in srgb, var(--dmb-border, #dfe1e7) 80%, transparent);
border-radius: inherit;
}
.dmb-preview-canvas {
width: 100%;
height: 100%;
display: block;
overflow: hidden;
border-radius: var(--dmb-radius-preview, 0);
}
.dmb-preview img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
display: block;
border-radius: 0;
}
.dmb-preview iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
pointer-events: none;
background: #ffffff;
}
.dmb-preview-label {
position: absolute;
z-index: 2;
right: 12px;
bottom: 12px;
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: var(--dmb-radius-pill, 0);
background: rgba(17, 19, 24, 0.7);
color: #ffffff;
padding: 5px 9px;
font-family: var(--dmb-font-body, var(--dmb-font-sans));
font-size: 11px;
font-weight: 700;
}
.dmb-preview-empty {
display: grid;
place-items: center;
cursor: default;
color: #8a8c95;
}
.dmb-preview-empty strong,
.dmb-preview-empty span {
display: block;
position: static;
border: 0;
background: transparent;
color: inherit;
padding: 0;
}
.dmb-preview-empty strong {
color: #1b1d24;
font-size: 30px;
}
.dmb-description {
max-width: 1040px;
margin: 0;
}
.dmb-description p {
margin: 0;
color: var(--dmb-ink-muted, #3f424b);
font-family: var(--dmb-font-body, var(--dmb-font-sans));
font-size: 15px;
line-height: 1.65;
}
.dmb-description p + p {
margin-top: 4px;
color: var(--dmb-ink-subtle, #6c707a);
font-size: 13px;
}
.dmb-tags {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 10px;
}
.dmb-overview-meta {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px 28px;
align-items: start;
margin-top: 22px;
padding: 18px 0 0;
border-top: 1px solid color-mix(in srgb, var(--dmb-border, #e1e2e7) 64%, transparent);
}
.dmb-overview-meta .dmb-tags {
justify-content: flex-end;
margin-top: 2px;
max-width: 420px;
}
.dmb-tags span,
.dmb-muted {
border: 1px solid #e2e3e8;
border-radius: var(--dmb-radius-pill, 0);
background: #f4f1ea;
color: #545964;
padding: 5px 8px;
font-size: 11px;
line-height: 1;
}
.dmb-token-card,
.dmb-block {
margin-top: 30px;
}
.dmb-token-card {
border: 1px solid #e1e2e7;
border-radius: var(--dmb-radius-card, 0);
background: color-mix(in srgb, var(--dmb-accent) 9%, #ffffff);
padding: 12px;
display: grid;
grid-template-columns: 190px 1fr;
gap: 10px;
}
.dmb-token-rail,
.dmb-token-grid {
display: grid;
gap: 8px;
}
.dmb-token-chip {
min-height: 58px;
border-radius: var(--dmb-radius-control, 0);
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
color: #ffffff;
overflow: hidden;
}
.dmb-token-chip strong {
font-size: 11px;
}
.dmb-token-chip code {
font-family: var(--dmb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 10px;
color: currentColor;
}
.dmb-token-grid {
grid-template-columns: minmax(0, 1.05fr) minmax(164px, 0.82fr) minmax(150px, 0.76fr) minmax(0, 1fr) minmax(0, 1fr);
}
.dmb-token-grid > div {
min-height: 90px;
border-radius: var(--dmb-radius-control, 0);
background: color-mix(in srgb, var(--dmb-accent) 7%, #ffffff);
padding: 14px;
overflow: hidden;
}
.dmb-token-grid small {
display: block;
margin-bottom: 8px;
color: #8a8c95;
font-size: 11px;
}
.dmb-token-grid strong {
font-family: var(--dmb-font-display, var(--dmb-font-body, var(--dmb-font-sans)));
font-size: 48px;
line-height: 0.9;
}
.dmb-token-grid button,
.dmb-component-strip button {
min-height: 34px;
border: 1px solid var(--dmb-accent);
border-radius: var(--dmb-radius-control, 0);
background: var(--dmb-accent);
color: var(--dmb-accent-contrast);
padding: 4px 12px;
font-size: 11px;
line-height: 1.1;
font-weight: 800;
margin: 0 6px 6px 0;
}
.dmb-token-buttons {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.dmb-token-buttons button {
width: 118px;
margin: 0;
}
.dmb-token-grid button span {
font-size: 9px;
font-weight: 700;
opacity: 0.78;
}
.dmb-token-grid .dmb-text-button {
background: transparent;
color: var(--dmb-link);
}
.dmb-outline-button {
background: #ffffff !important;
color: var(--dmb-link) !important;
border-color: #d9d9df !important;
}
.dmb-token-grid input {
width: 100%;
height: 34px;
border: 1px solid #ededf0;
border-radius: var(--dmb-radius-control, 0);
background: #ffffff;
padding: 0 12px;
font: inherit;
font-family: var(--dmb-font-body, var(--dmb-font-sans));
}
.dmb-lines {
display: grid;
align-content: center;
gap: 7px;
}
.dmb-lines span {
display: block;
height: 3px;
background: var(--dmb-accent);
}
.dmb-lines span:nth-child(2) {
width: 84%;
}
.dmb-lines span:nth-child(3) {
width: 68%;
}
.dmb-lines span:nth-child(4) {
width: 50%;
}
.dmb-block h2 {
margin: 0 0 14px;
color: var(--dmb-ink, #111318);
font-size: 12px;
line-height: 1.2;
text-transform: uppercase;
letter-spacing: 0;
}
.dmb-block h2 span {
margin-left: 8px;
color: var(--dmb-ink-subtle, var(--dmb-link));
font-weight: 600;
text-transform: none;
}
.dmb-palette {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}
.dmb-palette article {
min-height: 122px;
border-radius: var(--dmb-radius-card, 0);
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.dmb-palette strong,
.dmb-palette code,
.dmb-palette p {
display: block;
margin: 0;
}
.dmb-palette strong {
font-size: 12px;
line-height: 1.2;
}
.dmb-palette code {
font-family: var(--dmb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 11px;
}
.dmb-palette p {
font-size: 10px;
line-height: 1.35;
opacity: 0.84;
}
.dmb-type-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
.dmb-type-list article,
.dmb-component-strip,
.dmb-component-grid,
.dmb-spacing-scale {
border: 1px solid #ededf0;
border-radius: var(--dmb-radius-card, 0);
background: #f0f0f1;
}
.dmb-type-list article {
min-height: 58px;
padding: 14px 16px;
overflow: hidden;
}
.dmb-type-list div {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 6px;
}
.dmb-type-list strong {
color: #111318;
font-size: 11px;
}
.dmb-type-list span {
color: #7d808a;
font-size: 11px;
}
.dmb-type-list p {
margin: 0;
color: #111318;
font-size: var(--dmb-type-size, 18px);
line-height: 1.04;
font-weight: var(--dmb-type-weight, 600);
}
.dmb-type-wide {
grid-column: 1 / -1;
min-height: 224px;
}
.dmb-type-wide .dmb-type-sample {
width: 100%;
}
.dmb-type-list .dmb-type-sample-zh,
.dmb-type-list .dmb-type-sample-en {
display: block;
color: inherit;
font-size: inherit;
font-weight: inherit;
font-style: normal;
overflow-wrap: anywhere;
}
.dmb-type-list .dmb-type-sample-en {
margin-top: 0.18em;
color: color-mix(in srgb, var(--dmb-link) 72%, #111318);
font-size: inherit;
font-weight: inherit;
}
.dmb-type-wide .dmb-type-sample-en {
margin-top: 0.2em;
white-space: nowrap;
overflow-wrap: normal;
word-break: normal;
}
.dmb-type-sample-fit {
max-width: 100%;
overflow: visible;
}
.dmb-type-list .dmb-type-sample-fit-inner {
display: inline-block;
color: inherit;
font: inherit;
line-height: inherit;
max-width: none;
white-space: inherit;
transform: scale(var(--dmb-type-fit-scale, 1));
transform-origin: left top;
}
.dmb-type-display p {
font-family: var(--dmb-font-display, var(--dmb-font-body, var(--dmb-font-sans)));
}
.dmb-type-body p {
font-family: var(--dmb-font-body, var(--dmb-font-sans));
line-height: 1.38;
}
.dmb-type-mono p {
font-family: var(--dmb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
line-height: 1.35;
}
.dmb-spacing-scale {
height: 116px;
padding: 24px;
display: flex;
align-items: center;
gap: 12px;
overflow-x: auto;
}
.dmb-spacing-scale div {
display: grid;
gap: 8px;
justify-items: center;
color: #777a84;
font-size: 11px;
}
.dmb-spacing-scale i {
display: block;
width: 38px;
height: 18px;
border-left: 1px solid #999ca6;
border-right: 1px solid #999ca6;
border-bottom: 1px solid #999ca6;
}
.dmb-radius-grid,
.dmb-shadow-grid,
.dmb-border-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.dmb-shadow-grid,
.dmb-border-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.dmb-radius-grid article,
.dmb-shadow-grid article,
.dmb-border-grid article {
min-height: 154px;
border: 1px solid #ededf0;
border-radius: var(--dmb-radius-card, 0);
background: #ffffff;
padding: 14px;
overflow: hidden;
}
.dmb-radius-sample {
height: 58px;
border-radius: var(--dmb-radius-demo);
background: var(--dmb-surface);
border: 1px solid var(--dmb-border);
margin-bottom: 12px;
}
.dmb-radius-grid strong,
.dmb-shadow-grid strong,
.dmb-border-grid strong {
display: block;
color: #151720;
font-size: 12px;
line-height: 1.25;
}
.dmb-radius-grid code {
display: block;
margin-top: 7px;
color: var(--dmb-link);
font-family: var(--dmb-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 11px;
}
.dmb-radius-grid p,
.dmb-shadow-grid p,
.dmb-border-grid p {
margin: 8px 0 0;
color: #676b76;
font-size: 11px;
line-height: 1.42;
}
.dmb-shadow-grid article {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.dmb-border-sample {
display: block;
height: 58px;
margin: 14px 0 12px;
border: 1px solid #dfe1e7;
border-radius: 0;
background: var(--dmb-border-sample-bg, var(--dmb-surface, var(--dmb-bg, #ffffff)));
}
.dmb-component-strip {
padding: 16px;
margin-bottom: 10px;
}
.dmb-component-strip .dmb-button-hover {
filter: brightness(0.92);
}
.dmb-component-grid {
padding: 14px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.dmb-component-grid article {
min-height: 122px;
border-radius: var(--dmb-radius-card, 0);
background: #ffffff;
border: 1px solid #e5e5e9;
padding: 14px;
}
.dmb-component-grid small {
color: var(--dmb-link);
font-size: 11px;
font-weight: 800;
}
.dmb-component-grid h3 {
margin: 12px 0 8px;
color: #151720;
font-size: 16px;
}
.dmb-component-grid p {
margin: 0;
color: #666a75;
font-size: 12px;
line-height: 1.55;
}
.dmb-dos-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.dmb-dos-grid article {
border-radius: var(--dmb-radius-card, 0);
padding: 16px;
min-height: 142px;
}
.dmb-do {
border: 1px solid #bcebd8;
background: #effbf6;
color: #0b6b43;
}
.dmb-dont {
border: 1px solid #f4c7c7;
background: #fff1f1;
color: #b32626;
}
.dmb-dos-grid strong {
display: block;
margin-bottom: 10px;
font-size: 13px;
}
.dmb-dos-grid p {
margin: 0 0 9px;
color: inherit;
font-size: 12px;
line-height: 1.45;
}
.dmb-lightbox {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(8, 10, 14, 0.88);
padding: 54px 24px 28px;
display: flex;
justify-content: center;
align-items: flex-start;
overflow: hidden;
}
.dmb-lightbox > button {
position: absolute;
top: 18px;
left: 50%;
transform: translateX(-50%);
height: 34px;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: var(--dmb-radius-control, 0);
background: rgba(255, 255, 255, 0.12);
color: #ffffff;
padding: 0 12px;
font: inherit;
font-family: var(--dmb-font-body, var(--dmb-font-sans));
font-size: 12px;
font-weight: 800;
}
.dmb-lightbox-scroll {
width: min(1040px, calc(100vw - 48px));
max-width: calc(100vw - 48px);
height: auto;
max-height: calc(100vh - 86px);
overflow-y: auto;
overflow-x: hidden;
border-radius: var(--dmb-radius-preview, 0);
background: transparent;
padding: 0;
}
.dmb-lightbox img {
width: 100%;
height: auto;
max-height: none;
display: block;
margin: 0;
border-radius: var(--dmb-radius-preview, 0);
}
.dmb-lightbox iframe {
width: 100%;
height: calc(100vh - 86px);
min-height: calc(100vh - 86px);
border: 0;
display: block;
background: #ffffff;
border-radius: var(--dmb-radius-preview, 0);
}
@media (max-width: 980px) {
.dmb-layout {
width: min(100% - 32px, 1320px);
}
.dmb-token-grid,
.dmb-type-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dmb-radius-grid,
.dmb-shadow-grid,
.dmb-border-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.dmb-layout {
width: min(100% - 20px, 1320px);
padding: 20px 0 44px;
}
.dmb-sheet {
padding: 0;
border-radius: 0;
}
.dmb-sheet-head {
align-items: flex-start;
flex-direction: column;
}
.dmb-sheet-actions,
.dmb-tabs {
width: 100%;
}
.dmb-sheet-actions {
justify-content: flex-start;
}
.dmb-sheet-links {
width: auto;
}
.dmb-tabs button {
flex: 1 1 0;
}
.dmb-preview {
height: 360px;
}
.dmb-overview-meta {
grid-template-columns: 1fr;
}
.dmb-overview-meta .dmb-tags {
justify-content: flex-start;
max-width: none;
}
.dmb-lightbox {
padding: 52px 12px 16px;
}
.dmb-lightbox-scroll {
width: min(720px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
max-height: calc(100vh - 68px);
}
.dmb-token-card {
grid-template-columns: 1fr;
}
.dmb-token-grid,
.dmb-type-list,
.dmb-palette,
.dmb-radius-grid,
.dmb-shadow-grid,
.dmb-border-grid,
.dmb-component-grid,
.dmb-dos-grid {
grid-template-columns: 1fr;
}
.dmb-sheet-head h1 {
font-size: 32px;
}
.dmb-sheet-head span {
font-size: 15px;
}
}

View File

@@ -0,0 +1,28 @@
export type BatchShowcaseSourceLinks = {
sourceName?: string;
originalDetailUrl?: string;
websiteUrl?: string;
};
export type HeaderSourceLink = {
ariaLabel: string;
href: string;
kind: 'website' | 'source';
label: string;
title: string;
};
function isExternalUrl(value?: string) {
return Boolean(value && /^https?:\/\//i.test(value.trim()));
}
export function getHeaderLinks(source?: BatchShowcaseSourceLinks): HeaderSourceLink[] {
return [
source?.websiteUrl && isExternalUrl(source.websiteUrl)
? { ariaLabel: '打开品牌官网', href: source.websiteUrl, kind: 'website', label: '官网', title: '品牌官网' }
: null,
source?.originalDetailUrl && isExternalUrl(source.originalDetailUrl)
? { ariaLabel: '打开采集来源', href: source.originalDetailUrl, kind: 'source', label: '来源', title: '主题来源' }
: null,
].filter((item): item is HeaderSourceLink => Boolean(item));
}

View File

@@ -0,0 +1,802 @@
import './base.css';
import { FileSearch, Globe2 } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import { getHeaderLinks, type BatchShowcaseSourceLinks } from './headerLinks';
type TypographyFontRole = 'display' | 'body' | 'mono';
export type BatchPreviewImage = {
type: string;
url: string;
};
export type BatchPaletteSwatch = {
color: string;
labelZh: string;
labelEn: string;
textColor: string;
};
export type BatchStyleSystemItem = {
label: string;
value: string;
description?: string;
cssValue?: string;
};
export type BatchShowcaseConfig = {
brand: string;
brandAlias?: string;
description: string;
descriptionEn?: string;
source?: BatchShowcaseSourceLinks;
variant: 'saas-devtool' | 'dashboard' | 'consumer-commerce' | 'editorial-agency' | 'dark-experimental';
distributionTags: string[];
fontStylesheets?: string[];
palette: Array<string | BatchPaletteSwatch>;
radius?: {
control?: string;
card?: string;
preview?: string;
pill?: string;
source?: string;
};
spacing?: Record<string, string>;
shadows?: BatchStyleSystemItem[];
borders?: BatchStyleSystemItem[];
typography: string[];
previewImages: BatchPreviewImage[];
panels: {
title: string;
eyebrow: string;
body: string;
}[];
usageGuidance?: {
do: string[];
dont: string[];
};
};
export type BatchShowcaseTab = {
id: string;
label: string;
content: React.ReactNode;
};
export type DesignMdBatchShowcaseProps = {
config: BatchShowcaseConfig;
tabs?: BatchShowcaseTab[];
className?: string;
};
const variantLabels: Record<BatchShowcaseConfig['variant'], string> = {
'saas-devtool': 'SaaS 开发工具 - SaaS / Devtool',
dashboard: '数据仪表盘 - Dashboard',
'consumer-commerce': '消费与商业 - Consumer / Commerce',
'editorial-agency': '编辑与机构 - Editorial / Agency',
'dark-experimental': '暗色实验 - Dark / Experimental',
};
const NON_SELECTION_TAGS = new Set([
'DESIGN.md',
'设计 Token',
'品牌指南',
'GitHub Raw',
'Tailwind v4',
'CSS 变量',
'图片资产',
'浏览器复制',
'目录来源',
]);
const typographyRows = [
{
name: '展示标题 - Display',
fontRole: 'display',
layout: 'wide',
size: '72px',
weight: 'bold',
cssWeight: 800,
zh: '主题字形展示',
en: 'Design System Display',
},
{
name: '页面标题 - Heading',
fontRole: 'display',
layout: 'wide',
size: '48px',
weight: 'bold',
cssWeight: 800,
zh: '标题层级',
en: 'Product Experience',
},
{
name: '分区标题 - Section Heading',
fontRole: 'display',
layout: 'compact',
size: '28px',
weight: 'bold',
cssWeight: 800,
zh: '视觉系统',
en: 'Visual Language',
},
{
name: '副标题 - Subhead',
fontRole: 'body',
layout: 'compact',
size: '20px',
weight: 'semibold',
cssWeight: 600,
zh: '清晰的信息节奏',
en: 'A clear rhythm for product stories.',
},
{
name: '正文 - Body',
fontRole: 'body',
layout: 'compact',
size: '15px',
weight: 'regular',
cssWeight: 400,
zh: '每一种字体都让主题拥有不同的声音。',
en: 'Typography gives every theme its own voice.',
},
{
name: '小字 - Small',
fontRole: 'body',
layout: 'compact',
size: '12px',
weight: 'regular',
cssWeight: 400,
zh: '辅助说明与状态信息',
en: 'Helper text and interface states',
},
{
name: '注释 - Caption',
fontRole: 'mono',
layout: 'compact',
size: '11px',
weight: 'regular',
cssWeight: 400,
zh: '来源注释与细节标记',
en: 'Source notes and fine details',
},
] satisfies Array<{
name: string;
fontRole: TypographyFontRole;
layout: 'wide' | 'compact';
size: string;
weight: string;
cssWeight: number;
zh: string;
en: string;
}>;
function unique(items: string[]) {
return Array.from(new Set(items.filter(Boolean)));
}
function themeSelectionTags(tags: string[]) {
return unique(tags.map(tag => tag.trim()).filter(tag => !NON_SELECTION_TAGS.has(tag)));
}
function themeSelectionDescription(description: string) {
const text = Array.from(NON_SELECTION_TAGS).reduce(
(current, tag) => current.replace(new RegExp(`、?${tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g'), ''),
description,
);
return text
.replace(/围绕、/g, '围绕')
.replace(/围绕组织页面/g, '围绕品牌视觉、产品场景组织页面')
.replace(/、、+/g, '、');
}
function contrastFor(color: string) {
const value = color.replace('#', '').slice(0, 6);
if (value.length !== 6) return '#171717';
const r = parseInt(value.slice(0, 2), 16);
const g = parseInt(value.slice(2, 4), 16);
const b = parseInt(value.slice(4, 6), 16);
return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#171717' : '#ffffff';
}
function normalizePalette(palette: Array<string | BatchPaletteSwatch>): BatchPaletteSwatch[] {
const labels = [
['主色', 'Primary'],
['主色悬停', 'Primary Hover'],
['辅助色', 'Secondary'],
['强调色', 'Accent'],
['背景色', 'Background'],
['表面色', 'Surface'],
['文本色', 'Text'],
['边框色', 'Border'],
];
return palette.slice(0, 12).map((item, index) => {
if (typeof item !== 'string') return item;
const label = labels[index] || [`颜色 ${index + 1}`, `Color ${index + 1}`];
return {
color: item,
labelZh: label[0],
labelEn: label[1],
textColor: contrastFor(item),
};
});
}
function uniquePaletteByColor(palette: BatchPaletteSwatch[]): BatchPaletteSwatch[] {
const seen = new Set<string>();
return palette.filter(swatch => {
const key = swatch.color.trim().toLowerCase();
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
}
function useThemeFontStylesheets(urls: string[] = []) {
useEffect(() => {
if (typeof document === 'undefined' || urls.length === 0) return;
const links = urls.map(url => {
const existing = document.querySelector<HTMLLinkElement>(`link[data-dmb-font="${url}"]`);
if (existing) return existing;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.dataset.dmbFont = url;
document.head.appendChild(link);
return link;
});
return () => {
links.forEach(link => {
if (document.querySelectorAll(`link[data-dmb-font="${link.dataset.dmbFont}"]`).length === 1) {
link.remove();
}
});
};
}, [urls.join('|')]);
}
function FittedSampleText({
children,
className,
lang,
}: {
children: string;
className: string;
lang: string;
}) {
const containerRef = useRef<HTMLSpanElement>(null);
const textRef = useRef<HTMLSpanElement>(null);
const [fitScale, setFitScale] = useState(1);
useEffect(() => {
const container = containerRef.current;
const text = textRef.current;
if (!container || !text) return;
let active = true;
let frame = 0;
const updateFit = () => {
if (!active) return;
window.cancelAnimationFrame(frame);
frame = window.requestAnimationFrame(() => {
if (!active) return;
const availableWidth = container.clientWidth;
const textWidth = text.scrollWidth;
const nextScale = availableWidth > 0 && textWidth > 0
? Math.min(1, availableWidth / textWidth)
: 1;
setFitScale(current => (Math.abs(current - nextScale) < 0.005 ? current : nextScale));
});
};
updateFit();
let resizeObserver: ResizeObserver | null = null;
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(updateFit);
resizeObserver.observe(container);
resizeObserver.observe(text);
}
document.fonts?.ready.then(updateFit).catch(() => {});
window.addEventListener('resize', updateFit);
return () => {
active = false;
window.cancelAnimationFrame(frame);
resizeObserver?.disconnect();
window.removeEventListener('resize', updateFit);
};
}, [children]);
return (
<span
ref={containerRef}
className={`${className} dmb-type-sample-fit`}
lang={lang}
style={{ '--dmb-type-fit-scale': fitScale } as React.CSSProperties}
>
<span ref={textRef} className="dmb-type-sample-fit-inner">{children}</span>
</span>
);
}
function TagList({ tags }: { tags: string[] }) {
if (tags.length === 0) return <span className="dmb-muted"></span>;
return (
<div className="dmb-tags">
{tags.map(tag => <span key={tag}>{tag}</span>)}
</div>
);
}
function PreviewFigure({ config, onOpen }: { config: BatchShowcaseConfig; onOpen: (url: string) => void }) {
const primaryImage = config.previewImages[0];
const imageLabel = config.brandAlias || config.brand;
if (!primaryImage) {
return (
<button className="dmb-preview dmb-preview-empty" type="button" aria-label={`${imageLabel} preview pending`}>
<strong>{config.brand}</strong>
<span></span>
</button>
);
}
if (primaryImage.type === 'source-preview-html') {
return (
<button className="dmb-preview dmb-preview-iframe" type="button" onClick={() => onOpen(primaryImage.url)} aria-label={`Open ${imageLabel} source preview`}>
<span className="dmb-preview-canvas">
<iframe src={primaryImage.url} title={`${imageLabel} source preview`} loading="lazy" sandbox="allow-same-origin" />
</span>
<span className="dmb-preview-label"> - Source Preview</span>
</button>
);
}
return (
<button className="dmb-preview" type="button" onClick={() => onOpen(primaryImage.url)} aria-label={`Open ${imageLabel} preview`}>
<span className="dmb-preview-canvas">
<img src={primaryImage.url} alt={`${imageLabel} ${primaryImage.type} preview`} loading="lazy" />
</span>
<span className="dmb-preview-label">{primaryImage.type}</span>
</button>
);
}
function PaletteSystem({ palette }: { palette: BatchPaletteSwatch[] }) {
const colors = uniquePaletteByColor(palette).slice(0, 12);
return (
<section className="dmb-block">
<h2> - Color Palette</h2>
<div className="dmb-palette">
{colors.map((swatch, index) => (
<article key={`${swatch.color}-${index}`} style={{ backgroundColor: swatch.color, color: swatch.textColor }}>
<strong>{swatch.labelZh} - {swatch.labelEn}</strong>
<code>{swatch.color}</code>
<p>{index < 4 ? '用于主题预览和生成组件的核心色彩。' : '从来源元数据中提取的辅助色彩。'}</p>
</article>
))}
</div>
</section>
);
}
function TokenOverview({ config }: { config: BatchShowcaseConfig }) {
const uniquePalette = uniquePaletteByColor(normalizePalette(config.palette));
const primary = uniquePalette[0] || { labelZh: '主色', labelEn: 'Primary', color: 'var(--dmb-accent)', textColor: 'var(--dmb-accent-contrast)' };
const secondary = uniquePalette.find(swatch => swatch.color.toLowerCase() !== primary.color.toLowerCase()) || { labelZh: '辅助色', labelEn: 'Secondary', color: 'var(--dmb-link)', textColor: '#ffffff' };
const neutral = uniquePalette.find(swatch => ['#000000', '#ffffff'].includes(swatch.color.toLowerCase())
&& ![primary.color.toLowerCase(), secondary.color.toLowerCase()].includes(swatch.color.toLowerCase()));
const background = uniquePalette.find(swatch => ['#fafafa', '#ffffff', '#fbfbfd', '#fffefb'].includes(swatch.color.toLowerCase())
&& ![primary.color.toLowerCase(), secondary.color.toLowerCase(), neutral?.color.toLowerCase()].filter(Boolean).includes(swatch.color.toLowerCase()));
const selectedSummarySwatches = [primary, secondary, neutral, background]
.filter((swatch): swatch is BatchPaletteSwatch => Boolean(swatch));
const summarySwatches = uniquePaletteByColor(selectedSummarySwatches);
return (
<section className="dmb-token-card">
<div className="dmb-token-rail">
{summarySwatches.map(swatch => (
<div key={`${swatch.labelEn}-${swatch.color}`} className="dmb-token-chip" style={{ backgroundColor: swatch.color, color: swatch.textColor }}>
<strong>{swatch.labelZh} - {swatch.labelEn}</strong>
<code>{swatch.color}</code>
</div>
))}
</div>
<div className="dmb-token-grid">
<div>
<small> - Headline</small>
<strong>Aa</strong>
</div>
<div className="dmb-token-buttons">
<button type="button"><br /><span>Primary</span></button>
<button type="button" className="dmb-text-button"><br /><span>Secondary</span></button>
<button type="button" className="dmb-outline-button"><br /><span>Outline</span></button>
</div>
<div>
<input aria-label="搜索示例" placeholder="搜索 / Search" />
</div>
<div>
<small> - Body</small>
<strong>Aa</strong>
</div>
<div className="dmb-lines">
<span />
<span />
<span />
<span />
</div>
</div>
</section>
);
}
function TypographySystem({ typography }: { typography: string[] }) {
const fontNames = {
display: typography[0] || 'System Sans',
body: typography[1] || typography[0] || 'System Sans',
mono: typography[2] || 'ui-monospace',
};
const fontNameForRole = (role: TypographyFontRole) => {
if (role === 'body') return fontNames.body;
if (role === 'mono') return fontNames.mono;
return fontNames.display;
};
return (
<section className="dmb-block">
<h2> - Typography</h2>
<div className="dmb-type-list">
{typographyRows.map(row => (
<article
key={row.name}
className={`dmb-type-${row.fontRole} dmb-type-${row.layout}`}
style={{
'--dmb-type-size': row.size,
'--dmb-type-weight': row.cssWeight,
} as React.CSSProperties}
>
<div>
<strong>{row.name}</strong>
<span>{fontNameForRole(row.fontRole)} · {row.size} · {row.weight}</span>
</div>
<p className="dmb-type-sample">
<span className="dmb-type-sample-zh" lang="zh-CN">{row.zh}</span>
{row.layout === 'wide' ? (
<FittedSampleText className="dmb-type-sample-en" lang="en">{row.en}</FittedSampleText>
) : (
<span className="dmb-type-sample-en" lang="en">{row.en}</span>
)}
</p>
</article>
))}
</div>
</section>
);
}
function SpacingSystem() {
const marks = ['4px', '8px', '12px', '16px', '24px', '32px', '48px', '64px', '80px'];
return (
<section className="dmb-block">
<h2> - Spacing <span> · 4px</span></h2>
<div className="dmb-spacing-scale">
{marks.map(mark => (
<div key={mark}>
<i />
<span>{mark}</span>
</div>
))}
</div>
</section>
);
}
function RadiusSystem({ radius }: { radius?: BatchShowcaseConfig['radius'] }) {
if (radius?.source !== 'design-md') return null;
const radiusItems = [
radius?.control ? ['控件 - Control', radius.control, '按钮、输入框、分段控件'] : null,
radius?.card ? ['卡片 - Card', radius.card, '内容容器、面板、浮层'] : null,
radius?.preview ? ['预览 - Preview', radius.preview, '截图、画布、媒体框'] : null,
radius?.pill ? ['胶囊 - Pill', radius.pill, '标签、状态点、短操作'] : null,
].filter((item): item is [string, string, string] => Boolean(item));
if (radiusItems.length === 0) return null;
return (
<section className="dmb-block">
<h2> - Radius</h2>
<div className="dmb-radius-grid">
{radiusItems.map(([label, value, usage]) => (
<article key={label} style={{ '--dmb-radius-demo': value } as React.CSSProperties}>
<div className="dmb-radius-sample" />
<strong>{label}</strong>
<code>{value}</code>
<p>{usage}</p>
</article>
))}
</div>
</section>
);
}
function ShadowSystem({ shadows }: { shadows?: BatchStyleSystemItem[] }) {
if (!shadows?.length) return null;
return (
<section className="dmb-block">
<h2> - Shadow</h2>
<div className="dmb-shadow-grid">
{shadows.map(item => (
<article key={`${item.label}-${item.value}`} style={item.cssValue ? { boxShadow: item.cssValue } : undefined}>
<strong>{item.label}</strong>
<code>{item.value}</code>
{item.description ? <p>{item.description}</p> : null}
</article>
))}
</div>
</section>
);
}
function BorderSystem({ borders }: { borders?: BatchStyleSystemItem[] }) {
if (!borders?.length) return null;
return (
<section className="dmb-block">
<h2> - Border</h2>
<div className="dmb-border-grid">
{borders.map(item => (
<article key={`${item.label}-${item.value}`}>
<strong>{item.label}</strong>
<span
className="dmb-border-sample"
style={item.cssValue ? { borderColor: item.cssValue } : undefined}
/>
<code>{item.value}</code>
{item.description ? <p>{item.description}</p> : null}
</article>
))}
</div>
</section>
);
}
function ComponentsSystem({ panels }: { panels: BatchShowcaseConfig['panels'] }) {
return (
<section className="dmb-block">
<h2> - Components</h2>
<div className="dmb-component-strip">
<button type="button"></button>
<button type="button" className="dmb-button-hover"></button>
<button type="button" className="dmb-outline-button"></button>
</div>
<div className="dmb-component-grid">
{panels.map(panel => (
<article key={panel.title}>
<small>{panel.eyebrow}</small>
<h3>{panel.title}</h3>
<p>{panel.body}</p>
</article>
))}
</div>
</section>
);
}
function DosDonts({ config }: { config: BatchShowcaseConfig }) {
const usageGuidance = config.usageGuidance;
if (!usageGuidance?.do?.length || !usageGuidance?.dont?.length) return null;
return (
<section className="dmb-block">
<h2>使 - Do's & Don'ts</h2>
<div className="dmb-dos-grid">
<article className="dmb-do">
<strong> - Do</strong>
{usageGuidance.do.map(item => <p key={item}>{item}</p>)}
</article>
<article className="dmb-dont">
<strong> - Don't</strong>
{usageGuidance.dont.map(item => <p key={item}>{item}</p>)}
</article>
</div>
</section>
);
}
function ResourceTabs({
activeTabId,
tabs,
onSelect,
}: {
activeTabId: string;
tabs: BatchShowcaseTab[];
onSelect: (tabId: string) => void;
}) {
if (tabs.length === 0) return null;
return (
<div className="dmb-tabs" role="tablist" aria-label="Theme resource sections">
<button
id="dmb-tab-design-md"
type="button"
role="tab"
aria-selected={activeTabId === 'design-md'}
aria-controls="dmb-panel-design-md"
onClick={() => onSelect('design-md')}
>
规范
</button>
{tabs.map(tab => (
<button
id={`dmb-tab-${tab.id}`}
key={tab.id}
type="button"
role="tab"
aria-selected={activeTabId === tab.id}
aria-controls={`dmb-panel-${tab.id}`}
onClick={() => onSelect(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}
function HeaderLinks({ source }: { source?: BatchShowcaseSourceLinks }) {
const links = getHeaderLinks(source);
if (links.length === 0) return null;
return (
<nav className="dmb-sheet-links" aria-label="主题来源链接">
{links.map(link => {
const Icon = link.kind === 'website' ? Globe2 : FileSearch;
return (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noreferrer"
aria-label={link.ariaLabel}
data-label={link.title}
>
<Icon aria-hidden="true" focusable="false" strokeWidth={1.9} />
</a>
);
})}
</nav>
);
}
function HeaderActions({
activeTabId,
onSelect,
source,
tabs,
}: {
activeTabId: string;
onSelect: (tabId: string) => void;
source?: BatchShowcaseSourceLinks;
tabs: BatchShowcaseTab[];
}) {
const links = getHeaderLinks(source);
const hasTabs = tabs.length > 0;
const hasActions = Boolean(links.length || hasTabs);
if (!hasActions) return null;
return (
<div className="dmb-sheet-actions">
{links.length > 0 ? <HeaderLinks source={source} /> : null}
{hasTabs ? <ResourceTabs activeTabId={activeTabId} tabs={tabs} onSelect={onSelect} /> : null}
</div>
);
}
export function DesignMdBatchShowcase({ config, tabs = [], className = '' }: DesignMdBatchShowcaseProps) {
const [zoomImage, setZoomImage] = useState<string | null>(null);
const [activeTabId, setActiveTabId] = useState('design-md');
useThemeFontStylesheets(config.fontStylesheets);
const tags = themeSelectionTags(config.distributionTags).slice(0, 14);
const description = themeSelectionDescription(config.description);
const imageLabel = config.brandAlias || config.brand;
const headerTitle = config.brandAlias || config.brand.replace(/\s*主题$/, '');
const palette = normalizePalette(config.palette);
const hasResourceTabs = tabs.length > 0;
const activeTab = tabs.find(tab => activeTabId === tab.id);
const sheetHead = (
<div className="dmb-sheet-head">
<div className="dmb-sheet-head-main">
<p>{variantLabels[config.variant]}</p>
<h1>{headerTitle}</h1>
</div>
<HeaderActions
activeTabId={activeTabId}
source={config.source}
tabs={tabs}
onSelect={setActiveTabId}
/>
</div>
);
const overviewContent = (
<>
<PreviewFigure config={config} onOpen={setZoomImage} />
<div className="dmb-overview-meta">
<div className="dmb-description">
<p>{description || `${imageLabel} 的 Design.md 主题展示。`}</p>
{config.descriptionEn ? <p lang="en">{config.descriptionEn}</p> : null}
</div>
<TagList tags={tags} />
</div>
<TokenOverview config={config} />
<PaletteSystem palette={palette} />
<TypographySystem typography={config.typography} />
<SpacingSystem />
<RadiusSystem radius={config.radius} />
<ShadowSystem shadows={config.shadows} />
<BorderSystem borders={config.borders} />
<ComponentsSystem panels={config.panels} />
<DosDonts config={config} />
</>
);
return (
<main className={['dmb-page', `dmb-variant-${config.variant}`, className].filter(Boolean).join(' ')}>
<div className="dmb-layout">
<article className="dmb-sheet">
{sheetHead}
{hasResourceTabs && activeTab ? (
<div
id={`dmb-panel-${activeTab.id}`}
className="dmb-tab-panel"
role="tabpanel"
aria-labelledby={`dmb-tab-${activeTab.id}`}
>
{activeTab.content}
</div>
) : (
<div
id="dmb-panel-design-md"
className={hasResourceTabs ? 'dmb-tab-panel' : undefined}
role={hasResourceTabs ? 'tabpanel' : undefined}
aria-labelledby={hasResourceTabs ? 'dmb-tab-design-md' : undefined}
>
{overviewContent}
</div>
)}
</article>
</div>
{zoomImage ? (
<div className="dmb-lightbox" role="dialog" aria-modal="true" aria-label={`${imageLabel} enlarged preview`}>
<button type="button" onClick={() => setZoomImage(null)} aria-label="Close preview">关闭 - Close</button>
<div className="dmb-lightbox-scroll">
{zoomImage.endsWith('.html') ? (
<iframe src={zoomImage} title={`${imageLabel} enlarged source preview`} sandbox="allow-same-origin" />
) : (
<img src={zoomImage} alt={`${imageLabel} enlarged preview`} />
)}
</div>
</div>
) : null}
</main>
);
}
export default DesignMdBatchShowcase;

View File

@@ -0,0 +1,36 @@
/**
* ThemeShell 图标组件
*/
import React from 'react';
export const Icons = {
Menu: () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
),
Collapse: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7" />
<polyline points="18 17 13 12 18 7" />
</svg>
),
Expand: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7" />
<polyline points="6 17 11 12 6 7" />
</svg>
),
Close: () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
),
};

View File

@@ -0,0 +1,130 @@
/**
* MarkdownViewer - 轻量级 Markdown 渲染组件
*
* 零依赖,支持基础 Markdown 语法:
* - 标题 (h1-h3)
* - 列表 (有序/无序)
* - 引用
* - 行内代码
* - 加粗
*/
import React from 'react';
export interface MarkdownViewerProps {
content: string;
className?: string;
}
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, className }) => {
// 解析行内样式
const parseInlineStyles = (text: string): React.ReactNode => {
// 处理行内代码 `code`
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code
key={i}
style={{
backgroundColor: 'var(--theme-bg-tertiary, rgba(0, 0, 0, 0.04))',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '0.9em',
fontFamily: 'Monaco, Consolas, monospace',
color: 'var(--theme-text-primary, #1a1a1a)',
}}
>
{part.slice(1, -1)}
</code>
);
}
// 处理加粗 **text**
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((bp, j) => {
if (bp.startsWith('**') && bp.endsWith('**')) {
return <strong key={`${i}-${j}`}>{bp.slice(2, -2)}</strong>;
}
return bp;
});
});
};
const parseLine = (line: string, index: number): React.ReactNode => {
// H3
if (line.startsWith('### ')) {
return (
<h3 key={index} style={{ fontSize: '18px', fontWeight: 600, marginTop: '24px', marginBottom: '12px', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(4))}
</h3>
);
}
// H2
if (line.startsWith('## ')) {
return (
<h2 key={index} style={{ fontSize: '22px', fontWeight: 600, marginTop: '32px', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid var(--theme-border-light, rgba(0,0,0,0.06))', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(3))}
</h2>
);
}
// H1
if (line.startsWith('# ')) {
return (
<h1 key={index} style={{ fontSize: '28px', fontWeight: 600, marginBottom: '20px', color: 'var(--theme-text-primary, #1a1a1a)' }}>
{parseInlineStyles(line.slice(2))}
</h1>
);
}
// 无序列表
if (line.startsWith('- ')) {
return (
<li key={index} style={{ marginLeft: '20px', marginBottom: '8px', listStyle: 'disc', color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))' }}>
{parseInlineStyles(line.slice(2))}
</li>
);
}
// 有序列表
if (/^\d+\.\s/.test(line)) {
const match = line.match(/^\d+\.\s(.*)$/);
if (match) {
return (
<li key={index} style={{ marginLeft: '20px', marginBottom: '8px', listStyle: 'decimal', color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))' }}>
{parseInlineStyles(match[1])}
</li>
);
}
}
// 引用
if (line.startsWith('> ')) {
return (
<blockquote key={index} style={{ marginLeft: 0, paddingLeft: '16px', borderLeft: '3px solid var(--theme-border, rgba(0,0,0,0.1))', color: 'var(--theme-text-tertiary, rgba(0,0,0,0.55))', fontStyle: 'italic', margin: '16px 0' }}>
{parseInlineStyles(line.slice(2))}
</blockquote>
);
}
// 分隔线
if (line.trim() === '---') {
return <hr key={index} style={{ border: 'none', borderTop: '1px solid var(--theme-border-light, rgba(0,0,0,0.06))', margin: '24px 0' }} />;
}
// 空行
if (line.trim() === '') {
return <div key={index} style={{ height: '12px' }} />;
}
// 普通段落
return (
<p key={index} style={{ color: 'var(--theme-text-secondary, rgba(0,0,0,0.65))', lineHeight: 1.7, marginBottom: '12px' }}>
{parseInlineStyles(line)}
</p>
);
};
const lines = content.split('\n');
return (
<div className={className} style={{ fontFamily: 'inherit' }}>
{lines.map((line, i) => parseLine(line, i))}
</div>
);
};
export default MarkdownViewer;

View File

@@ -0,0 +1,330 @@
/**
* ThemeShell - 去主题化的设计系统展示组件
*
* 核心特性:
* - 零视觉干扰:使用中性色,不抢占内容焦点
* - 完全可配置:标题、分组、导航项均通过 props 配置
* - 响应式支持:桌面端侧边栏 + 移动端抽屉式导航
* - 可折叠侧边栏:支持展开/收起
* - 主题支持:支持深色/浅色主题配置
*/
import React, { useState, useCallback, useMemo, CSSProperties } from 'react';
import { ThemeShellProps, NavGroup, NavItem, ThemeColors } from './types';
import { createShellStyles, getThemeColors } from './styles';
import { Icons } from './Icons';
// 单个导航项组件 - 使用独立的 hover 状态
const NavItemButton: React.FC<{
item: NavItem;
isActive: boolean;
onNavigate: (id: string) => void;
styles: Record<string, CSSProperties>;
colors: ThemeColors;
}> = ({ item, isActive, onNavigate, styles, colors }) => {
const [isHovered, setIsHovered] = useState(false);
// 点击后清除 hover 状态
const handleClick = () => {
setIsHovered(false);
onNavigate(item.id);
};
// 只有当前激活项显示激活样式,其他项只在 hover 时显示 hover 样式
const style: CSSProperties = {
...styles.navItem,
...(isActive ? styles.navItemActive : {}),
...(!isActive && isHovered ? styles.navItemHover : {}),
};
return (
<li>
<button
onClick={handleClick}
style={style}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{item.icon && <span style={{ marginRight: 8 }}>{item.icon}</span>}
{item.label}
</button>
</li>
);
};
export const ThemeShell: React.FC<ThemeShellProps> = ({
brand,
groups,
items,
activeId,
onNavigate,
sidebar,
theme,
children,
header,
className,
style,
}) => {
const [sidebarOpen, setSidebarOpen] = useState(sidebar?.defaultOpen ?? true);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const sidebarWidth = sidebar?.width ?? 256;
const collapsible = sidebar?.collapsible ?? true;
// 计算主题颜色和样式
const themeColors = useMemo(() =>
getThemeColors(theme?.mode ?? 'light', theme?.colors),
[theme?.mode, theme?.colors]
);
const STYLES = useMemo(() =>
createShellStyles(themeColors),
[themeColors]
);
// 按分组组织导航项
const sortedGroups = [...groups].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
const getItemsByGroup = useCallback(
(groupId: string): NavItem[] => items.filter(item => item.groupId === groupId),
[items]
);
// 处理导航点击
const handleNavigate = useCallback(
(id: string) => {
onNavigate(id);
setMobileMenuOpen(false);
},
[onNavigate]
);
// 渲染单个导航项
const renderNavItem = (item: NavItem) => {
const isActive = item.id === activeId;
return (
<NavItemButton
key={item.id}
item={item}
isActive={isActive}
onNavigate={handleNavigate}
styles={STYLES}
colors={themeColors}
/>
);
};
// 渲染导航分组
const renderNavGroup = (group: NavGroup) => {
const groupItems = getItemsByGroup(group.id);
if (groupItems.length === 0) return null;
return (
<div key={group.id} style={STYLES.navGroup}>
<div style={STYLES.navGroupTitle}>{group.title}</div>
<ul style={STYLES.navList}>
{groupItems.map(renderNavItem)}
</ul>
</div>
);
};
// 渲染品牌区域
const renderBrand = () => {
if (!brand) return null;
return (
<div style={STYLES.brandArea}>
<div style={STYLES.brandContent as CSSProperties}>
<div style={STYLES.brandText as CSSProperties}>
<span style={STYLES.brandName}>{brand.name}</span>
{brand.subtitle && (
<span style={STYLES.brandSubtitle}>{brand.subtitle}</span>
)}
</div>
</div>
{collapsible && (
<button
onClick={() => setSidebarOpen(false)}
style={STYLES.collapseBtn}
title="收起侧边栏"
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgHover;
e.currentTarget.style.color = themeColors.textPrimary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = themeColors.textTertiary;
}}
>
<Icons.Collapse />
</button>
)}
</div>
);
};
// 渲染侧边栏内容
const renderSidebarContent = () => (
<>
{renderBrand()}
<nav className="theme-shell-scroll" style={STYLES.navArea as CSSProperties}>
{sortedGroups.map(renderNavGroup)}
</nav>
</>
);
// 桌面端侧边栏样式
const desktopSidebarStyle: CSSProperties = {
...STYLES.sidebar as CSSProperties,
...(sidebarOpen
? { ...STYLES.sidebarOpen, width: sidebarWidth }
: STYLES.sidebarClosed),
};
// 移动端侧边栏样式
const mobileSidebarStyle: CSSProperties = {
...STYLES.mobileSidebar as CSSProperties,
...(mobileMenuOpen ? STYLES.mobileSidebarOpen : {}),
};
return (
<div
className={className}
style={{
...STYLES.root as CSSProperties,
'--theme-text-primary': themeColors.textPrimary,
'--theme-text-secondary': themeColors.textSecondary,
'--theme-text-tertiary': themeColors.textTertiary,
'--theme-text-muted': themeColors.textMuted,
'--theme-border': themeColors.border,
'--theme-border-light': themeColors.borderLight,
'--theme-bg-secondary': themeColors.bgSecondary,
'--theme-bg-tertiary': themeColors.bgTertiary,
...style,
} as CSSProperties}
>
<style>{`
.theme-shell-scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
.theme-shell-scroll::-webkit-scrollbar {
width: 0;
height: 0;
}
.theme-shell-scroll::-webkit-scrollbar-track {
background: transparent;
}
.theme-shell-scroll::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 0;
}
.theme-shell-scroll:hover::-webkit-scrollbar-thumb {
background-color: transparent;
}
`}</style>
{/* 移动端顶栏 */}
<div
className="theme-shell-mobile-topbar"
style={{
...STYLES.mobileTopbar as CSSProperties,
display: 'none', // 默认隐藏CSS 媒体查询控制
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{brand && <span style={{ fontWeight: 600, fontSize: 15 }}>{brand.name}</span>}
</div>
<button
onClick={() => setMobileMenuOpen(true)}
style={{
background: 'none',
border: 'none',
padding: 8,
cursor: 'pointer',
color: themeColors.textSecondary,
}}
>
<Icons.Menu />
</button>
</div>
{/* 移动端遮罩 */}
{mobileMenuOpen && (
<div
style={STYLES.mobileOverlay as CSSProperties}
onClick={() => setMobileMenuOpen(false)}
/>
)}
{/* 移动端侧边栏 */}
<aside className="theme-shell-mobile-sidebar" style={mobileSidebarStyle}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ ...STYLES.brandArea as CSSProperties, justifyContent: 'space-between' }}>
{brand && (
<div style={STYLES.brandContent as CSSProperties}>
<div style={STYLES.brandText as CSSProperties}>
<span style={STYLES.brandName}>{brand.name}</span>
{brand.subtitle && (
<span style={STYLES.brandSubtitle}>{brand.subtitle}</span>
)}
</div>
</div>
)}
<button
onClick={() => setMobileMenuOpen(false)}
style={{
background: 'none',
border: 'none',
padding: 4,
cursor: 'pointer',
color: themeColors.textTertiary,
}}
>
<Icons.Close />
</button>
</div>
<nav className="theme-shell-scroll" style={STYLES.navArea as CSSProperties}>
{sortedGroups.map(renderNavGroup)}
</nav>
</div>
</aside>
{/* 桌面端侧边栏 */}
<aside className="theme-shell-sidebar" style={desktopSidebarStyle}>
{renderSidebarContent()}
</aside>
{/* 展开按钮(侧边栏关闭时) */}
{!sidebarOpen && collapsible && (
<button
className="theme-shell-expand-btn"
onClick={() => setSidebarOpen(true)}
style={STYLES.expandBtn as CSSProperties}
title="展开侧边栏"
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgHover;
e.currentTarget.style.color = themeColors.textPrimary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = themeColors.bgPrimary;
e.currentTarget.style.color = themeColors.textSecondary;
}}
>
<Icons.Expand />
</button>
)}
{/* 主内容区 */}
<main style={STYLES.main as CSSProperties}>
{/* 可选头部区域 */}
{header && <div style={STYLES.header as CSSProperties}>{header}</div>}
{/* 内容区 */}
<div style={STYLES.content as CSSProperties}>{children}</div>
</main>
</div>
);
};
export default ThemeShell;

View File

@@ -0,0 +1,10 @@
/**
* ThemeShell - 去主题化的设计系统展示组件
*
* 导出模块
*/
export { ThemeShell, default } from './ThemeShell';
export * from './types';
export { MarkdownViewer } from './MarkdownViewer';
export type { MarkdownViewerProps } from './MarkdownViewer';

View File

@@ -0,0 +1,314 @@
/**
* ThemeShell 样式定义
*
* 支持深色/浅色主题 - 使用中性色,减少视觉干扰
*/
import { CSSProperties } from 'react';
import { ThemeColors, ThemeMode } from './types';
// 浅色主题色板
export const LIGHT_COLORS: ThemeColors = {
// 文字
textPrimary: '#1a1a1a',
textSecondary: '#666666',
textTertiary: '#999999',
textMuted: '#b3b3b3',
// 背景
bgPrimary: '#ffffff',
bgSecondary: '#fafafa',
bgTertiary: '#f5f5f5',
bgHover: '#f0f0f0',
bgActive: '#e8e8e8',
// 边框
border: '#e5e5e5',
borderLight: '#f0f0f0',
// 状态指示
activeIndicator: '#1a1a1a',
};
// 深色主题色板
export const DARK_COLORS: ThemeColors = {
// 文字
textPrimary: '#f5f5f5',
textSecondary: '#a0a0a0',
textTertiary: '#707070',
textMuted: '#505050',
// 背景
bgPrimary: '#1a1a1a',
bgSecondary: '#141414',
bgTertiary: '#242424',
bgHover: '#2a2a2a',
bgActive: '#333333',
// 边框
border: '#333333',
borderLight: '#2a2a2a',
// 状态指示
activeIndicator: '#f5f5f5',
};
// 获取主题颜色
export function getThemeColors(mode: ThemeMode = 'light', customColors?: Partial<ThemeColors>): ThemeColors {
const baseColors = mode === 'dark' ? DARK_COLORS : LIGHT_COLORS;
return customColors ? { ...baseColors, ...customColors } : baseColors;
}
// 生成样式
export function createShellStyles(colors: ThemeColors): Record<string, CSSProperties> {
return {
// 根容器
root: {
display: 'flex',
minHeight: '100vh',
height: '100vh',
backgroundColor: colors.bgSecondary,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
color: colors.textPrimary,
overflow: 'hidden',
},
// 侧边栏
sidebar: {
display: 'flex',
flexDirection: 'column',
backgroundColor: colors.bgPrimary,
borderRight: `1px solid ${colors.border}`,
transition: 'width 0.2s ease, opacity 0.2s ease',
overflow: 'hidden',
},
sidebarOpen: {
width: 256,
opacity: 1,
},
sidebarClosed: {
width: 0,
opacity: 0,
borderRight: 'none',
},
// 品牌区域
brandArea: {
padding: '20px 16px',
borderBottom: `1px solid ${colors.borderLight}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minWidth: 224,
},
brandContent: {
display: 'flex',
alignItems: 'center',
gap: 12,
},
brandLogo: {
width: 36,
height: 36,
borderRadius: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
flexShrink: 0,
},
brandText: {
display: 'flex',
flexDirection: 'column',
gap: 2,
},
brandName: {
fontSize: 15,
fontWeight: 600,
color: colors.textPrimary,
lineHeight: 1.2,
},
brandSubtitle: {
fontSize: 11,
color: colors.textTertiary,
letterSpacing: '0.02em',
},
// 折叠按钮
collapseBtn: {
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
background: 'transparent',
color: colors.textTertiary,
cursor: 'pointer',
borderRadius: 6,
transition: 'all 0.15s ease',
flexShrink: 0,
},
collapseBtnHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
// 导航区域
navArea: {
flex: 1,
overflowY: 'auto',
padding: '16px 0',
minWidth: 224,
},
// 分组
navGroup: {
marginBottom: 24,
},
navGroupTitle: {
padding: '0 16px',
marginBottom: 8,
fontSize: 11,
fontWeight: 600,
color: colors.textMuted,
letterSpacing: '0.08em',
textTransform: 'uppercase',
},
navList: {
listStyle: 'none',
margin: 0,
padding: 0,
},
// 导航项
navItem: {
display: 'block',
width: '100%',
textAlign: 'left',
padding: '8px 16px',
border: 'none',
background: 'transparent',
fontSize: 14,
color: colors.textSecondary,
cursor: 'pointer',
transition: 'all 0.15s ease',
},
navItemHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
navItemActive: {
backgroundColor: colors.bgTertiary,
color: colors.textPrimary,
fontWeight: 500,
},
// 主内容区
main: {
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minHeight: 0,
},
// 头部区域(可选)
header: {
flexShrink: 0,
padding: '16px 24px',
backgroundColor: colors.bgPrimary,
borderBottom: `1px solid ${colors.borderLight}`,
},
// 内容区域
content: {
flex: 1,
overflowY: 'auto',
minHeight: 0,
padding: '24px 32px',
},
// 展开按钮(侧边栏关闭时)
expandBtn: {
position: 'fixed',
top: 20,
left: 20,
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.bgPrimary,
border: `1px solid ${colors.border}`,
borderRadius: 8,
cursor: 'pointer',
color: colors.textSecondary,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
transition: 'all 0.15s ease',
},
expandBtnHover: {
backgroundColor: colors.bgHover,
color: colors.textPrimary,
},
// 移动端顶栏
mobileTopbar: {
display: 'none', // 默认隐藏,通过媒体查询显示
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 50,
height: 56,
padding: '0 16px',
backgroundColor: colors.bgPrimary,
borderBottom: `1px solid ${colors.border}`,
alignItems: 'center',
justifyContent: 'space-between',
},
// 移动端遮罩
mobileOverlay: {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 99,
},
// 移动端侧边栏
mobileSidebar: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
width: 280,
zIndex: 100,
backgroundColor: colors.bgPrimary,
boxShadow: '4px 0 24px rgba(0,0,0,0.12)',
transform: 'translateX(-100%)',
transition: 'transform 0.3s ease',
},
mobileSidebarOpen: {
transform: 'translateX(0)',
},
};
}
// 保持向后兼容 - 默认使用浅色主题
export const SHELL_STYLES = createShellStyles(LIGHT_COLORS);

View File

@@ -0,0 +1,118 @@
/**
* ThemeShell 类型定义
*
* 去主题化的设计系统展示组件类型
*/
/** 主题模式 */
export type ThemeMode = 'light' | 'dark';
/** 主题颜色配置 */
export interface ThemeColors {
/** 主要文字色 */
textPrimary: string;
/** 次要文字色 */
textSecondary: string;
/** 三级文字色 */
textTertiary: string;
/** 静音文字色 */
textMuted: string;
/** 主背景色 */
bgPrimary: string;
/** 次要背景色 */
bgSecondary: string;
/** 三级背景色 */
bgTertiary: string;
/** 悬浮背景色 */
bgHover: string;
/** 激活背景色 */
bgActive: string;
/** 边框色 */
border: string;
/** 浅边框色 */
borderLight: string;
/** 激活指示器色 */
activeIndicator: string;
}
/** 主题配置 */
export interface ThemeConfig {
/** 主题模式 */
mode?: ThemeMode;
/** 自定义颜色(覆盖默认值) */
colors?: Partial<ThemeColors>;
}
/** 导航项 */
export interface NavItem {
/** 唯一标识 */
id: string;
/** 显示标签 */
label: string;
/** 所属分组 ID */
groupId: string;
/** 可选描述 */
description?: string;
/** 可选图标React 节点) */
icon?: React.ReactNode;
}
/** 导航分组 */
export interface NavGroup {
/** 分组唯一标识 */
id: string;
/** 分组标题 */
title: string;
/** 分组排序权重(越小越靠前) */
order?: number;
}
/** 品牌配置 */
export interface BrandConfig {
/** 品牌名称 */
name: string;
/** 副标题 */
subtitle?: string;
/** Logo 图标React 节点) */
logo?: React.ReactNode;
/** Logo 背景色 */
logoBgColor?: string;
/** Logo 文字色 */
logoTextColor?: string;
}
/** 侧边栏配置 */
export interface SidebarConfig {
/** 默认宽度 */
width?: number;
/** 是否默认展开 */
defaultOpen?: boolean;
/** 是否允许折叠 */
collapsible?: boolean;
}
/** ThemeShell 组件属性 */
export interface ThemeShellProps {
/** 品牌配置 */
brand?: BrandConfig;
/** 导航分组列表 */
groups: NavGroup[];
/** 导航项列表 */
items: NavItem[];
/** 当前激活项 ID */
activeId: string;
/** 导航切换回调 */
onNavigate: (id: string) => void;
/** 侧边栏配置 */
sidebar?: SidebarConfig;
/** 主题配置 */
theme?: ThemeConfig;
/** 内容区域 */
children: React.ReactNode;
/** 顶部额外内容(如比选面板) */
header?: React.ReactNode;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
}

View File

@@ -0,0 +1,863 @@
/**
*本项目Variant Switcher 组件 (重构版)
*
* 核心特性:
* - 零依赖:不依赖任何外部 CSS 框架或图标库
* - 轻量化 UI使用图标入口替代重型控制条
* - 丰富信息:支持标题和描述
* - 全局面板:支持页面级统一管理和跳转
* - 隐形控制:支持快捷键显隐入口
* - 自动全局入口:当有比选组件时自动显示全局入口按钮
*/
import React, { useState, useEffect, useCallback, CSSProperties, useRef } from 'react';
import { createPortal } from 'react-dom';
// --- 类型定义 ---
export interface VariantItem {
/** 唯一标识,若不提供则使用索引 */
key?: string;
/** 渲染内容 */
content: React.ReactNode;
/** 方案标题 */
title: string;
/** 方案一句话描述 */
description: string;
/** 方案详细说明文档Markdown 格式) */
markdown?: string;
}
export interface VariantAPI {
id: string;
/** 比选方案的中文名称,用于在全局面板中显示 */
name: string;
currentIndex: number;
totalVariants: number;
isDecided: boolean;
variants: VariantItem[]; // 暴露方案详情供全局面板使用
select: (index: number) => void;
confirm: () => void;
reset: () => void;
focus: () => void; // 聚焦到该组件(滚动)
}
declare global {
interface Window {
AXHUB_VARIANT_MANAGER?: VariantManager;
}
}
type Listener = () => void;
export interface VariantManager {
register: (id: string, api: VariantAPI) => void;
unregister: (id: string) => void;
instances: Record<string, VariantAPI>;
subscribe: (listener: Listener) => () => void;
notify: () => void;
setVisibility: (visible: boolean) => void;
isVisible: boolean;
}
export interface VariantSwitcherProps {
id?: string;
/** 比选方案的中文名称,显示在全局面板中(如"头部设计"、"登录页布局" */
name?: string;
/** 方案列表 */
variants: VariantItem[];
defaultIndex?: number;
onConfirm?: (index: number, item: VariantItem) => void;
onReset?: () => void;
style?: CSSProperties;
className?: string;
}
// --- 图标定义 ---
const Icons = {
Switcher: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
),
Check: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
Close: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
),
// 比选图标:两个重叠的卡片,表示多个方案比选
VariantCompare: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{/* 底层卡片 */}
<rect x="4" y="6" width="12" height="10" rx="2" opacity="0.4" />
{/* 顶层卡片(偏移) */}
<rect x="8" y="4" width="12" height="10" rx="2" />
</svg>
),
Target: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
</svg>
),
Exit: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
),
Doc: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
),
Back: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
)
};
// --- 主题配置 ---
const THEME_COLOR = '#008F5D';
const THEME_COLOR_BG = 'rgba(0, 143, 93, 0.1)';
// --- 内置 Markdown 渲染器(零依赖) ---
const MarkdownViewer: React.FC<{ content: string }> = ({ content }) => {
const parseInlineStyles = (text: string): React.ReactNode => {
// 处理行内代码 `code`
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '13px',
fontFamily: 'Monaco, Consolas, monospace',
color: '#e83e8c'
}}>
{part.slice(1, -1)}
</code>
);
}
// 处理加粗 **text**
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((bp, j) => {
if (bp.startsWith('**') && bp.endsWith('**')) {
return <strong key={`${i}-${j}`}>{bp.slice(2, -2)}</strong>;
}
return bp;
});
});
};
const parseLine = (line: string, index: number): React.ReactNode => {
// 标题
if (line.startsWith('### ')) {
return <h3 key={index} style={{ fontSize: '16px', fontWeight: 600, color: '#333', marginTop: '20px', marginBottom: '8px' }}>{parseInlineStyles(line.slice(4))}</h3>;
}
if (line.startsWith('## ')) {
return <h2 key={index} style={{ fontSize: '18px', fontWeight: 600, color: '#333', marginTop: '24px', marginBottom: '12px', paddingBottom: '8px', borderBottom: '1px solid #eee' }}>{parseInlineStyles(line.slice(3))}</h2>;
}
if (line.startsWith('# ')) {
return <h1 key={index} style={{ fontSize: '22px', fontWeight: 600, color: '#333', marginBottom: '16px' }}>{parseInlineStyles(line.slice(2))}</h1>;
}
// 列表
if (line.startsWith('- ')) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'disc' }}>
{parseInlineStyles(line.slice(2))}
</li>
);
}
if (/^\d+\.\s/.test(line)) {
const match = line.match(/^(\d+)\.\s(.*)$/);
if (match) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'decimal' }}>
{parseInlineStyles(match[2])}
</li>
);
}
}
// 引用
if (line.startsWith('> ')) {
return (
<blockquote key={index} style={{ marginLeft: 0, paddingLeft: '12px', borderLeft: `3px solid ${THEME_COLOR}`, color: '#666', fontStyle: 'italic', margin: '12px 0' }}>
{parseInlineStyles(line.slice(2))}
</blockquote>
);
}
// 空行
if (line.trim() === '') {
return <div key={index} style={{ height: '12px' }} />;
}
// 普通段落
return <p key={index} style={{ color: '#555', lineHeight: 1.7, marginBottom: '12px' }}>{parseInlineStyles(line)}</p>;
};
const lines = content.split('\n');
return (
<div style={{ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: '14px' }}>
{lines.map((line, i) => parseLine(line, i))}
</div>
);
};
// --- 全局管理器实现 ---
const listeners: Listener[] = [];
let globalVisible = true;
function initGlobalManager(): VariantManager {
if (!window.AXHUB_VARIANT_MANAGER) {
window.AXHUB_VARIANT_MANAGER = {
instances: {},
isVisible: true,
register(id, api) {
this.instances[id] = api;
this.notify();
},
unregister(id) {
delete this.instances[id];
this.notify();
},
subscribe(listener) {
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
};
},
notify() {
this.isVisible = globalVisible;
listeners.forEach(fn => fn());
},
setVisibility(visible) {
globalVisible = visible;
this.notify();
}
};
}
return window.AXHUB_VARIANT_MANAGER;
}
// --- Hooks ---
/** 获取所有注册的实例及全局可见性 */
function useVariantManager() {
const [state, setState] = useState<{
instances: Record<string, VariantAPI>;
isVisible: boolean;
}>({ instances: {}, isVisible: true });
useEffect(() => {
const manager = initGlobalManager();
const update = () => setState({
instances: { ...manager.instances },
isVisible: manager.isVisible
});
update();
return manager.subscribe(update);
}, []);
return state;
}
// --- 样式定义 ---
const STYLES = {
container: {
position: 'relative' as const,
width: '100%',
height: '100%',
},
triggerBtn: {
position: 'absolute' as const,
top: '4px',
right: '4px',
zIndex: 9001,
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(0, 0, 0, 0.08)',
borderRadius: '2px',
color: '#666',
cursor: 'pointer',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
transition: 'all 0.2s',
},
popover: {
position: 'absolute' as const,
top: '32px',
right: '0px',
width: '260px',
backgroundColor: '#fff',
borderRadius: '2px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)',
padding: '4px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column' as const,
gap: '2px',
opacity: 0,
transform: 'translateY(-4px)',
pointerEvents: 'none' as const,
transition: 'all 0.15s ease-out',
},
popoverVisible: {
opacity: 1,
transform: 'translateY(0)',
pointerEvents: 'auto' as const,
},
variantCard: {
display: 'flex',
flexDirection: 'column' as const,
padding: '8px 10px',
borderRadius: '0',
cursor: 'pointer',
border: 'none',
transition: 'background 0.2s',
textAlign: 'left' as const,
background: 'transparent',
},
variantCardActive: {
background: THEME_COLOR_BG,
border: 'none',
},
variantTitle: {
fontSize: '13px',
fontWeight: 500,
color: '#333',
marginBottom: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
variantDesc: {
fontSize: '12px',
color: '#888',
lineHeight: '1.4',
},
globalTrigger: {
position: 'fixed' as const,
bottom: '24px',
right: '24px',
zIndex: 99999,
width: '32px',
height: '32px',
borderRadius: '16px',
backgroundColor: '#fff',
color: '#555',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
cursor: 'pointer',
border: '1px solid rgba(0,0,0,0.05)',
transition: 'transform 0.2s, opacity 0.2s',
},
globalPanel: {
position: 'fixed' as const,
top: 0,
right: 0,
bottom: 0,
width: '300px',
backgroundColor: '#fff',
boxShadow: '-4px 0 24px rgba(0,0,0,0.08)',
zIndex: 100000,
padding: '0',
display: 'flex',
flexDirection: 'column' as const,
transform: 'translateX(100%)',
transition: 'transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)',
},
globalPanelVisible: {
transform: 'translateX(0)',
},
globalPanelHeader: {
padding: '16px',
borderBottom: '1px solid #f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: '15px',
fontWeight: 600,
color: '#333',
},
globalPanelContent: {
flex: 1,
overflowY: 'auto' as const,
padding: '16px',
},
globalPanelFooter: {
padding: '12px 16px',
borderTop: '1px solid #f5f5f5',
display: 'flex',
justifyContent: 'center',
},
nodeGroup: {
marginBottom: '16px',
border: '1px solid #eee',
borderRadius: '0',
overflow: 'hidden',
},
nodeHeader: {
padding: '6px 10px',
backgroundColor: '#fafafa',
borderBottom: '1px solid #eee',
fontSize: '12px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
color: '#666',
},
exitBtn: {
display: 'flex',
alignItems: 'center',
gap: '6px',
background: 'none',
border: 'none',
color: '#999',
fontSize: '12px',
cursor: 'pointer',
padding: '8px',
borderRadius: '0',
transition: 'all 0.2s',
}
};
// --- 全局入口组件(单例) ---
let globalControlMountRef = { current: false };
/** 全局入口控制组件 */
const GlobalVariantControl: React.FC = () => {
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [docView, setDocView] = useState<{ title: string; content: string } | null>(null);
const { instances: allInstances, isVisible } = useVariantManager();
// 键盘快捷键监听
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === '.') {
e.preventDefault();
initGlobalManager().setVisibility(!isVisible);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isVisible]);
const instancesList = Object.values(allInstances);
// 按 id 字母顺序排序,保持稳定的显示顺序
const sortedInstances = [...instancesList].sort((a, b) => a.id.localeCompare(b.id));
// 如果没有实例或不可见,则不渲染
if (!isVisible || sortedInstances.length === 0) {
return null;
}
return (
<>
{/* 全局悬浮球 */}
<button
style={STYLES.globalTrigger}
onClick={() => setIsPanelOpen(true)}
title="方案比选 (Ctrl + .)"
>
<Icons.VariantCompare />
</button>
{/* 全局侧边栏面板 */}
<div style={{
...STYLES.globalPanel,
...(isPanelOpen ? STYLES.globalPanelVisible : {})
}}>
{/* Header */}
<div style={STYLES.globalPanelHeader}>
{docView ? (
<>
<button
onClick={() => setDocView(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: '#666', display: 'flex', alignItems: 'center', gap: '4px' }}
>
<Icons.Back />
<span style={{ fontSize: '14px' }}></span>
</button>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
) : (
<>
<span></span>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
)}
</div>
{/* Content */}
<div style={STYLES.globalPanelContent}>
{docView ? (
/* 文档视图 - 简洁无风格 */
<div style={{ padding: '0 4px' }}>
<div style={{ fontSize: '13px', color: '#999', marginBottom: '16px' }}>{docView.title}</div>
<MarkdownViewer content={docView.content} />
</div>
) : (
/* 方案列表视图 */
sortedInstances.map(inst => (
<div key={inst.id} style={STYLES.nodeGroup}>
{/* Node Header */}
<div style={STYLES.nodeHeader}>
<span>{inst.name}</span>
<button
onClick={() => {
inst.focus();
setIsPanelOpen(false);
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: THEME_COLOR, display: 'flex', alignItems: 'center', fontSize: '12px'
}}
>
<Icons.Target />
<span style={{ marginLeft: 4 }}></span>
</button>
</div>
{/* Variants List */}
<div>
{inst.variants.map((v, idx) => {
const isActive = inst.currentIndex === idx;
return (
<div
key={v.key || idx}
style={{
...STYLES.variantCard,
...(isActive ? { background: THEME_COLOR_BG } : {}),
borderBottom: '1px solid #f9f9f9',
borderRadius: 0,
}}
>
<div
onClick={() => inst.select(idx)}
style={{ cursor: 'pointer' }}
>
<div style={STYLES.variantTitle}>
<span>{v.title}</span>
{isActive && <span style={{color: THEME_COLOR, fontSize: '12px'}}></span>}
</div>
<div style={STYLES.variantDesc}>{v.description}</div>
</div>
{/* 文档按钮 */}
{v.markdown && (
<button
onClick={(e) => {
e.stopPropagation();
setDocView({ title: v.title, content: v.markdown! });
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: '#999', display: 'flex', alignItems: 'center',
fontSize: '12px', marginTop: '6px', padding: 0
}}
onMouseEnter={e => e.currentTarget.style.color = THEME_COLOR}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Doc />
<span style={{ marginLeft: 4 }}></span>
</button>
)}
</div>
);
})}
</div>
</div>
))
)}
</div>
{/* Footer: Exit Button - 仅在列表视图显示 */}
{!docView && (
<div style={STYLES.globalPanelFooter}>
<button
style={STYLES.exitBtn}
onClick={() => {
initGlobalManager().setVisibility(false);
setIsPanelOpen(false);
}}
title="隐藏比选入口 (Ctrl + . 重新开启)"
onMouseEnter={e => e.currentTarget.style.color = '#333'}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Exit />
<span>退</span>
</button>
</div>
)}
</div>
{/* 遮罩层 */}
{isPanelOpen && (
<div
onClick={() => setIsPanelOpen(false)}
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.1)', zIndex: 99999
}}
/>
)}
</>
);
};
// --- 主组件 ---
export const VariantSwitcher: React.FC<VariantSwitcherProps> = ({
id: propId,
name: propName,
variants = [],
defaultIndex = 0,
onConfirm,
onReset,
style,
className,
}) => {
const [instanceId] = useState(() =>
propId || `axhub_vs_${Math.random().toString(36).substr(2, 9)}`
);
// 如果没有提供 name使用 id 作为显示名称
const displayName = propName || instanceId;
const containerRef = useRef<HTMLDivElement>(null);
const [currentIndex, setCurrentIndex] = useState(defaultIndex);
const [isDecided, setIsDecided] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { isVisible: globalVisible } = useVariantManager();
// 标记当前组件负责渲染全局入口(单例,只渲染一次)
const [isGlobalControlOwner, setIsGlobalControlOwner] = useState(false);
useEffect(() => {
// 如果还没有组件负责渲染全局入口,则当前组件负责
if (!globalControlMountRef.current) {
globalControlMountRef.current = true;
setIsGlobalControlOwner(true);
}
// 组件卸载时,如果当前组件是全局入口的拥有者,则释放
return () => {
if (isGlobalControlOwner) {
globalControlMountRef.current = false;
}
};
}, [isGlobalControlOwner]);
// --- API Methods ---
const select = useCallback((index: number) => {
if (index >= 0 && index < variants.length) {
setCurrentIndex(index);
}
}, [variants.length]);
const confirm = useCallback(() => {
setIsDecided(true);
setIsPopoverOpen(false);
if (variants[currentIndex]) {
onConfirm?.(currentIndex, variants[currentIndex]);
}
}, [currentIndex, variants, onConfirm]);
const reset = useCallback(() => {
setIsDecided(false);
onReset?.();
}, [onReset]);
const focus = useCallback(() => {
if (containerRef.current) {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
setIsHovered(true);
setTimeout(() => setIsHovered(false), 2000);
}
}, []);
// --- 注册到全局 ---
useEffect(() => {
if (variants.length > 0) {
const manager = initGlobalManager();
const api: VariantAPI = {
id: instanceId,
name: displayName,
currentIndex,
totalVariants: variants.length,
isDecided,
variants,
select,
confirm,
reset,
focus,
};
manager.register(instanceId, api);
return () => manager.unregister(instanceId);
}
}, [instanceId, displayName, currentIndex, variants, isDecided, select, confirm, reset, focus]);
// --- 点击外部关闭弹窗 ---
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsPopoverOpen(false);
}
};
if (isPopoverOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isPopoverOpen]);
if (variants.length === 0) return null;
return (
<>
{/* 全局入口(单例,通过 Portal 渲染到 body只由第一个组件渲染 */}
{isGlobalControlOwner && typeof document !== 'undefined' && createPortal(
<GlobalVariantControl />,
document.body
)}
<div
ref={containerRef}
className={className}
style={{ ...STYLES.container, ...style }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
data-axhub-variant-id={instanceId}
>
{/* 渲染当前方案内容 */}
{variants[currentIndex]?.content}
{/* 触发器图标 (轻量化,悬停显示) */}
{globalVisible && (
<button
style={{
...STYLES.triggerBtn,
opacity: (isHovered || isPopoverOpen) ? 1 : 0,
pointerEvents: (isHovered || isPopoverOpen) ? 'auto' : 'none',
backgroundColor: isPopoverOpen ? THEME_COLOR : '#fff',
color: isPopoverOpen ? '#fff' : '#666',
borderColor: isPopoverOpen ? THEME_COLOR : 'rgba(0,0,0,0.06)'
}}
onClick={(e) => {
e.stopPropagation();
setIsPopoverOpen(!isPopoverOpen);
}}
title="切换方案"
>
<Icons.Switcher />
</button>
)}
{/* 下拉选择面板 */}
{globalVisible && (
<div style={{
...STYLES.popover,
...(isPopoverOpen ? STYLES.popoverVisible : {})
}}>
<div style={{ padding: '4px 8px', fontSize: '12px', color: '#999', fontWeight: 600 }}>
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{variants.map((variant, index) => {
const isActive = index === currentIndex;
return (
<div
key={variant.key || index}
onClick={(e) => {
e.stopPropagation();
select(index);
}}
style={{
...STYLES.variantCard,
...(isActive ? STYLES.variantCardActive : {}),
}}
>
<div style={STYLES.variantTitle}>
{variant.title}
{isActive && <Icons.Check />}
</div>
<div style={STYLES.variantDesc}>
{variant.description}
</div>
</div>
);
})}
</div>
<div style={{ borderTop: '1px solid #f5f5f5', marginTop: '4px', paddingTop: '8px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
{isDecided ? (
<button
onClick={(e) => { e.stopPropagation(); reset(); }}
style={{
background: 'transparent', border: '1px solid #eee', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#666'
}}
>
</button>
) : (
<button
onClick={(e) => { e.stopPropagation(); confirm(); }}
style={{
background: THEME_COLOR, border: 'none', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#fff'
}}
>
</button>
)}
</div>
</div>
)}
</div>
</>
);
};
export default VariantSwitcher;

237
src/common/axure-types.ts Normal file
View File

@@ -0,0 +1,237 @@
/**
*Axue渲染引擎共类型定义
* 包含所有组件和页面共用的类型、接口和工具函数
*/
// ============ 基础类型定义 ============
// 小写字母 a-z
type Lower =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g'
| 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n'
| 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u'
| 'v' | 'w' | 'x' | 'y' | 'z';
// 数字 0-9
type Digit =
| '0' | '1' | '2' | '3' | '4' | '5'
| '6' | '7' | '8' | '9';
// 允许的字符:小写字母 + 数字 + 下划线
type AllowedChar = Lower | Digit | '_';
// snake_case 字符串类型仅用于文档说明TypeScript 无法完全约束)
// 正确示例: 'user_name', 'item_count', 'is_active'
// 错误示例: 'userName', 'ItemCount', 'Is-Active'
type SnakeCaseString = string;
// name 字段规则:
// - 必须由小写字母、数字、下划线组成a-z、0-9、_
// - 长度 >= 1不能为空
// - 建议使用 snake_case 命名风格
//
// ⚠️ 注意TypeScript 类型系统无法在编译时完全约束字符串内容
// 请手动遵守命名规范,或使用下面的运行时验证函数
export type KeyDesc = {
name: SnakeCaseString;
desc: string;
};
/**
* 验证 name 是否符合规范(运行时检查)
* @param name 要验证的名称
* @returns 是否合法
*/
export function isValidKeyName(name: string): boolean {
return /^[a-z0-9_]+$/.test(name);
}
/**
* 断言 name 符合规范,不符合则抛出错误
* @param name 要验证的名称
* @param context 上下文信息(用于错误提示)
*/
export function assertValidKeyName(name: string, context = 'KeyDesc'): asserts name is SnakeCaseString {
if (!isValidKeyName(name)) {
throw new Error(
`[${context}] Invalid name "${name}". Name must only contain lowercase letters, digits, and underscores (a-z, 0-9, _)`
);
}
}
export type DataKey = { name: string; desc: string };
export type DataDesc = { name: string; desc: string; keys: DataKey[] };
/**
* 配置项定义
* Configuration item definition
* 参考 AttributeComponentProps 结构
*/
export type ConfigItem = {
/** 组件类型 Component type */
type?:
| 'group' // 分组
| 'input' // 文本输入框
| 'inputNumber' // 数字输入框
| 'checkbox' // 复选框
| 'slider' // 滑块
| 'select' // 下拉选择框
| 'autoComplete' // 自动完成
| 'colorPicker' // 颜色选择器
| 'arrayData' // 数组数据编辑器
| 'table' // 表格数据编辑器
| 'map' // 键值对数据编辑器
| 'fontSetting' // 字体设置
| 'lineSetting' // 线条设置
| 'pointSetting' // 端点设置
| 'collapse'; // 折叠面板
/** 属性唯一标识符 Unique attribute identifier (supports dot notation like 'style.fontSize') */
attributeId?: string;
/** 显示名称 Display name shown in UI */
displayName?: string;
/** 描述信息(提示文本) Description or tooltip text */
info?: string;
/** 默认值 Initial/default value */
initialValue?: any;
/** 子配置项 Child configuration items (for nested structures) */
children?: ConfigItem[];
/** 是否显示 Whether to show this item (default: true) */
show?: boolean;
/** 组件特定配置 Component-specific configuration properties */
[k: string]: any;
};
export type Action = { name: string; desc: string; params?: string };
export type EventItem = { name: string; desc: string; payload?: string };
export type CSSProperties = Record<string, string | number>;
export type AnyFunction = (...args: any[]) => any;
export type Ref<T> = { current: T | null } | ((instance: T | null) => void) | null;
export type ForwardRefRenderFunction<T, P> = (props: P, ref: Ref<T>) => any;
// ============Axure组件接口 ============
/**
* Axure 组件的 Props 接口
* 所有 Axure 组件都应该接受这些属性
*/
export interface AxureProps {
/** 数据源,用于传递组件需要的数据 */
data?: Record<string, any>;
/** 配置项,用于配置组件的行为和样式 */
config?: Record<string, any>;
/** 事件处理函数,组件触发事件时调用
* ⚠️ 强制规则payload 必须是字符串类型
*/
onEvent?: (name: string, payload?: string) => void;
/** 容器元素,用于挂载组件 */
container?: HTMLElement | null;
}
/**
* Axure 组件的 Handle 接口
* 通过 ref 暴露给外部的方法和属性
*/
export interface AxureHandle {
/** 获取组件内部变量 */
getVar: (name: string) => any;
/** 触发组件动作
* ⚠️ 强制规则params 必须是字符串类型
*/
fireAction: (name: string, params?: string) => void;
/** 组件支持的事件列表 */
eventList: EventItem[];
/** 组件支持的动作列表 */
actionList: Action[];
/** 组件暴露的变量列表 */
varList: KeyDesc[];
/** 组件的配置项列表 */
configList: ConfigItem[];
/** 组件的数据项列表 */
dataList: DataDesc[];
}
// ============ 工具函数 ============
/**
* 安全地触发事件
* @param handler 事件处理函数
* @param eventName 事件名称
* @param payload 事件数据(⚠️ 强制规则:必须是字符串类型)
*/
export function safeEmitEvent(
handler: ((name: string, payload?: string) => void) | undefined,
eventName: string,
payload?: string
): void {
if (typeof handler === 'function') {
try {
handler(eventName, payload);
} catch (error) {
console.warn(`事件 ${eventName} 调用失败:`, error);
}
}
}
/**
* 创建事件发射器
* @param onEventHandler 事件处理函数
* @returns 事件发射函数(⚠️ 强制规则payload 必须是字符串类型)
*/
export function createEventEmitter(onEventHandler?: (name: string, payload?: string) => void) {
return function emitEvent(eventName: string, payload?: string) {
safeEmitEvent(onEventHandler, eventName, payload);
};
}
/**
* 合并样式对象
* @param styles 样式对象数组
* @returns 合并后的样式对象
*/
export function mergeStyles(...styles: (CSSProperties | undefined)[]): CSSProperties {
return Object.assign({}, ...styles.filter(Boolean));
}
/**
* 获取配置值,如果不存在则返回默认值
* @param config 配置对象
* @param key 配置键
* @param defaultValue 默认值
* @returns 配置值或默认值
*/
export function getConfigValue<T>(
config: Record<string, any> | undefined,
key: string,
defaultValue: T
): T {
if (!config || config[key] === undefined) {
return defaultValue;
}
return config[key] as T;
}
/**
* 获取数据值,如果不存在则返回默认值
* @param data 数据对象
* @param key 数据键
* @param defaultValue 默认值
* @returns 数据值或默认值
*/
export function getDataValue<T>(
data: Record<string, any> | undefined,
key: string,
defaultValue: T
): T {
if (!data || data[key] === undefined) {
return defaultValue;
}
return data[key] as T;
}

View File

@@ -0,0 +1,501 @@
/**
* ConfigPanel API Type Definitions
* 配置面板 API 类型定义
*
* @version 2.0
* @author Lintendo
* @description 可扩展的属性配置系统,支持第三方集成使用
*
* 核心特性:
* - 声明式配置
* - 树形结构组织
* - 动态显示/隐藏
* - 丰富的组件类型
*/
// ============================================================================
// 核心类型定义 Core Type Definitions
// ============================================================================
/**
* 配置项基础属性
* Base properties for all configuration items
*/
export interface AttributeComponentProps {
/** 组件类型 Component type (e.g., 'input', 'select', 'colorPicker') */
type?: string;
/** 属性唯一标识符 Unique attribute identifier (supports dot notation like 'style.fontSize') */
attributeId?: string;
/** 显示名称 Display name shown in UI */
displayName?: string;
/** 描述信息(提示文本) Description or tooltip text */
info?: string;
/** 默认值 Initial/default value */
initialValue?: any;
/** 子配置项 Child configuration items (for nested structures) */
children?: AttributeComponentProps[];
/** 是否显示 Whether to show this item (default: true) */
show?: boolean;
/** 组件特定配置 Component-specific configuration properties */
[k: string]: any;
}
/**
* 完整配置对象
* Complete configuration object
*/
export interface AttributesConfig {
/** 配置树 Configuration tree */
config: AttributeComponentProps;
}
// ============================================================================
// 布局组件 Layout Components
// ============================================================================
/**
* 分组配置
* Group configuration
*
* @description 用于在面板内部进行逻辑分组
* Used for logical grouping within panels
*/
export interface GroupConfig extends AttributeComponentProps {
type: 'group';
/** 分组显示名称 Group display name */
displayName: string;
/** 显示类型 Display type ('inline' for inline display) */
displayType?: 'inline' | 'default';
/** 分组内的配置项 Configuration items within the group */
children: AttributeComponentProps[];
}
// ============================================================================
// 基础输入组件 Basic Input Components
// ============================================================================
/**
* 文本输入框配置
* Text input configuration
*/
export interface InputConfig extends AttributeComponentProps {
type: 'input';
attributeId: string;
displayName: string;
/** 占位符文本 Placeholder text */
placeholder?: string;
/** 输入框宽度 Input width (default: '40%') */
width?: string;
/** 是否禁用 Whether disabled */
disabled?: boolean;
/** 初始值 Initial value */
initialValue?: string;
}
/**
* 数字输入框配置
* Number input configuration
*/
export interface InputNumberConfig extends AttributeComponentProps {
type: 'inputNumber';
attributeId: string;
displayName: string;
/** 最小值 Minimum value */
min?: number;
/** 最大值 Maximum value */
max?: number;
/** 步长 Step increment (default: 1) */
step?: number;
/** 占位符文本 Placeholder text */
placeholder?: string;
/** 初始值 Initial value */
initialValue?: number;
}
/**
* 复选框配置
* Checkbox configuration
*/
export interface CheckboxConfig extends AttributeComponentProps {
type: 'checkbox';
attributeId: string;
displayName: string;
/** 是否禁用 Whether disabled */
disabled?: boolean;
/** 初始值 Initial value */
initialValue?: boolean;
}
/**
* 滑块配置
* Slider configuration
*/
export interface SliderConfig extends AttributeComponentProps {
type: 'slider';
attributeId: string;
displayName: string;
/** 最小值 Minimum value */
min: number;
/** 最大值 Maximum value */
max: number;
/** 步长 Step increment */
step?: number;
/** 是否显示数字输入框 Whether to show number input alongside slider */
showInputNumber?: boolean;
/** 初始值 Initial value */
initialValue?: number;
}
// ============================================================================
// 选择组件 Selection Components
// ============================================================================
/**
* 选项定义
* Option definition for select components
*/
export interface SelectOption {
/** 显示标签 Display label */
label: string;
/** 选项值 Option value */
value: string | number;
}
/**
* 下拉选择框配置
* Select dropdown configuration
*/
export interface SelectConfig extends AttributeComponentProps {
type: 'select';
attributeId: string;
displayName: string;
/** 选项数组 Array of options */
options: SelectOption[];
/** 选择模式 Selection mode ('multiple' for multi-select) */
mode?: 'multiple';
/** 下拉框宽度 Dropdown width (default: 120) */
dropdownMatchSelectWidth?: number;
/** 初始值 Initial value */
initialValue?: string | number | string[] | number[];
}
/**
* 自动完成配置
* AutoComplete configuration
*/
export interface AutoCompleteConfig extends AttributeComponentProps {
type: 'autoComplete';
attributeId: string;
displayName: string;
/** 建议选项数组 Array of suggestion options */
options: SelectOption[];
/** 弹出框宽度 Popup width (default: 200) */
popupMatchSelectWidth?: number;
/** 初始值 Initial value */
initialValue?: string;
}
// ============================================================================
// 颜色组件 Color Components
// ============================================================================
/**
* 颜色选择器配置
* Color picker configuration
*/
export interface ColorPickerConfig extends AttributeComponentProps {
type: 'colorPicker';
attributeId: string;
displayName: string;
/** 选择器类型 Picker type */
picker?: 'common' | 'lite';
/** 初始值 Initial color value (hex format) */
initialValue?: string;
}
// ============================================================================
// 复杂数据组件 Complex Data Components
// ============================================================================
/**
* 数组数据编辑器配置
* Array data editor configuration
*
* @description 弹窗编辑,每行一个数据项
* Modal editor, one item per line
*/
export interface ArrayDataConfig extends AttributeComponentProps {
type: 'arrayData';
attributeId: string;
displayName: string;
/** 初始值 Initial array value */
initialValue?: string[];
}
/**
* 表格列定义
* Table column definition
*/
export interface TableColumn {
/** 字段名 Field name */
name: string;
/** 列显示名 Column display name */
colName: string;
/** 输入类型 Input type */
type: 'text' | 'select' | 'color' | 'number' | 'icon';
/** select 类型的选项 Options for select type */
options?: SelectOption[];
}
/**
* 表格数据编辑器配置
* Table data editor configuration
*
* @description 支持添加、删除、复制、排序行
* Supports add, delete, copy, and reorder rows
*/
export interface TableConfig extends AttributeComponentProps {
type: 'table';
attributeId: string;
displayName: string;
/** 是否为 Map 结构(以第一列值为 key Whether to use Map structure (first column as key) */
isMap?: boolean;
/** 列定义数组 Array of column definitions */
columns: TableColumn[];
/** 初始值 Initial table data */
initialValue?: any[];
}
/**
* Map 列定义
* Map column definition
*/
export interface MapColumn {
/** 键名 Key name */
key: string;
/** 列显示名 Column display name */
colName: string;
/** 输入类型 Input type */
type: 'text' | 'select' | 'color' | 'number' | 'icon';
/** 属性 ID支持 {key} 占位符) Attribute ID (supports {key} placeholder) */
attributeId: string;
/** select 类型的选项 Options for select type */
options?: SelectOption[];
}
/**
* 键值对数据编辑器配置
* Key-value pair data editor configuration
*
* @description 固定键名,编辑值,支持嵌套属性更新
* Fixed keys, editable values, supports nested property updates
*/
export interface MapConfig extends AttributeComponentProps {
type: 'map';
attributeId: string;
displayName: string;
/** 列定义数组 Array of column definitions */
columns: MapColumn[];
/** 初始值 Initial map value */
initialValue?: Record<string, any>;
}
// ============================================================================
// 组合组件 Composite Components
// ============================================================================
/**
* 字体设置配置
* Font setting configuration
*
* @description 组合了颜色、字号、字重、透明度等设置
* Combines color, size, weight, opacity settings
*/
export interface FontSettingConfig extends AttributeComponentProps {
type: 'fontSetting';
displayName: string;
/** 属性 ID 映射 Attribute ID mapping */
attributeIdMap: {
fontColor?: string;
fontSize?: string;
fontWeight?: string;
fontFamily?: string;
textAlign?: string;
opacity?: string;
};
/** 初始值 Initial values */
initialValue?: {
fontColor?: string;
fontSize?: number;
fontWeight?: string;
fontFamily?: string;
textAlign?: string;
opacity?: number;
};
}
/**
* 线条设置配置
* Line setting configuration
*
* @description 组合了线条颜色、线宽、虚线样式、长度等设置
* Combines line color, width, dash style, length settings
*/
export interface LineSettingConfig extends AttributeComponentProps {
type: 'lineSetting';
displayName: string;
/** 属性 ID 映射 Attribute ID mapping */
attributeIdMap: {
lineWidth?: string;
lineColor?: string;
lineDash?: string;
length?: string;
};
/** 初始值 Initial values */
initialValue?: {
lineWidth?: number;
lineColor?: string;
lineDash?: number[];
length?: number;
};
}
/**
* 端点设置配置
* Point setting configuration
*
* @description 用于设置线条端点样式
* Used for setting line endpoint styles
*/
export interface PointSettingConfig extends AttributeComponentProps {
type: 'pointSetting';
displayName: string;
attributeIdMap?: Record<string, string>;
initialValue?: any;
}
// ============================================================================
// 使用示例 Usage Examples
// ============================================================================
/**
* 完整配置示例
* Complete configuration example
*
* @example
* const config: AttributesConfig = {
* config: {
* type: 'collapse',
* children: [
* {
* displayName: '基础设置',
* show: true,
* children: [
* {
* type: 'input',
* attributeId: 'title',
* displayName: '标题',
* initialValue: '默认标题',
* placeholder: '请输入标题'
* },
* {
* type: 'checkbox',
* attributeId: 'showBorder',
* displayName: '显示边框',
* initialValue: true
* },
* {
* type: 'colorPicker',
* attributeId: 'borderColor',
* displayName: '边框颜色',
* initialValue: '#000000'
* }
* ]
* }
* ]
* }
* };
*/
// ============================================================================
// 最佳实践 Best Practices
// ============================================================================
/**
* 最佳实践指南
* Best Practices Guide
*
* 1. attributeId 命名规范 Naming Convention:
* - 使用点分隔的路径 Use dot-separated paths: 'style.fontSize'
* - 保持一致性和可读性 Maintain consistency and readability
* - 避免使用特殊字符 Avoid special characters
*
* 2. 初始值设置 Initial Values:
* - 始终提供合理的 initialValue Always provide reasonable initialValue
* - 确保初始值类型与组件匹配 Ensure type matches component
*
* 3. 分组组织 Grouping:
* - 使用 Collapse 组织大量配置项 Use Collapse for many items
* - 使用 Group 进行逻辑分组 Use Group for logical grouping
* - 相关配置项放在一起 Keep related items together
*
* 4. 性能优化 Performance:
* - 避免过深的嵌套层级 Avoid deep nesting
* - 合理使用 show 属性预先隐藏不需要的项 Use show to hide unnecessary items
* - 大量数据使用 Table 或 Map 组件 Use Table/Map for large datasets
*/

24
src/common/react-dom-shim.js vendored Normal file
View File

@@ -0,0 +1,24 @@
// react-dom-shim.js
const RD = window.ReactDOM;
export default RD;
// ReactDOM 18+ (createRoot / hydrateRoot)
export const {
createRoot,
hydrateRoot,
// ReactDOM 17 兼容 API一些环境仍然可能需要
render,
hydrate,
unmountComponentAtNode,
findDOMNode,
// Server side features (如果 CDN 提供)
createPortal,
// React 18 Transition API可能存在
flushSync,
unstable_batchedUpdates,
unstable_renderSubtreeIntoContainer,
} = RD;

47
src/common/react-shim.js vendored Normal file
View File

@@ -0,0 +1,47 @@
const R = window.React;
const RJSXRuntime = window.ReactJSXRuntime || {};
export default R;
export const {
useState,
useEffect,
useRef,
useMemo,
useCallback,
useContext,
useReducer,
useLayoutEffect,
useImperativeHandle,
useDebugValue,
useDeferredValue,
useTransition,
useId,
useSyncExternalStore,
useInsertionEffect,
forwardRef,
memo,
createElement,
Fragment,
Component,
PureComponent,
createContext,
createRef,
lazy,
Suspense,
StrictMode,
Profiler,
Children,
cloneElement,
isValidElement,
createFactory,
startTransition,
act,
version,
} = R;
// JSX Runtime exports for modern React
export const jsx = RJSXRuntime.jsx || createElement;
export const jsxs = RJSXRuntime.jsxs || createElement;
export const jsxDEV = RJSXRuntime.jsxDEV || createElement;

View File

@@ -0,0 +1,237 @@
/**
* @name 侧边菜单
*
* 参考资料:
* - /rules/development-guide.md
* - /rules/axure-api-guide.md
* - /docs/设计规范.UIGuidelines.md
*/
import './style.css';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
ChevronDown,
ChevronRight,
LayoutDashboard,
PanelLeftClose,
PanelLeftOpen,
Settings,
ShoppingBag,
User,
type LucideIcon,
} from 'lucide-react';
import { getNewlyOpenedSubmenuKey } from './side-menu-utils';
type MenuItemInput = {
key?: string;
label?: string;
icon?: string;
disabled?: boolean;
children?: MenuItemInput[];
};
type SideMenuProps = {
title?: string;
width?: number;
collapsible?: boolean;
defaultCollapsed?: boolean;
defaultSelectedKey?: string;
defaultOpenKeys?: string[];
items?: MenuItemInput[];
onMenuSelect?: (key: string) => void;
onCollapseChange?: (collapsed: boolean) => void;
};
function resolveIcon(name?: string): LucideIcon | null {
switch (name) {
case 'dashboard':
return LayoutDashboard;
case 'shop':
return ShoppingBag;
case 'user':
return User;
case 'setting':
return Settings;
default:
return null;
}
}
function normalizeItems(items: any): MenuItemInput[] {
if (!Array.isArray(items)) {
return [];
}
return items.map(function (it: any) {
return {
key: typeof it?.key === 'string' && it.key ? it.key : String(it?.key ?? it?.label ?? ''),
label: typeof it?.label === 'string' ? it.label : String(it?.label ?? ''),
icon: typeof it?.icon === 'string' ? it.icon : undefined,
disabled: it?.disabled === true,
children: Array.isArray(it?.children) ? normalizeItems(it.children) : undefined
};
}).filter(function (it: MenuItemInput) { return !!it.key; });
}
const DEFAULT_ITEMS: MenuItemInput[] = [
{ key: 'dashboard', label: '仪表盘', icon: 'dashboard' },
{
key: 'orders',
label: '订单管理',
icon: 'shop',
children: [
{ key: 'orders_list', label: '订单列表' },
{ key: 'orders_refund', label: '退款管理' }
]
},
{ key: 'users', label: '用户管理', icon: 'user' },
{ key: 'settings', label: '系统设置', icon: 'setting' }
];
const Component = function SideMenu(props: SideMenuProps) {
const title = typeof props.title === 'string' && props.title ? props.title : 'Axhub';
const width = typeof props.width === 'number' && props.width > 0 ? props.width : 240;
const collapsible = props.collapsible !== false;
const defaultCollapsed = props.defaultCollapsed === true;
const defaultSelectedKey = typeof props.defaultSelectedKey === 'string' && props.defaultSelectedKey
? props.defaultSelectedKey
: 'dashboard';
const defaultOpenKeys = Array.isArray(props.defaultOpenKeys)
? props.defaultOpenKeys.map(String).filter(Boolean)
: [];
const normalizedItems = useMemo(function () {
const fromProps = normalizeItems(props.items);
return fromProps.length > 0 ? fromProps : DEFAULT_ITEMS;
}, [props.items]);
const collapsedState = useState<boolean>(defaultCollapsed);
const collapsed = collapsedState[0];
const setCollapsed = collapsedState[1];
const selectedKeyState = useState<string>(defaultSelectedKey);
const selectedKey = selectedKeyState[0];
const setSelectedKey = selectedKeyState[1];
const openKeysState = useState<string[]>(defaultOpenKeys);
const openKeys = openKeysState[0];
const setOpenKeys = openKeysState[1];
useEffect(function syncSelectedKey() {
setSelectedKey(defaultSelectedKey);
}, [defaultSelectedKey]);
useEffect(function syncOpenKeys() {
setOpenKeys(defaultOpenKeys);
}, [defaultOpenKeys]);
const handleToggleCollapsed = useCallback(function () {
setCollapsed(function (prev) {
const next = !prev;
if (typeof props.onCollapseChange === 'function') {
props.onCollapseChange(next);
}
return next;
});
}, [props]);
const handleMenuClick = useCallback(function (info: any) {
const key = typeof info?.key === 'string' ? info.key : String(info?.key ?? '');
if (!key) {
return;
}
setSelectedKey(key);
if (typeof props.onMenuSelect === 'function') {
props.onMenuSelect(key);
}
}, [props]);
const handleOpenChange = useCallback(function (keys: any) {
const next = Array.isArray(keys) ? keys.map(String) : [];
const openedKey = getNewlyOpenedSubmenuKey(openKeys, next);
setOpenKeys(next);
if (openedKey && typeof props.onMenuSelect === 'function') {
props.onMenuSelect(openedKey);
}
}, [openKeys, props]);
const renderMenuItems = useCallback(function renderMenuItems(items: MenuItemInput[], level = 0): React.ReactNode {
return (
<ul className={level === 0 ? 'axhub-side-menu__list' : 'axhub-side-menu__sublist'}>
{items.map(function (item) {
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
const Icon = resolveIcon(item.icon);
const isOpen = item.key ? openKeys.includes(item.key) : false;
const isSelected = item.key === selectedKey;
const itemClassName = [
'axhub-side-menu__item',
isSelected ? 'axhub-side-menu__item--selected' : '',
item.disabled ? 'axhub-side-menu__item--disabled' : '',
hasChildren ? 'axhub-side-menu__item--parent' : '',
].filter(Boolean).join(' ');
return (
<li key={item.key} className={itemClassName}>
<button
type="button"
className="axhub-side-menu__item-button"
disabled={item.disabled}
title={collapsed ? item.label : undefined}
onClick={function () {
if (hasChildren) {
handleOpenChange(isOpen
? openKeys.filter(function (key) { return key !== item.key; })
: [...openKeys, String(item.key)]);
return;
}
handleMenuClick({ key: item.key });
}}
>
<span className="axhub-side-menu__item-main">
{Icon && <Icon className="axhub-side-menu__icon" aria-hidden="true" size={18} />}
{!collapsed && <span className="axhub-side-menu__label">{item.label}</span>}
</span>
{!collapsed && hasChildren && (
isOpen
? <ChevronDown className="axhub-side-menu__chevron" aria-hidden="true" size={16} />
: <ChevronRight className="axhub-side-menu__chevron" aria-hidden="true" size={16} />
)}
</button>
{!collapsed && hasChildren && isOpen && renderMenuItems(item.children || [], level + 1)}
</li>
);
})}
</ul>
);
}, [collapsed, handleMenuClick, handleOpenChange, openKeys, selectedKey]);
return (
<aside
className="axhub-side-menu"
style={{
width: collapsed ? 64 : width
}}
>
<div className={'axhub-side-menu__header' + (collapsed ? ' axhub-side-menu__header--collapsed' : '')}>
{!collapsed && <div className="axhub-side-menu__title">{title}</div>}
{collapsible && (
<button
className="axhub-side-menu__collapse"
type="button"
aria-label={collapsed ? '展开侧边菜单' : '折叠侧边菜单'}
onClick={handleToggleCollapsed}
>
{collapsed
? <PanelLeftOpen aria-hidden="true" size={18} />
: <PanelLeftClose aria-hidden="true" size={18} />}
</button>
)}
</div>
<nav className="axhub-side-menu__nav" aria-label={title}>
{renderMenuItems(normalizedItems)}
</nav>
</aside>
);
};
export default Component;

View File

@@ -0,0 +1,9 @@
export function getNewlyOpenedSubmenuKey(previousKeys: string[], nextKeys: string[]): string | null {
for (const key of nextKeys) {
if (!previousKeys.includes(key)) {
return key;
}
}
return null;
}

View File

@@ -0,0 +1,138 @@
# 侧边菜单
## 📋 业务与功能
### 1.1 核心目标
该组件提供后台场景常见的侧边导航能力,支持分级菜单与折叠交互。
该元素强调结构清晰、信息密度适中,适配后台场景。
### 1.2 功能清单
- **顶部标题区域**:显示应用或系统名称
- **菜单项区域**:支持一级/二级菜单项、图标与禁用状态
- **折叠按钮**:控制侧边栏折叠/展开
### 1.3 交互要点
- 点击菜单项:选中菜单项并触发 `onMenuSelect`
- 点击折叠按钮:切换折叠状态并触发 `onCollapseChange`
- 子菜单展开/收起:点击父级菜单项控制
- 悬停菜单项:高亮反馈
---
## 📊 内容规划
### 2.1 信息架构
```
侧边菜单
├── 顶部标题
│ └── 折叠按钮
└── 菜单列表
├── 一级菜单
└── 二级子菜单(可选)
```
### 2.2 数据来源
- **数据类型**:菜单项列表(`items`
- **数据源**用户提供props/ 内置默认菜单
- **关键字段**
- `key`: 唯一标识
- `label`: 显示文本
- `icon`: 图标名称dashboard / shop / user / setting
- `disabled`: 是否禁用
- `children`: 子菜单数组
### 2.3 内容示例
**默认菜单示例**
- 仪表盘 / 订单管理(含订单列表、退款管理)/ 用户管理 / 系统设置
---
## 🎨 布局与结构
### 3.1 整体布局
- **布局模式**:垂直侧边栏
- **容器宽度**:固定
- **关键尺寸**
- 展开宽度:`240px`
- 折叠宽度:`64px`
- 顶部标题区高度:`48px`
### 3.2 响应式适配
- **桌面端≥1200px**:默认展开显示
- **平板端768-1199px**:建议默认折叠或在主区保留更大空间
- **移动端(<768px**:建议折叠或使用抽屉式替代
---
## 🎨 视觉规范
### 4.1 设计规范来源
**设计依据**
- [x] 用户提供的设计规范:`/docs/设计规范.UIGuidelines.md`
- [x] 主题设计系统:`/src/themes/antd-new/`DESIGN.md + designToken.json + globals.css
### 4.2 自定义设计要点
**自定义色彩**(如有):
-
**自定义尺寸**(如有):
-
**其他自定义规范**(如有):
-
### 4.3 组件状态
- **默认态default**:菜单项正常显示
- **悬停态hover**:菜单项高亮背景
- **选中态selected**:当前菜单项高亮
- **禁用态disabled**:菜单项灰化不可点击
- **折叠态collapsed**:仅展示图标,标题隐藏
---
## ⚙️ Axure API 说明
### 5.1 事件列表eventList
### 5.2 动作列表actionList
### 5.3 变量列表varList
### 5.4 配置项列表configList
### 5.5 数据项列表dataList
**数据结构**
```typescript
{
items: Array<{
key: string; // 菜单项唯一标识
label: string; // 显示文本
icon?: string; // 图标名称
disabled?: boolean; // 是否禁用
children?: Array<{ // 子菜单项
key: string;
label: string;
icon?: string;
disabled?: boolean;
}>;
}>;
}
```

View File

@@ -0,0 +1,131 @@
.axhub-side-menu {
box-sizing: border-box;
min-height: 100vh;
flex: 0 0 auto;
overflow: hidden;
color: #1f2937;
background: #ffffff;
border-right: 1px solid #e5e7eb;
transition: width 180ms ease;
}
.axhub-side-menu__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid #f1f5f9;
}
.axhub-side-menu__header--collapsed {
justify-content: center;
padding: 0;
}
.axhub-side-menu__title {
font-size: 14px;
font-weight: 600;
color: #111827;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.axhub-side-menu__collapse {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
color: #475569;
background: transparent;
border: 0;
border-radius: 6px;
cursor: pointer;
}
.axhub-side-menu__collapse:hover {
background: #f1f5f9;
}
.axhub-side-menu__nav {
padding: 8px;
}
.axhub-side-menu__list,
.axhub-side-menu__sublist {
padding: 0;
margin: 0;
list-style: none;
}
.axhub-side-menu__sublist {
padding-left: 12px;
margin-top: 2px;
}
.axhub-side-menu__item {
margin: 2px 0;
}
.axhub-side-menu__item-button {
appearance: none;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 36px;
gap: 8px;
padding: 0 10px;
color: #334155;
font: inherit;
text-align: left;
background: transparent;
border: 0;
border-radius: 6px;
cursor: pointer;
}
.axhub-side-menu__item-button:hover {
background: #f8fafc;
}
.axhub-side-menu__item--selected > .axhub-side-menu__item-button {
color: #0f172a;
background: #e8f0ff;
}
.axhub-side-menu__item--disabled > .axhub-side-menu__item-button {
color: #94a3b8;
cursor: not-allowed;
}
.axhub-side-menu__item--disabled > .axhub-side-menu__item-button:hover {
background: transparent;
}
.axhub-side-menu__item-main {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 10px;
}
.axhub-side-menu__icon {
flex: 0 0 auto;
}
.axhub-side-menu__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.axhub-side-menu__chevron {
flex: 0 0 auto;
color: #64748b;
}

153
src/common/useHashPage.ts Normal file
View File

@@ -0,0 +1,153 @@
/**
* 多页面原型的轻量 hash 路由 hook。
*
* URL 格式: #page=<pageId>
* pageId 仅允许小写字母、数字、连字符a-z 0-9 -)。
*
* 用法:
* const { page, setPage } = useHashPage('home');
* const route = defineHashPageRoute([
* { id: 'dashboard', title: '工作台' },
* ], { defaultPageId: 'dashboard' });
* const { page, setPage, pages } = useHashPage(route);
*/
import { useCallback, useEffect, useState } from 'react';
export interface HashPageRoutePage {
id: string;
title: string;
}
export interface HashPageRoute {
pages: HashPageRoutePage[];
defaultPageId: string;
}
const PAGE_ID_RE = /^[a-z0-9-]+$/u;
function normalizePageId(value: unknown): string {
const id = typeof value === 'string' ? value.trim() : '';
return PAGE_ID_RE.test(id) ? id : '';
}
function normalizeRoutePages(pages: HashPageRoutePage[]): HashPageRoutePage[] {
if (!Array.isArray(pages)) {
return [];
}
return pages
.map((page) => {
const id = normalizePageId(page?.id);
const title = typeof page?.title === 'string' ? page.title.trim() : '';
return id && title ? { id, title } : null;
})
.filter((page): page is HashPageRoutePage => Boolean(page));
}
export function parseHashPage(hash: string): string | null {
const rawHash = String(hash || '').replace(/^#/, '');
const pageId = new URLSearchParams(rawHash).get('page');
return normalizePageId(pageId) || null;
}
export function parseSearchPage(search: string): string | null {
const rawSearch = String(search || '').replace(/^\?/, '');
const pageId = new URLSearchParams(rawSearch).get('page');
return normalizePageId(pageId) || null;
}
export function defineHashPageRoute(
pages: HashPageRoutePage[],
options?: { defaultPageId?: string },
): HashPageRoute {
const normalizedPages = normalizeRoutePages(pages);
const defaultPageId = normalizePageId(options?.defaultPageId);
return {
pages: normalizedPages,
defaultPageId: normalizedPages.some((page) => page.id === defaultPageId)
? defaultPageId
: normalizedPages[0]?.id || 'home',
};
}
function normalizeRouteInput(routeOrDefault?: HashPageRoute | string): HashPageRoute {
if (routeOrDefault && typeof routeOrDefault === 'object') {
return defineHashPageRoute(routeOrDefault.pages, { defaultPageId: routeOrDefault.defaultPageId });
}
const defaultPageId = normalizePageId(routeOrDefault) || 'home';
return {
pages: [],
defaultPageId,
};
}
function notifyHostPrototypePageChange(pageId: string) {
if (typeof window === 'undefined' || window.parent === window) {
return;
}
window.parent.postMessage({
type: 'AXHUB_PROTOTYPE_PAGE_CHANGE',
pageId,
}, '*');
}
function notifyHostPrototypeRouteInfo(
pages: HashPageRoutePage[],
defaultPageId: string,
activePageId: string,
) {
if (
pages.length === 0
|| typeof window === 'undefined'
|| window.parent === window
) {
return;
}
window.parent.postMessage({
type: 'AXHUB_PROTOTYPE_ROUTE_INFO',
pages,
defaultPageId,
activePageId,
}, '*');
}
export function useHashPage(routeOrDefault: HashPageRoute | string = 'home') {
const route = normalizeRouteInput(routeOrDefault);
const { pages, defaultPageId } = route;
const routeSignature = `${defaultPageId}:${pages.map((routePage) => `${routePage.id}=${routePage.title}`).join('|')}`;
const [page, setPageState] = useState<string>(() => {
if (typeof window === 'undefined') {
return defaultPageId;
}
return parseHashPage(window.location.hash) ?? parseSearchPage(window.location.search) ?? defaultPageId;
});
useEffect(() => {
notifyHostPrototypeRouteInfo(pages, defaultPageId, page);
}, [routeSignature]);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const onHashChange = () => {
const next = parseHashPage(window.location.hash) ?? parseSearchPage(window.location.search);
const nextPageId = next ?? defaultPageId;
setPageState(nextPageId);
notifyHostPrototypePageChange(nextPageId);
};
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, [defaultPageId]);
const setPage = useCallback((pageId: string) => {
const nextPageId = normalizePageId(pageId);
if (!nextPageId || typeof window === 'undefined') {
return;
}
window.location.hash = `page=${nextPageId}`;
}, []);
return { page, setPage, pages, defaultPageId };
}

177
src/index.html Normal file
View File

@@ -0,0 +1,177 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axhub Make Project</title>
<style>
:root {
color: #20242a;
background: #f7f8fa;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body {
min-height: 100vh;
margin: 0;
}
body {
display: grid;
place-items: center;
}
main {
box-sizing: border-box;
width: min(100%, 680px);
padding: 32px;
text-align: center;
}
p {
margin: 0;
color: #22262d;
font-size: 22px;
font-weight: 500;
line-height: 1.7;
}
.actions {
display: flex;
justify-content: center;
margin-top: 20px;
}
button {
border: 0;
border-radius: 8px;
background: #20242a;
color: #fff;
cursor: pointer;
font: inherit;
font-size: 15px;
font-weight: 600;
line-height: 1;
min-height: 40px;
padding: 0 18px;
}
button:disabled {
cursor: progress;
opacity: 0.68;
}
.status {
color: #5f6673;
font-size: 14px;
line-height: 1.6;
margin-top: 12px;
min-height: 22px;
}
@media (max-width: 520px) {
main {
padding: 24px;
}
p {
font-size: 18px;
}
}
</style>
</head>
<body>
<main>
<p>可以通过 npx @axhub/make 启动管理页面。</p>
<div class="actions">
<button id="start-button" type="button" hidden>启动管理服务</button>
</div>
<div id="status" class="status">正在检查管理服务...</div>
</main>
<script>
(function () {
var statusElement = document.getElementById('status');
var startButton = document.getElementById('start-button');
var pollTimer = 0;
function setStatus(text) {
if (statusElement) {
statusElement.textContent = text;
}
}
function redirectWhenReady(payload) {
if (payload && payload.ready && payload.adminUrl) {
location.replace(payload.adminUrl);
return true;
}
return false;
}
function showStartButton() {
if (startButton) {
startButton.hidden = false;
startButton.disabled = false;
startButton.textContent = '启动管理服务';
}
}
function schedulePoll() {
window.clearTimeout(pollTimer);
pollTimer = window.setTimeout(checkStatus, 1200);
}
async function checkStatus() {
try {
var response = await fetch('/__axhub/make-server/status', { headers: { accept: 'application/json' } });
var payload = await response.json();
if (redirectWhenReady(payload)) {
return;
}
if (payload && payload.starting) {
setStatus('管理服务正在启动...');
schedulePoll();
return;
}
setStatus(payload && payload.error ? payload.error : '管理服务尚未启动。');
showStartButton();
} catch (error) {
setStatus('管理服务尚未启动。');
showStartButton();
}
}
async function startServer() {
if (!startButton || startButton.disabled) {
return;
}
startButton.disabled = true;
startButton.textContent = '启动中...';
setStatus('正在执行 npx @axhub/make...');
try {
var response = await fetch('/__axhub/make-server/start', { method: 'POST', headers: { accept: 'application/json' } });
var payload = await response.json();
if (redirectWhenReady(payload)) {
return;
}
setStatus(payload && payload.error ? payload.error : '管理服务正在启动...');
schedulePoll();
} catch (error) {
setStatus('启动失败,请稍后重试。');
showStartButton();
}
}
if (startButton) {
startButton.addEventListener('click', startServer);
}
checkStatus();
}());
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
html,
body,
#root {
min-height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
{{PREVIEW_LOADER}}
</script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
body {
max-width: 920px;
margin: 0 auto;
padding: 32px 20px;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #f7f8fb;
}
nav {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
nav a {
color: #1760bf;
text-decoration: none;
}
pre {
white-space: pre-wrap;
line-height: 1.65;
font-family: inherit;
background: #fff;
border: 1px solid #e4e8f0;
border-radius: 8px;
padding: 24px;
}
</style>
</head>
<body>
<nav>{{DOC_TABS}}</nav>
<pre>{{MARKDOWN}}</pre>
</body>
</html>

View File

@@ -0,0 +1,63 @@
# Prototype Review
- 审查目标:`src/prototypes/xll-miniapp`(小羚羚小程序)
- 用户资料/参考资料:
- `src/resources/xll-miniapp/PRD-00-小程序总览.md`V1.3
- `src/resources/xll-miniapp/PRD-01-审批中心改版说明.md`V1.3
- `src/prototypes/xll-miniapp/annotation-source.json``page-audit``page-audit-return``page-audit-pickup``prd-overview``prd-approval-v11`
- `src/prototypes/xll-miniapp/index.tsx`(单文件原型实现)
- 生成时间2026-06-29
- 修复完成2026-06-29
## 总体点评
小羚羚小程序原型在**审批中心 V1.4** 上已与 PRD/标注对齐:四 Tab 列表、流程 Chip、标题搜索、**更多筛选**与**更多类型**(手机预览框内 `.xll-mod-sheet-*`)、还车四部门子流程标题、提车实收口径统一、占位类型通用详情页、还车主流程「业务管理组发起」规则及三条 mock 场景均已落地。
当前剩余限制主要为原型固有约束:审批操作不回写 mock 状态、无多角色切换、合同审批等通用详情页为最小骨架(非业务专属办理页)。整体可作为对外演示与评审基底。
## P0-P3 优先级问题(已全部修复)
### ~~P1 - 还车四部门子流程列表主标题~~ ✅ 已修复
- 修复:`acCardBoldTitle``acIsReturnSettleSubFlow` 返回完整 `flowType`(如「还车应结款-业务管理组」);主流程「还车应结款」仍返回车牌。
### ~~P1 - 提车应收款实收金额列表与详情 Hero 口径不一致~~ ✅ 已修复
- 修复:详情 Hero 统一使用 `acPickupDisplayActualAmount(task)``ap-2` mock `actualAmount` 校正为 `218510.00`
### ~~P2 - 部分审批类型仅有列表占位~~ ✅ 已修复(最小详情页)
- 修复:新增 `GenericWorkflowApprovePage``openTaskDetail` 对所有类型打开详情;合同审批、氢费对账单、车辆异动等可进入通用办理/查看页。
### ~~P2 - 还车主流程「四部门通过后待审」缺可演示样例~~ ✅ 已修复
- 修复:`ap-15`/`ap-18`/`ap-19`/`ap-20` 状态改为「已通过」;`ap-14` 主流程在「我的待办」可见,当前节点「财务审核」。
### ~~P3 - 「更多类型」仍用 Ant Design 全宽 Drawer~~ ✅ 已修复
- 修复:改为 `.xll-mod-sheet-overlay` 手机框内底部抽屉,与「更多筛选」体验一致。
## 完整性与项目对齐
| 维度 | 对齐情况 |
| --- | --- |
| 核心用户 | 一线运维、管理人员PRD-00原型以 `MOCK_USER = '张明辉'` 模拟待办/已办/抄送,无法切换角色 |
| 主流程 | 工作台待办 → 业务入口 → 各运维模块;审批中心列表 → 详情办理,与 PRD-00 一致 |
| 信息架构 | 10 个 hash 功能页;默认免登录进入工作台;导航栏无「需求说明」按钮 |
| 审批中心搜索/筛选 | 标题搜索、发起人滚轮、发起时间滚轮、更多类型 sheet — **已对齐** |
| 审批中心卡片 | 还车子流程主标题、审批人 +N、发起时间至分钟 — **已对齐** |
| 还车应结款 | 主/子 Hero、部门费项、四部门全过后业务管理组发起主流程、三条 mock 场景 — **已对齐** |
| 提车应收款 | 列表与 Hero 实收口径 — **已对齐** |
| 占位类型 | 通用详情页骨架,可演示进入办理链路 |
## 已知原型限制(非阻塞)
- 审批操作为前端 toast不回写 `AC_MOCK_TASKS`
- 无真实权限/多角色切换
- 合同审批等通用详情页不含业务专属字段与流程节点
- 工作台 `transfer`/`move`/`return` 与审批中心调拨详情路径分离
## 证据与评估说明
- **用户资料优先级**:以 PRD-01 V1.3 + `annotation-source.json` 标注为业务基线
- **修复验证**:源码审查 + `node scripts/check-app-ready.mjs /prototypes/xll-miniapp`typecheck 因项目既有 `@types/react` 缺失失败,与本次改动无关;`index.tsx` 无新增 lint 问题)

View File

@@ -0,0 +1,82 @@
# UI Review
- 审查目标:`src/prototypes/xll-miniapp`(小羚羚小程序)
- 使用设计依据:`src/prototypes/xll-miniapp/DESIGN.md`V1.0,自现有源码 token/组件规范提炼)
- 生成时间2026-06-29
- 修复时间2026-06-29
## 总体点评
小羚羚小程序在**手机预览框 + 微信式 chrome** 上形成了较稳定的自有视觉语言:品牌绿 `#7AB929`、Arco 系中性灰、14px 圆角卡片与流程色条标签,审批中心近期的 **`.xll-mod-sheet-*` 手机框内抽屉**与滚轮选择器与 DESIGN.md 方向一致。运维类长表单交车、车辆管理信息密度高但层级尚可辨认Hero 区按流程配色有助于扫读。
**2026-06-29 修复后**P1P3 共 5 项已全部落地——底栏与列表辅助文字可读性提升、卡片 focus 环补齐、车辆筛选改为框内 sheet、Ant `message` 统一为框内 `MpToast`、列表卡片左侧色条改为顶部 2px 色带。Impeccable detector 仅剩 1 条 `layout-transition` warning非本次范围
## 修复记录
| 优先级 | 问题 | 状态 | 修复说明 |
|--------|------|------|----------|
| P1 | 辅助文字字号与对比度不足 | ✅ 已修复 | 底栏 Tab 13px`.xll-mod-list-head` / `.xll-mod-meta` 13px + `COLOR_TEXT_SEC` |
| P1 | 列表卡片缺 focus 环 | ✅ 已修复 | `.xll-mod-card:focus-visible``.xll-mod-card-btn:focus-visible` |
| P2 | 车辆管理 Ant Drawer | ✅ 已修复 | 筛选改为 `.xll-mod-sheet-overlay`,与审批/交车一致 |
| P2 | Ant message 脱离手机框 | ✅ 已修复 | 封装 `MpToast` + `MpToastHost`,全文件 117 处替换 |
| P3 | 左侧 3px 色条 AI accent | ✅ 已修复 | 卡片改为顶部 2px 色带;`.xll-prd-highlight` 改顶部色带 |
## P0-P3 优先级问题(原始记录)
### P1 - 辅助文字字号与对比度未达 DESIGN.md 可读底线 ✅
- 证据:底栏 Tab 标签 `font-size: 11px``.xll-tabbar-btn`);列表区头 / 元信息 `12px` + `COLOR_MUTED`
- 修复:底栏 **13px**;列表区头 / meta **13px** + `COLOR_TEXT_SEC`;「去审批」按钮 **44px** 触控高。
### P1 - 可点击列表卡片缺少键盘 focus 可视反馈 ✅
- 证据:`.xll-mod-card``:focus-visible`
- 修复:`outline: 2px solid XLL_GREEN; outline-offset: 2px`;上传区 `.xll-mod-upload:focus-visible` 同步补齐。
### P2 - 车辆管理等模块仍用 Ant Design 全宽 Drawer ✅
- 证据:车辆管理「筛选车辆」曾用 `<Drawer placement="bottom">`
- 修复:迁移为 `.xll-mod-sheet-panel` 结构,挂载于 `.xll-mod-root` 内;返回键可关闭 sheet。
### P2 - Ant Design `message` 反馈脱离手机预览框 ✅
- 证据:全文件大量 `message.*` 挂载 document 层。
- 修复:`mpToast` 限定在 `.xll-phone` 内顶部 stack 展示。
### P3 - 列表卡片左侧色条被识别为通用 AI 侧栏 accent ✅
- 证据:`.xll-mod-card::before` 3px 左侧色条;`.xll-prd-highlight` 左侧 border。
- 修复:改为**顶部 2px 色带**;工作台 `.xll-task-card` 同步。
## 核心元件
### 手机 Chrome状态栏 / 导航栏 / 胶囊 / 底栏 Tab
**符合点**390px 预览框、44px 触控区、Tab 带 `role="tablist"``aria-selected`;底栏标签已升至 13px。
### 审批中心Tab + 搜索 + Chip + 列表卡片 + Sheet
**符合点**:四 Tab 结构清晰;搜索 15px + focus-within 绿环;框内 sheet卡片 focus 环与 44px 操作按钮。
### 详情 Hero + 底部操作栏(提车 / 还车 / 通用审批)
**符合点**`.tc-hero` 渐变 + 大字号金额;`.tc-action-bar` 固定底栏、按钮高度 44px+`prefers-reduced-motion` 已覆盖 sheet 动画。
### 运维长表单(交车 / 车辆管理 / 年审)
**符合点**分步导航、44px 表单行;车辆筛选已与审批中心 sheet 一致;上传区有 focus 样式。
## 响应式与可访问性
- **响应式**:原型锁定 390px 宽,无断点需求。
- **触控**主按钮与「去审批」≥44px。
- **键盘**Tab 栏 / 搜索 / 返回 / 列表卡片 / 上传区有 focus 环。
- **对比度**:列表辅助信息改用 `COLOR_TEXT_SEC` 13px可读性提升。
- **Reduced motion**sheet / 卡片入场已降级。
- **语义**:审批 Tab、筛选 sheet 有 `aria-modal` / `aria-label`toast 有 `aria-live="polite"`
## 证据与评估说明
- **浏览器/截图**:本次未启动浏览器目视检查;结论基于源码静态审查 + 修复 diff。
- **Scanner**`detect.mjs` 修复后仅剩 1 条 `layout-transition: width` warning约 461 行),`side-tab` 已消除。
- **独立评估**`degraded`(无浏览器双评估)。

View File

@@ -0,0 +1,152 @@
# 小羚羚小程序 · 设计规范DESIGN
**版本**V1.0
**更新日期**2026-06-29
**适用范围**`src/prototypes/xll-miniapp` 全部页面与内联模块
---
## 1. 产品身份
- **产品**:氢能车辆运营移动端小程序原型(一线运维 / 管理人员)
- **形态**390×844 手机预览框 + 微信式顶栏(状态栏 / 导航栏 / 胶囊)+ 底部 Tab
- **气质**:务实、清晰、字段可读;偏 Arco Design 中性色 + 品牌绿,非营销页
- **不适用**:桌面宽屏布局、深色模式(本期未定义)
---
## 2. 色彩系统
| Token | 值 | 用途 |
| --- | --- | --- |
| `XLL_GREEN` | `#7AB929` | 主色、Tab 选中、主按钮 |
| `XLL_GREEN_DEEP` | `#6AA322` | 主色深阶、强调文字 |
| `XLL_GREEN_SOFT` | `rgba(122,185,41,0.14)` | 主色浅底、Chip 选中 |
| `COLOR_TEXT` | `#1D2129` | 主文字 |
| `COLOR_TEXT_SEC` | `#4E5969` | 次级文字 |
| `COLOR_MUTED` | `#86909C` | 辅助 / 标签(**仅用于 ≥13px 或非关键信息** |
| `COLOR_LINE` | `#E5E6EB` | 分割线 |
| `COLOR_BG` | `#FFFFFF` | 卡片 / 顶栏 / 底栏 |
| `COLOR_PAGE` | `#F2F3F5` | 页面底、输入框底 |
| `COLOR_DANGER` | `#F53F3F` | 驳回 / 危险 |
| `COLOR_WARN` | `#FF7D00` | 审批中 / 警告 |
| `COLOR_SUCCESS` | `#00B42A` | 通过 / 成功 |
**流程 accent**(审批卡片色条、类型标签):按 `AC_FLOW_THEME` 映射,不替代主色。
**对比度**:正文与背景 ≥ **4.5:1**WCAG AA大号标题≥18px 或 bold ≥14px**3:1**
---
## 3. 字体与层级
- **字体栈**`-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, sans-serif`
- **移动端正文**:列表标题、表单值、搜索框 **1516px**
- **辅助信息**1213px不得承载唯一关键决策信息
- **Tab / 底栏标签**:最小 **12px**,推荐 **13px**;避免 11px 作为可读主文案
- **金额 Hero**2836px`font-variant-numeric: tabular-nums`
- **字重**:标题 700800正文 500600标签 600
---
## 4. 布局与间距
- **手机框**`max-width: 390px`,圆角 28px居中于灰底渐变
- **安全区**:底部 sheet / 操作栏使用 `env(safe-area-inset-bottom)`
- **页面内边距**:列表 / 模块横向 **1416px**
- **卡片**:圆角 **14px**,间距 **12px**,轻阴影 + 1px 浅边框
- **模块根**`.xll-mod-root` 占满 body内部滚动详情 `.xll-mod-detail-wrap`
---
## 5. 核心组件规则
### 5.1 导航
- **顶栏**:高 48px返回 / 铃铛 **44×44** 触控区;标题居中 17px bold
- **底栏 Tab**:高 52px + safe area图标 24px + 文字标签;`role="tablist"` + `aria-selected`
- **内页返回**:模块内 `onRegisterBack` 栈式返回sheet → 详情 → 列表)
### 5.2 列表卡片(`.xll-mod-card`
- 左侧 **3px** 流程色条(`--mod-accent`
- 结构:类型标签 + 状态 → 加粗标题 → 副标题 → 元信息 → 底栏时间 + 「去审批」
- 整卡可点;待办态显式按钮
- **必须**:键盘 `focus-visible`2px `XLL_GREEN`
### 5.3 筛选与搜索
- 搜索框 min-height **44px**15px 字号
- Chip 高 **36px**;横向滚动
- **更多筛选 / 更多类型**`.xll-mod-sheet-overlay`,相对手机框 `absolute`**禁止** Ant Design 全浏览器宽 `Drawer`
### 5.4 滚轮选择器(`.xll-mp-picker-*`
- 三列年/月/日或单列选项;行高 **44px**
- 高亮条 `z-index: 0`,文字列 `z-index: 1`
- 叠在 sheet 之上
### 5.5 详情 Hero`.tc-hero`
- 按流程渐变(提车橙、还车紫、调拨绿等)
- 还车子流程:部门名 + **车牌胶囊**重点样式,不展示汇总金额
- 主流程还车:应退/应补 + 结算明细
### 5.6 底部操作栏(`.tc-action-bar` / `.xll-mod-action-bar`
- 固定底栏;按钮 min-height **4448px**
- 待办:评论 | 终止 | 驳回 | 通过(通过为主色填充)
- 审批半屏表单:`.tc-mini-sheet-*`,限定手机框内
### 5.7 反馈
- 操作结果优先 **手机框内** toast / inline 提示
- 避免仅依赖 viewport 级 Ant `message`(与预览框脱节)
- destructive 操作需半屏确认表单
---
## 6. 动效
- Sheet / 卡片入场:`translateY` / `opacity`**0.24s ease**
- 按压:`:active` 背景或 `scale(0.98)`150ms
- **`prefers-reduced-motion: reduce`**:关闭 sheet 动画与卡片入场
---
## 7. 无障碍
- 交互控件 min **44×44px**
- 图标按钮必须 `aria-label`
- 可点击 div/卡片:`role="button"` + `tabIndex={0}` + **`:focus-visible`**
- 表单输入保留可见 focus 环
- 状态不单靠颜色(审批状态含文案)
---
## 8. 禁止做法Don't
- 不用 emoji 作功能图标(用 SVG`IconSearch``IconFilter` 等)
- 不在手机预览框外弹出全宽 Ant Drawer除标注工具等 IDE 层)
- 不用 11px 承载 Tab 标签或列表关键字段
- 不用 `#86909C` 12px 文字表达必须阅读的金额 / 状态
- 不混用两套底部面板模式sheet vs Ant Drawer于同一产品主路径
---
## 9. 页面范围
| 路由 | 模块 |
| --- | --- |
| `todo` | 工作台待办卡片 |
| `business` | 业务入口宫格 |
| `audit` | 审批中心Tab + 筛选 + 详情) |
| `delivery` / `inspection` / `vehicle` / … | 运维办理长表单 |
| `map` / `mine` | 地图、我的 |
---
## 10. Mock 与原型边界
- 数据为前端 mocktoast 表示占位行为
- 标注目录承载 PRD导航栏不设「需求说明」入口

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
{
"preserveFolderIds": ["directory-docs"],
"groups": [
{
"folderId": "directory-main",
"folderTitle": "主流程",
"defaultExpanded": true,
"pageIds": ["todo", "business", "map", "mine"]
},
{
"folderId": "directory-modules",
"folderTitle": "业务模块",
"defaultExpanded": true,
"pageIds": ["audit", "inspection", "vehicle", "delivery", "third-return", "replace"]
}
],
"extraRoutes": [
{
"folderId": "directory-modules",
"route": "audit-return",
"title": "还车应结款(说明)"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/* 小羚羚小程序 — 主样式内联于 index.tsx 的 PAGE_STYLE */

0
src/resources/.gitkeep Normal file
View File

2
src/resources/README.md Normal file
View File

@@ -0,0 +1,2 @@
Stores project documentation, reference notes, and other long-lived project materials.
Put document images and supporting attachments in `assets/` or beside the document that uses them.

View File

@@ -0,0 +1,31 @@
# 小羚羚小程序 · 总览
氢能车辆运营移动端原型,面向一线运维与管理人员。**原型无需登录**,默认进入工作台。
## 核心路径
1. 工作台处理待办
2. 业务 → 进入各运维 / 审批模块(独立原型页)
3. 审批中心 → 本页内跳转**本期已接入**的审批工作流(提车应收款、还车应结款及子类型、租赁账单、车辆调拨、替换车申请等)
## 页面导航
左侧预览列表可切换 10 个功能页;各业务模块内列表与表单在同一页内跳转。
## 最新改版
审批中心与还车应结款多部门子流程见 **PRD 01审批中心改版说明**V1.4)。
### 审批中心要点(与标注 `page-audit` 一致)
- 搜索:仅按卡片加粗标题(工作流标题)模糊匹配
- 更多筛选:发起人(滚轮选择)、发起时间(年/月/日滚轮,起止日期)
- 底部抽屉与选择器:限定在手机预览框内,模拟小程序交互
- 还车应结款:主流程以车牌为主标题;四部门子流程以「还车应结款-部门名」为主标题;四部门全过后须业务管理组发起主流程
## 需求文档与标注
- **标注目录**PRD 00 / PRD 01 / PRD 02 及页面说明(`page-audit``page-delivery` 等)
- **PRD 02交车说明**:交车列表、分步办理与签章流程
- **导航栏**:不设「需求说明」入口;以标注工具与目录为主
- **业务评审**`.spec/prototype-review.md`

View File

@@ -0,0 +1,262 @@
# 小羚羚小程序 · 审批中心改版说明
**文档版本**V1.4
**更新日期**2026-06-29
**适用范围**:审批中心、还车应结款、提车应收款及相关导航结构
---
## 一、改版背景
本次迭代聚焦审批体验与还车应结款多部门子流程,并调整原型信息架构:去除登录门槛、按功能拆分为独立预览页,便于评审与交付。
---
## 二、信息架构与导航
### 2.1 去除登录
- 原型默认进入「工作台 · 待办」,无需登录页。
- 左侧预览列表可切换 **10 个功能页**hash 路由)。
- 源码中仍保留登录页组件与样式,**不在默认导航与预览目录中暴露**;评审以「免登录直达工作台」为准。
### 2.2 页面清单
| 路由 | 页面 | 说明 |
| --- | --- | --- |
| `todo` | 工作台 | 待办聚合,默认入口 |
| `business` | 业务 | 运维 / 审批 / 数据可视化入口 |
| `map` | 地图 | 氢能车与加氢站 |
| `mine` | 我的 | 个人信息 |
| `audit` | 审批中心 | 列表 + 本页内跳转各审批详情 |
| `audit-return` | 还车应结款(说明) | 还车应结款专项说明页 |
| `delivery` | 交车 | 列表与办理表单同页切换 |
| `inspection` | 年审 | 列表与办理表单同页切换 |
| `vehicle` | 车辆管理 | 列表与详情同页切换 |
| `third-return` | 三方退车 | 列表与办理同页切换 |
| `replace` | 替换车 | 列表与详情同页切换 |
### 2.3 核心路径
1. 工作台处理待办 → 「去处理」跳转对应业务页
2. 业务 → 进入各运维 / 审批模块(独立原型页)
3. 审批中心 → 本页内打开**本期已接入**的审批工作流详情(见 §3.5
### 2.4 需求文档入口
- 导航栏**不设**「需求说明」按钮(按批注已移除该入口)。
- 需求与 PRD 通过**左侧标注目录**「PRD 00小程序总览」「PRD 01审批中心改版说明」及页面标注节点`page-audit`)查看。
---
## 三、审批中心列表
### 3.1 Tab
四类:**我发起的**、**我的待办**、**我的已办**、**抄送我的**。
### 3.2 筛选与搜索
- **流程类型 Chip**:全部、合同审批、提车应收款、租赁账单、**还车应结款**(含四部门子类型)、车辆调拨等;「更多类型」抽屉展示全部流程类型。
- 「还车应结款」Chip 同时匹配主流程与四个部门子流程。
- **更多筛选**(搜索框右侧筛选按钮):**发起人**、**发起时间**(开始至结束,按日期区间过滤)。
- **搜索**:按**卡片加粗标题(工作流标题)**模糊匹配;不按单据号、摘要等多字段联合检索。示例:提车应收款搜**客户全称**;还车应结款主流程搜**车牌号**;还车四部门子流程搜**「还车应结款-部门名」**。
> **决策说明**(与 `page-audit` 标注一致):发起人、发起时间从搜索中拆出,归入「更多筛选」,搜索仅面向列表主识别字段。
### 3.3 列表卡片字段
以提车应收款为例:
| 区域 | 说明 |
| --- | --- |
| 左上角标签 | 流程类型,如「提车应收款」 |
| 右上角状态 | 审批中 + 当前节点审批人(最多 2 人,超出显示 +N |
| 加粗标题 | 工作流标题(见 §3.4 标题规则) |
| 副标题 | 合同项目名称等 |
| 元信息 | 发起人、实收金额(提车:选中车辆租金 + 服务费 + 保证金合计,**不含氢费预付款** |
| 底部 | 发起时间精确至分钟;待办卡片可点「去审批」或整卡进入办理页 |
**还车应结款**:主流程以车牌为主标题并展示应退/应补;四部门子流程主标题为「还车应结款-部门名」,不展示汇总金额。四部门子流程全部通过后,须由**业务管理组主动发起**主流程审批,任务才会进入审批列表。
### 3.4 列表卡片标题规则
| 类型 | 主标题(加粗) | 辅信息 | 金额 |
| --- | --- | --- | --- |
| 主流程「还车应结款」 | **车牌号** | 客户、项目等 | 展示应退 / 应补 |
| 四部门子流程 | **还车应结款-部门名** | 车牌、客户等 | 不展示汇总金额 |
| 提车应收款 | **客户全称** | 合同项目 | 实收金额(见 §3.3 |
| 租赁账单 / 替换车申请 | **客户全称** | — | 按业务 |
| 车辆调拨 | **调拨路线摘要** | — | 按业务 |
| 其他流程 | 摘要或单据号 | — | 按业务 |
部门子流程包括:**还车应结款-业务管理组**、**还车应结款-能源服务组**、**还车应结款-运维组**、**还车应结款-安全部**。
### 3.5 跳转与本期原型范围
**已接入详情页(页内跳转)**:提车应收款、还车应结款(含四部门子类型)、租赁账单、车辆调拨、替换车申请。待办 Tab 展示底部操作栏;其余 Tab 为只读查看态。
**列表占位(点击 Toast 提示,暂无办理页)**:合同审批、氢费对账单(对站/对客)、车辆异动等。演示时可在列表中看到待办,但无法进入完整办理流程。
**审批操作弹窗**:待办详情底部「通过」「驳回」「终止」「评论」均从手机屏幕底部滑出半屏表单(限定在手机预览框内);通过/驳回/终止除标题外字段一致(通知方式、附件上传、抄送人、审批意见)。「通过」标题为「审批通过」。
### 3.6 更多筛选交互(小程序式)
| 能力 | 规则 |
| --- | --- |
| 底部抽屉 | 「更多筛选」及同类 `.xll-mod-sheet-*` 抽屉从**手机预览框底部**滑出,遮罩与面板相对当前页面模块定位,**不铺满浏览器全屏** |
| 发起人 | 表单行展示「请选择 ›」;点击后弹出底部**滚轮选择器**,选项来自 mock 发起人列表,首项「不限」 |
| 发起时间 | 「开始日期 至 结束日期」两行可点;分别弹出**年 / 月 / 日**三列滚轮,工具栏「取消 / 确定」 |
| 嵌套层级 | 滚轮选择器叠在筛选抽屉之上(`z-index` 更高),同样限定在手机预览框内;选中高亮条在文字下层,不遮挡选项文字 |
---
## 四、还车应结款审批
### 4.1 流程结构
**主工作流**`还车应结款` — 汇总四部门费用,判定保证金抵扣后的应退/应补。
**四个子流程**(各部门独立提交审批):
| 子流程 | 提交部门 |
| --- | --- |
| 还车应结款-业务管理组 | 业务管理组 |
| 还车应结款-能源服务组 | 能源服务组 |
| 还车应结款-运维组 | 运维组 |
| 还车应结款-安全部 | 安全部 |
**提交规则**
1. 四个子流程分别由对应部门填报费用并**独立提交审批**
2. 四个子流程**全部审批通过**后,**不会自动生成**主流程「还车应结款」审批任务;
3. 须由**业务管理组**在还车结算侧**主动发起**主流程审批,发起后任务进入审批列表;
4. 主流程进入列表后,由财务等环节整体审批;详情页展示四部门子流程通过状态;
5. 四部门未全部通过前,主流程不可发起;若已进入主流程详情,「通过」操作在四部门未全过时仍被拦截。
#### 原型 mock 场景(开发 / 测试对照)
| 场景 | 车牌 | 单据号 | 说明 |
| --- | --- | --- | --- |
| A | 粤B58888F | HC-2026-0418 | 四部门已通过 → 业务管理组已发起 → 主流程在「我的待办」(财务审核) |
| B | 浙F06901F | HC-2026-0625 | 四部门子流程均在待办(审批中);主流程未发起 |
| C | 苏E12345F | HC-2026-0610 | 四部门已通过 → 业务管理组**尚未发起** → 主流程不出现在列表 |
> 主流程发起人 mock 为业务管理组人员(如陈高伟);发起时间晚于四部门子流程全部通过时间。
### 4.2 Hero 区规则
| 类型 | Hero 主信息 | 是否展示应退/应补 |
| --- | --- | --- |
| 主流程「还车应结款」 | 应退还总额 / 应补缴总额;保证金、待结算总额、车辆实际租金;「结算明细」抽屉;部门子流程进度 | **是** |
| 业务管理组子流程 | 「还车应结款-业务管理组」+ **车牌重点样式** | 否 |
| 能源服务组子流程 | 「还车应结款-能源服务组」+ 车牌重点样式 | 否 |
| 运维组子流程 | 「还车应结款-运维组」+ 车牌重点样式 | 否 |
| 安全部子流程 | 「还车应结款-安全部」+ 车牌重点样式 | 否 |
**车牌重点样式**Hero 内展示「车牌号」标签 + 加粗胶囊样式车牌,便于子流程快速识别车辆。
子流程 Hero **不展示**保证金、待结算总额、应退/应补等汇总类金额。
### 4.3 分组费用明细
#### 业务管理组
- 固定费项表格(如 ETC 费用、停车费等)。
- **不含**违章处理违约金、保险上浮(归属安全部)。
- 底部展示本部门应结算总额。
#### 能源服务组
- 氢量差补缴、交/还车氢量、退还单价、预付款退费等。
#### 运维组
- 费项:清洗、保养、维修、车损、工具/证件/广告丢失、送接车服务、轮胎磨损等。
- 表格列:**序号 / 费用项 / 金额 / 无忧包减免**(横向滚动,减免为输入框样式只读展示)。
- **应结算 = 金额 无忧包减免**(按行合计后汇总)。
#### 安全部
- **固定费项三行**(由安全部填报):
1. 违章处理违约金
2. 保险上浮
3. 其他违规费用
- 保留违章统计区:违章次数、已缴/未缴金额等。
- 应结算总额为上述三行费用合计。
#### 部门结算卡片状态标签
各分组费用卡片右上角展示本部门子流程提交状态:
| 状态 | 展示文案 | 触发条件 |
| --- | --- | --- |
| 待提交 | 待提交 | 表单未提交、子流程未发起 |
| 审批中 | 审批中 | 用户已提交子流程审批 |
| 已提交 | 已提交 | 该部门子流程全部审批完成 |
| 已驳回 | 已驳回 | 子流程审批被驳回 |
**主流程详情**:四张分组卡片右上角均展示「已提交」(四部门已全部完成子流程审批,且业务管理组已发起主流程后才可进入主流程详情)。
### 4.4 审批操作
- **审批情况**:纵向时间轴,节点展示审批人头像、状态 Tag、时间与评论气泡。
- **待办态底部**:评论、终止、驳回、通过(规则同提车应收款)。
- **查看态**:隐藏操作栏,仅浏览。
---
## 五、提车应收款审批
### 5.1 页面目标
审批人核对实收金额、车辆明细、氢费预付款(如有)及开票信息。
### 5.2 布局细节
- **车辆明细**:区块标题行(`.tc-section-head`)与首张车辆卡片之间保留 **12px** 上间距,避免标题分隔线与首卡紧贴。
- **审批情况**:时间轴支持评论展示;待办态底部操作同还车应结款。
### 5.3 实收金额口径
| 位置 | 规则 |
| --- | --- |
| 列表卡片 | 选中车辆 **租金 + 服务费 + 保证金 减免** 合计(**不含氢费预付款**),由车辆明细动态汇总 |
| 详情 Hero | 与列表卡片**同一口径**,展示实收款总额 |
> **原型缺口**:当前列表已按 §3.3 动态计算,部分 mock 详情 Hero 仍使用静态 `actualAmount` 字段,评审时需对齐(见 `.spec/prototype-review.md`)。
---
## 六、验收要点
请在预览中逐项确认(可提供截图或页面路径反馈):
- [ ] 打开原型无需登录,默认进入工作台
- [ ] 左侧可切换 10 个功能页
- [ ] 导航栏无「需求说明」按钮PRD 可从标注目录打开
- [ ] 审批中心「还车应结款」Chip 可筛出主流程 + 四部门子流程
- [ ] **更多筛选**抽屉在手机预览框内弹出(非全浏览器宽)
- [ ] **更多筛选**:发起人、发起时间为滚轮选择器(非原生输入框)
- [ ] **搜索**仅匹配卡片加粗标题(工作流标题)
- [ ] 子流程列表卡片主标题为「还车应结款-部门名」;主流程卡片以车牌为主标题并展示应退/应补
- [ ] 四部门子流程详情 Hero 为部门标题 + 车牌重点样式,无汇总金额
- [ ] 主流程还车应结款 Hero 展示应退/应补、结算明细与四部门子流程进度
- [ ] 四部门子流程未全部通过时,主流程不可发起;四部门全过但业务管理组未发起时,主流程不出现在列表
- [ ] 业务管理组无违章违约金、保险上浮;安全部固定三行费项
- [ ] 运维组费用表含「无忧包减免」列,应结算 = 金额 减免
- [ ] 提车应收款车辆明细首卡与标题间距 12px
- [ ] 提车列表实收金额与详情 Hero 口径一致(不含氢费预付款)
- [ ] 审批时间轴展示头像、状态与评论;待办态可操作通过/驳回/终止/评论
**预览地址示例**`http://localhost:51721/prototypes/xll-miniapp#page=audit`
---
## 七、关联文档
| 文档 | 位置 |
| --- | --- |
| 小程序总览 | 标注目录「PRD 00小程序总览」 |
| 原型评审结论 | `src/prototypes/xll-miniapp/.spec/prototype-review.md` |
| 页面标注 | `page-audit``page-audit-return``page-audit-pickup` 等 |

View File

@@ -0,0 +1,163 @@
# 小羚羚小程序 · 交车说明
**文档版本**V1.2
**更新日期**2026-06-29
**适用范围**:运维管理 / 交车模块(`#page=delivery`
**参照**web 交车管理 + Axhub「交车完成」原型 · 默认区域权限:浙江省-嘉兴市
---
## 一、列表页
- **KPI**:全部 / 进行中 / 已完成,点击切换筛选。
- **搜索**:车牌、项目、客户名称。
- **卡片字段**:车牌(未选车显示「车辆待选」)、交车状态 Tag未开始 / 已保存 / 待客户签章 / 待重新签章 / 客户已签章)、项目、客户、交车区域。
- **进行中 Tab**:提供状态筛选 Chip全部状态、未开始、已保存、待客户签章、待重新签章
- **操作**:进行中任务展示「去办理 / 继续办理」按钮。
---
## 二、分步表单(顶部步骤条)
顶部步骤条:**车辆情况** → **交车检查项****拍摄照片**,可点击切换。
### 2.1 车辆情况
Hero 展示客户名称、项目名称与交车区域;含选择车辆、车辆配置、驾驶培训、交车数据、交车位置(独立卡片)。
#### 识别车牌号
拍照 OCR 识别车牌后,根据识别结果匹配车辆并校验交车资格,须同时满足:
- **车辆状态 = 已备车**
- **保险状态 = 正常**
- **证照状态 = 正常**
任一条件不满足则报错阻断,不可选车。报错提示文案与「选择车辆」页车辆卡片底部 readiness 提示条一致,例如:
- 「交强险已到期无法交车」
- 「商业险已到期无法交车」
- 「交强险、商业险已到期无法交车」
- 「行驶证已到期无法交车」
- 未备车时「该车辆未备车,请先进行备车」
支持**拍照识别**或**相册选取**车牌照片。
#### 相册上传
交车模块内**所有拍照环节**均支持从手机相册本地上传图片,包括:识别车牌号、车辆配置(车身广告/放大字)、驾驶培训手动记录证件照、交车连拍/补拍、瑕疵与其他照片添加、备胎拍摄等;相机页底部提供「拍照」「相册」操作。
#### 车辆配置
- 车身广告及放大字、尾板(必填,有则须上传照片)
- 备胎(选填,仅选择有/无)
#### 驾驶培训(必填)
支持**电子培训** / **手动记录**两种模式。
**电子培训**:展示培训二维码(微信扫码观看视频),完成后识别提车码。
**手动记录(运维端)**
1. 录入司机手机号、姓名、身份证号
2. 上传身份证正反面、驾驶证正反面、司机正面照片、从业资格证(重卡必填)
3. 点击底部「生成培训码」
4. 培训码与录入信息绑定,展示二维码供司机微信扫码
5. 生成后交车页标记「待司机确认」,可「查看培训码」或「刷新培训状态」
6. 司机签字完成并同步后标记「已完成驾驶培训」
**手动记录 · 司机微信端**(仅需求说明,本期原型不实现):
司机微信扫描二维码后,首先阅读并同意安全培训文件(倒计时 10 秒后可点确定);随后在司机微信浏览器展示运维登记的司机「姓名」「手机号」「身份证号」及身份证正反面、驾驶证正反面、司机正面照片、从业资格证照片;下方点击「确认签字」唤起签名板,完成签字后提示培训成功,相关信息同步至安全培训记录。
#### 交车数据(必填)
- 里程、氢量、电量均支持 2 位小数输入(无数字步进器)
- 氢量单位按「型号参数」仪表盘单位随车型自动匹配(%/MPa不可手动切换
- 送车服务费为选填,精确至 2 位小数(元)
- 氢量下方备注为选填
- **不含**交车时间字段
#### 交车位置
- 车辆 GPS 在线:交车位置取车辆 GPS 当前定位
- 车辆 GPS 离线:交车位置取手机当前定位
- 独立卡片展示地图、蓝点标记坐标GPS 离线时支持手动「获取当前定位」
### 2.2 交车检查项
证件信息 → 工具信息 → 外观检查 → 轮胎检查。
- 前 3 类:开关表示检查情况并可填备注
- 轮胎检查项:填写胎纹深度 (mm)
**实际交车时间**:运维提交 E签宝签章时系统自动记录仅在交车列表「待客户签章」卡片展示。
### 2.3 拍摄照片
- 分车身 / 底盘 / 轮胎 / 瑕疵 / 其他五类
- 连拍严格按顺序拍摄且不可跳过;中途退出后再次进入自动从下一项续拍
- 模拟相机页支持**拍照或相册上传**,轮胎项下方录入胎纹;完成后可补拍瑕疵/其他
- **所有拍照场景均支持调焦**:点击取景区域设定对焦点,右侧 +/- 调整 1.0×3.0× 数码变焦(连拍、备胎、车身广告/放大字、手动记录证件照)
**备胎照片**:从车辆情况步骤移至拍摄照片「轮胎情况」连拍末项(有备胎时必拍);车辆情况仅保留是否有备胎。
**分步操作条**:车辆情况、交车检查项点击「下一步」时自动保存;拍摄照片完成后为「保存」「发送签章文件」。
**照片上传**:连拍/单拍每次点击「完成」继续时,上一张自动上传并在服务端打水印(时间、地点)。
**胎纹 OCR**:胎纹未反写成功时禁止继续拍摄下一张。
### 2.4 签章流程
**选择被授权人**:多卡片展示姓名与手机号,选中高亮;发送签章文件后跳转 E签宝 完成运维签字。
| 状态 | 规则 |
| --- | --- |
| **待客户签章** | 只读查看各步骤,底部「返回」「清除签章」;确认后记录变为「待重新签章」 |
| **待重新签章** | 交车信息全部锁定,仅可重选被授权人;底部「返回」「重新发起签章」,完成 E签宝 运维签字后恢复「待客户签章」 |
| **客户已签章** | 被授权人短信完成签章后进入「已完成」列表 |
---
## 三、查看态
- **客户已签章 / 待客户签章**:只读浏览各步骤,不可编辑。
- **待重新签章**:各步骤只读,仅「选择被授权人」可改。
- **已完成**:可下载 E签宝签章 PDF展示是否归还。
---
## 四、导航
- 表单内左上返回先退出办理页;再次返回退出交车模块。
- 步骤条可点击跳转。
---
## 五、验收要点
请在预览中逐项确认(可提供截图或页面路径反馈):
- [ ] 业务页进入交车模块,列表 KPI / 搜索 / 状态筛选可用
- [ ] 进行中任务可「去办理 / 继续办理」,列表与表单同页切换
- [ ] OCR / 相册识别车牌,未备车或保险/证照异常时阻断选车
- [ ] 车辆配置、驾驶培训、交车数据、交车位置各卡片可填写
- [ ] 交车检查项四类可填,轮胎胎纹支持 mm 录入
- [ ] 拍摄照片五类连拍、相册上传、调焦与备胎末项规则
- [ ] 发送签章 → 待客户签章 → 清除签章 / 重新签章 / 客户已签章状态流转
- [ ] 标注目录可打开本 PRD页面标注 `page-delivery` 可定位交车模块
**预览地址示例**`http://localhost:51722/prototypes/xll-miniapp#page=delivery`
---
## 六、关联文档
| 文档 | 位置 |
| --- | --- |
| 小程序总览 | 标注目录「PRD 00小程序总览」 |
| 审批中心改版 | 标注目录「PRD 01审批中心改版说明」 |
| 页面标注 | `page-delivery` |
| 原型评审 | `src/prototypes/xll-miniapp/.spec/prototype-review.md` |

View File

@@ -0,0 +1,49 @@
# 8returns 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
8returns 的 Design.md 主题展示,围绕运营系统、运营监控、数据仪表盘、浅色界面组织页面。
Explore 8returns's light E-commerce design system: Midnight Ink #051923, Lagoon Teal #004853 colors, roobert typography, and DESIGN.md for AI agents.
## Tags
- 运营系统
- 运营监控
- 数据仪表盘
- 浅色界面
- 电商零售
- AI
- 消费品牌
- 品牌展示
- 移动端
- 内容货架
- 趣味互动
## Colors
- color-1: `#051923`
- color-2: `#004853`
- color-3: `#ffffff`
- color-4: `#000000`
- color-5: `#eef0f2`
- color-6: `#1e3039`
- color-7: `#bfbfbf`
- color-8: `#c8d4e0`
- color-9: `#cfff69`
- color-10: `#6289ff`
- color-11: `#525252`
- color-12: `#e0e0e0`
## Typography
- display: Inter
- body: Inter
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/d5799c71-f9ae-4bad-aef3-4c07bac42c08

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,26 @@
{
"palette": [
"#051923",
"#004853",
"#ffffff",
"#000000",
"#eef0f2",
"#1e3039",
"#bfbfbf",
"#c8d4e0",
"#cfff69",
"#6289ff",
"#525252",
"#e0e0e0"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
},
"sourceCategory": "E-commerce"
}

View File

@@ -0,0 +1,41 @@
/**
* @name 8returns 主题 - 8returns
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for 8returns. */
.dmb-page {
--dmb-accent: #051923;
--dmb-accent-contrast: #ffffff;
--dmb-link: #004853;
--dmb-muted: #004853;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,263 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/d5799c71-f9ae-4bad-aef3-4c07bac42c08",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-010",
"slug": "8returns",
"brand": "8returns",
"titleZh": "8returns 主题",
"titleEn": "8returns",
"descriptionZh": "8returns 的 Design.md 主题展示,围绕运营系统、运营监控、数据仪表盘、浅色界面组织页面。",
"descriptionEn": "Explore 8returns's light E-commerce design system: Midnight Ink #051923, Lagoon Teal #004853 colors, roobert typography, and DESIGN.md for AI agents."
},
"tags": {
"sourceTags": [
"8returns-design-system",
"8returns-website-design",
"8returns-colors",
"8returns-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"e-commerce-design-system",
"midnight-ink",
"lagoon-teal",
"spring-green",
"vivid-violet",
"roobert"
],
"generatedTags": [
"operations",
"image-assets",
"monitoring",
"dashboard",
"light",
"e-commerce",
"ai"
],
"designTags": [
"Midnight Ink",
"Spring Green",
"Vivid Violet",
"light"
],
"distributionTags": [
"运营系统",
"运营监控",
"数据仪表盘",
"浅色界面",
"电商零售",
"AI",
"消费品牌",
"品牌展示",
"移动端",
"内容货架",
"趣味互动"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/8returns/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/ebc76f50-1aea-42ce-bc3c-8d2a25d79038.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#051923",
"#004853",
"#ffffff",
"#000000",
"#eef0f2",
"#1e3039",
"#bfbfbf",
"#c8d4e0",
"#cfff69",
"#6289ff",
"#525252",
"#e0e0e0"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "8returns 主题",
"brandAlias": "8returns",
"description": "8returns 的 Design.md 主题展示,围绕运营系统、运营监控、数据仪表盘、浅色界面组织页面。",
"descriptionEn": "Explore 8returns's light E-commerce design system: Midnight Ink #051923, Lagoon Teal #004853 colors, roobert typography, and DESIGN.md for AI agents.",
"variant": "consumer-commerce",
"distributionTags": [
"运营系统",
"运营监控",
"数据仪表盘",
"浅色界面",
"电商零售",
"AI",
"消费品牌",
"品牌展示",
"移动端",
"内容货架",
"趣味互动"
],
"fontStylesheets": [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
],
"palette": [
{
"color": "#051923",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#004853",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#ffffff",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#000000",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#ffffff"
},
{
"color": "#eef0f2",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#1e3039",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#ffffff"
},
{
"color": "#bfbfbf",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#171717"
},
{
"color": "#c8d4e0",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#171717"
},
{
"color": "#cfff69",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#171717"
},
{
"color": "#6289ff",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#525252",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#ffffff"
},
{
"color": "#e0e0e0",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#171717"
}
],
"shadows": [],
"borders": [],
"typography": [
"Inter",
"ui-monospace"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "品牌",
"title": "第一眼识别明确",
"body": "适合消费品牌、电商、媒体和产品展示页。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议:基于 Refero 采集图和 metadata 还原 8returns 的颜色、字体和页面密度,优先服务 运营系统、运营监控、数据仪表盘 场景。",
"建议:保留 8returns 的来源标签和视觉 token不把它混入旧来源的泛化模板。"
],
"dont": [
"避免:不要把 8returns 套用到与 运营系统、运营监控、数据仪表盘 明显无关的产品语境。",
"避免:不要用通用行业模板覆盖 8returns 的采集图风格;需要保留原图里的版式密度、留白和视觉重心。"
]
}
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for 8returns. */

View File

@@ -0,0 +1,50 @@
# Aevi Wellness 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
Aevi Wellness 的 Design.md 主题展示,围绕健康生活、消费品牌、医疗健康、浅色界面组织页面。
Explore Aevi Wellness's light E-commerce design system: Canvas White #ffffff, Charcoal Text #231f20 colors, Primary Font, Custom Font 1 typography, and...
## Tags
- 健康生活
- 消费品牌
- 医疗健康
- 浅色界面
- 电商零售
- AI
- 数据仪表盘
- 数据分析
- B2B 产品
- 运营监控
- 专业工具
- 高密度信息
## Colors
- color-1: `#231f20`
- color-2: `#000000`
- color-3: `#ffffff`
- color-4: `#d5e0ea`
- color-5: `#a3bfdb`
- color-6: `#134fc2`
- color-7: `#006fcf`
- color-8: `#ffa8cd`
- color-9: `#5a31f4`
- color-10: `#fff48d`
- color-11: `#005b9a`
- color-12: `#0e73b9`
## Typography
- display: Primary Font
- body: Custom Font 1
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/c2fadeda-8ef3-4ab2-9909-ffdf02ab0a24

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,28 @@
{
"palette": [
"#231f20",
"#000000",
"#ffffff",
"#d5e0ea",
"#a3bfdb",
"#134fc2",
"#006fcf",
"#ffa8cd",
"#5a31f4",
"#fff48d",
"#005b9a",
"#0e73b9"
],
"typography": {
"display": "Primary Font",
"body": "Custom Font 1",
"mono": "ui-monospace",
"hints": [
"Primary Font",
"Custom Font 1",
"ui-monospace",
"Custom Font 2"
]
},
"sourceCategory": "E-commerce"
}

View File

@@ -0,0 +1,41 @@
/**
* @name Aevi Wellness 主题 - Aevi Wellness
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Aevi Wellness. */
.dmb-page {
--dmb-accent: #231f20;
--dmb-accent-contrast: #ffffff;
--dmb-link: #000000;
--dmb-muted: #000000;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Primary Font", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Custom Font 1", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,270 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/c2fadeda-8ef3-4ab2-9909-ffdf02ab0a24",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-002",
"slug": "aevi-wellness",
"brand": "Aevi Wellness",
"titleZh": "Aevi Wellness 主题",
"titleEn": "Aevi Wellness",
"descriptionZh": "Aevi Wellness 的 Design.md 主题展示,围绕健康生活、消费品牌、医疗健康、浅色界面组织页面。",
"descriptionEn": "Explore Aevi Wellness's light E-commerce design system: Canvas White #ffffff, Charcoal Text #231f20 colors, Primary Font, Custom Font 1 typography, and..."
},
"tags": {
"sourceTags": [
"aevi-wellness-design-system",
"aevi-wellness-website-design",
"aevi-wellness-colors",
"aevi-wellness-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"e-commerce-design-system",
"canvas-white",
"charcoal-text",
"sky-mist",
"midnight-ink",
"primary-font",
"custom-font-1",
"custom-font-2"
],
"generatedTags": [
"wellness",
"consumer",
"healthcare",
"image-assets",
"light",
"e-commerce",
"ai"
],
"designTags": [
"Canvas White",
"Charcoal Text",
"Sky Mist",
"Midnight Ink",
"light"
],
"distributionTags": [
"健康生活",
"消费品牌",
"医疗健康",
"浅色界面",
"电商零售",
"AI",
"数据仪表盘",
"数据分析",
"B2B 产品",
"运营监控",
"专业工具",
"高密度信息"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/aevi-wellness/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/c6bff352-2934-4704-a607-6fabadafe463.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#231f20",
"#000000",
"#ffffff",
"#d5e0ea",
"#a3bfdb",
"#134fc2",
"#006fcf",
"#ffa8cd",
"#5a31f4",
"#fff48d",
"#005b9a",
"#0e73b9"
],
"typography": {
"display": "Primary Font",
"body": "Custom Font 1",
"mono": "ui-monospace",
"hints": [
"Primary Font",
"Custom Font 1",
"ui-monospace",
"Custom Font 2"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "Aevi Wellness 主题",
"brandAlias": "Aevi Wellness",
"description": "Aevi Wellness 的 Design.md 主题展示,围绕健康生活、消费品牌、医疗健康、浅色界面组织页面。",
"descriptionEn": "Explore Aevi Wellness's light E-commerce design system: Canvas White #ffffff, Charcoal Text #231f20 colors, Primary Font, Custom Font 1 typography, and...",
"variant": "dashboard",
"distributionTags": [
"健康生活",
"消费品牌",
"医疗健康",
"浅色界面",
"电商零售",
"AI",
"数据仪表盘",
"数据分析",
"B2B 产品",
"运营监控",
"专业工具",
"高密度信息"
],
"fontStylesheets": [],
"palette": [
{
"color": "#231f20",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#000000",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#ffffff",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#d5e0ea",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#171717"
},
{
"color": "#a3bfdb",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#134fc2",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#ffffff"
},
{
"color": "#006fcf",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#ffffff"
},
{
"color": "#ffa8cd",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#171717"
},
{
"color": "#5a31f4",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#ffffff"
},
{
"color": "#fff48d",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#171717"
},
{
"color": "#005b9a",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#ffffff"
},
{
"color": "#0e73b9",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#ffffff"
}
],
"shadows": [],
"borders": [],
"typography": [
"Primary Font",
"Custom Font 1",
"ui-monospace",
"Custom Font 2"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "数据",
"title": "指标与状态清晰",
"body": "适合分析看板、金融科技、监控和运营页面。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议:基于 Refero 采集图和 metadata 还原 Aevi Wellness 的颜色、字体和页面密度,优先服务 健康生活、消费品牌、医疗健康 场景。",
"建议:保留 Aevi Wellness 的来源标签和视觉 token不把它混入旧来源的泛化模板。"
],
"dont": [
"避免:不要把 Aevi Wellness 套用到与 健康生活、消费品牌、医疗健康 明显无关的产品语境。",
"避免:不要用通用行业模板覆盖 Aevi Wellness 的采集图风格;需要保留原图里的版式密度、留白和视觉重心。"
]
}
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Aevi Wellness. */

545
src/themes/airbnb/DESIGN.md Normal file
View File

@@ -0,0 +1,545 @@
---
version: alpha
name: Airbnb-design-analysis
description: A warm, generous consumer marketplace anchored on a clean white canvas and Airbnb Rausch (#ff385c), the single brand voltage that carries every primary CTA, search-button orb, and rating dot. Type runs Airbnb Cereal VF at modest weights — display sits at 2228px in weight 500/600 rather than the heavy 700+ that fintech and enterprise systems use; the brand trusts photography and generous whitespace over typographic muscle. Three product entries (Homes, Experiences, Services) sit in the top nav with hand-illustrated 32-icon glyphs and "NEW" badges, signaling a marketplace expansion rather than a feature dump. Pill-shaped search bars (`{rounded.full}`), softly rounded property cards (`{rounded.lg}` ~14px), and 32px button radii read as friendly and human — there is no hard corner anywhere except the body grid.
colors:
primary: "#ff385c"
primary-active: "#e00b41"
primary-disabled: "#ffd1da"
primary-error-text: "#c13515"
primary-error-text-hover: "#b32505"
luxe: "#460479"
plus: "#92174d"
ink: "#222222"
body: "#3f3f3f"
muted: "#6a6a6a"
muted-soft: "#929292"
hairline: "#dddddd"
hairline-soft: "#ebebeb"
border-strong: "#c1c1c1"
canvas: "#ffffff"
surface-soft: "#f7f7f7"
surface-card: "#ffffff"
surface-strong: "#f2f2f2"
on-primary: "#ffffff"
on-dark: "#ffffff"
legal-link: "#428bff"
star-rating: "#222222"
scrim: "#000000"
typography:
display-xl:
fontFamily: "'Airbnb Cereal VF', Circular, -apple-system, system-ui, Roboto, 'Helvetica Neue', sans-serif"
fontSize: 28px
fontWeight: 700
lineHeight: 1.43
letterSpacing: 0
display-lg:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 22px
fontWeight: 500
lineHeight: 1.18
letterSpacing: -0.44px
display-md:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 21px
fontWeight: 700
lineHeight: 1.43
letterSpacing: 0
display-sm:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 20px
fontWeight: 600
lineHeight: 1.20
letterSpacing: -0.18px
title-md:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 16px
fontWeight: 600
lineHeight: 1.25
letterSpacing: 0
title-sm:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 16px
fontWeight: 500
lineHeight: 1.25
letterSpacing: 0
rating-display:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 64px
fontWeight: 700
lineHeight: 1.1
letterSpacing: -1px
body-md:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 16px
fontWeight: 400
lineHeight: 1.5
letterSpacing: 0
body-sm:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.43
letterSpacing: 0
caption:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 14px
fontWeight: 500
lineHeight: 1.29
letterSpacing: 0
caption-sm:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 13px
fontWeight: 400
lineHeight: 1.23
letterSpacing: 0
badge:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 11px
fontWeight: 600
lineHeight: 1.18
letterSpacing: 0
micro-label:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 12px
fontWeight: 700
lineHeight: 1.33
letterSpacing: 0
uppercase-tag:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 8px
fontWeight: 700
lineHeight: 1.25
letterSpacing: 0.32px
textTransform: uppercase
button-md:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 16px
fontWeight: 500
lineHeight: 1.25
letterSpacing: 0
button-sm:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 14px
fontWeight: 500
lineHeight: 1.29
letterSpacing: 0
link:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.43
letterSpacing: 0
nav-link:
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
fontSize: 16px
fontWeight: 600
lineHeight: 1.25
letterSpacing: 0
rounded:
none: 0px
xs: 4px
sm: 8px
md: 14px
lg: 20px
xl: 32px
full: 9999px
spacing:
xxs: 2px
xs: 4px
sm: 8px
md: 12px
base: 16px
lg: 24px
xl: 32px
xxl: 48px
section: 64px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button-md}"
rounded: "{rounded.sm}"
padding: 14px 24px
height: 48px
button-primary-active:
backgroundColor: "{colors.primary-active}"
textColor: "{colors.on-primary}"
rounded: "{rounded.sm}"
button-primary-disabled:
backgroundColor: "{colors.primary-disabled}"
textColor: "{colors.on-primary}"
rounded: "{rounded.sm}"
button-secondary:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button-md}"
rounded: "{rounded.sm}"
padding: 13px 23px
height: 48px
button-tertiary-text:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.button-md}"
button-pill-rausch:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button-sm}"
rounded: "{rounded.full}"
padding: 10px 20px
search-orb:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
rounded: "{rounded.full}"
height: 48px
icon-button-circle:
backgroundColor: "{colors.surface-strong}"
textColor: "{colors.ink}"
rounded: "{rounded.full}"
height: 32px
icon-button-outline:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
rounded: "{rounded.full}"
height: 40px
top-nav:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.nav-link}"
height: 80px
product-tab-active:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.nav-link}"
rounded: "{rounded.none}"
product-tab-inactive:
backgroundColor: transparent
textColor: "{colors.muted}"
typography: "{typography.nav-link}"
search-bar-pill:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.full}"
padding: 14px 24px
height: 64px
search-field-segment:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.caption}"
padding: 8px 24px
category-strip:
backgroundColor: "{colors.canvas}"
textColor: "{colors.muted}"
typography: "{typography.button-sm}"
category-tab-active:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.button-sm}"
rounded: "{rounded.none}"
property-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.md}"
property-card-photo:
rounded: "{rounded.md}"
experience-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.title-md}"
rounded: "{rounded.md}"
city-link-block:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.title-sm}"
rating-display-card:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.rating-display}"
guest-favorite-badge:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.badge}"
rounded: "{rounded.full}"
padding: 4px 10px
new-tag:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.uppercase-tag}"
rounded: "{rounded.full}"
padding: 2px 6px
amenity-row:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.body-md}"
padding: 12px 0
reviews-card:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
host-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.md}"
padding: 24px
reservation-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.md}"
padding: 24px
date-picker-day:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.full}"
date-picker-day-selected:
backgroundColor: "{colors.ink}"
textColor: "{colors.on-dark}"
rounded: "{rounded.full}"
text-input:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.sm}"
padding: 14px 12px
height: 56px
footer-light:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
padding: 48px 80px
footer-link:
backgroundColor: transparent
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
legal-band:
backgroundColor: "{colors.canvas}"
textColor: "{colors.muted}"
typography: "{typography.caption-sm}"
---
## Overview
Airbnb is the canonical example of a generous, photography-led consumer marketplace. The base canvas is **pure white** (`{colors.canvas}`#ffffff) with deep near-black ink (`{colors.ink}`#222222) for headlines and body, and a single voltage of **Rausch** (`{colors.primary}`#ff385c) carrying every primary CTA, the search-button orb, the heart save state, and inline brand links. There is no secondary brand color in mainline marketing — the **Luxe purple** (`{colors.luxe}`#460479) and **Plus magenta** (`{colors.plus}`#92174d) tokens are sub-brand accents that only appear inside Airbnb Luxe / Plus contexts.
Type runs **Airbnb Cereal VF** (a custom variable font Airbnb licenses), with **Circular** as the historic in-house fallback and a system stack underneath. Cereal sits at modest weights — display headlines render at 2228px in weight 500600, not the heavy 700+ weights that financial or enterprise systems lean on. The hero h1 ("Inspiration for future getaways") on the homepage is just 28px / 700, which would feel small on a typical SaaS page; here it works because the layout leans on photography (city collage, property cards) for visual weight rather than typographic muscle.
The shape language is **soft**. Buttons are 8px radius (`{rounded.sm}`), property cards are ~14px (`{rounded.md}`), the search bar is fully pill-shaped (`{rounded.full}`), wishlist hearts and search orbs are circles (`{rounded.full}`), and category strip rounded corners run at 32px (`{rounded.xl}`). There is essentially no hard corner anywhere except the body grid itself — every interactive element is rounded.
**Key Characteristics:**
- Single accent color: `{colors.primary}` (#ff385c — "Rausch") carries every primary CTA, the search orb, the heart save state, and the brand wordmark. Used scarcely — most pages are 90% white + ink with one or two Rausch moments.
- Custom variable type: `Airbnb Cereal VF`. Display weights sit at 500700, body at 400. Modest weight is intentional — the system trusts photography for visual heft.
- Three-product top nav: Homes, Experiences, Services — each with a hand-illustrated 32px icon and "NEW" badges (`{component.new-tag}`) on the two newer products. Active tab uses an underline rule (`{component.product-tab-active}`).
- Pill-shaped global search bar: white surface, fully rounded (`{rounded.full}`), divided by 1px hairlines into Where / When / Who segments, terminated by a circular Rausch search orb (`{component.search-orb}`).
- Property cards are photo-first: aspect-ratio rectangles with `{rounded.md}` corner clipping, swipeable image carousel, "Guest favorite" floating badge top-left, heart icon top-right, then 45 lines of meta beneath.
- Editorial dropdowns (footer, language picker) are clean text columns over the white canvas — no card surface, no shadow.
- The design system caps elevation at one shadow tier (`box-shadow: rgba(0,0,0,0.02) 0 0 0 1px, rgba(0,0,0,0.04) 0 2px 6px, rgba(0,0,0,0.1) 0 4px 8px`) — used on hover-floated cards and search/account dropdowns.
- 8px base spacing system, with major sections at `{spacing.section}` (64px) — generous but not airy enough to feel editorial-magazine; the marketplace density wants more cards per scroll.
## Colors
### Brand & Accent
- **Rausch** (`{colors.primary}`#ff385c): The single brand color. Used for primary CTA backgrounds (Reserve, Continue), the search orb, the heart save state on property cards, and inline brand links. The most recognizable color in consumer travel.
- **Rausch Active** (`{colors.primary-active}`#e00b41): The press / pointer-down variant — slightly more saturated. Used on `{component.button-primary-active}`.
- **Rausch Disabled** (`{colors.primary-disabled}`#ffd1da): A pale tint used on disabled CTAs.
- **Luxe Purple** (`{colors.luxe}`#460479): Sub-brand accent for Airbnb Luxe. Only appears inside Luxe-branded surfaces — never in mainline marketing.
- **Plus Magenta** (`{colors.plus}`#92174d): Sub-brand accent for Airbnb Plus. Same scoping as Luxe — sub-product only.
### Surface
- **Canvas** (`{colors.canvas}`#ffffff): The default page floor for every public page. Airbnb does not have a dark mode on the public web.
- **Surface Soft** (`{colors.surface-soft}`#f7f7f7): The lightest fill — used on disabled fields, sub-nav hover backgrounds, and the inline search filter band.
- **Surface Strong** (`{colors.surface-strong}`#f2f2f2): Slightly heavier fill — circular icon-button surface (e.g., the breadcrumb back-arrow and listing toolbar buttons).
### Hairlines & Borders
- **Hairline** (`{colors.hairline}`#dddddd): The default 1px border tone — search bar dividers, table separators, footer column splitters, card 1px borders.
- **Hairline Soft** (`{colors.hairline-soft}`#ebebeb): A lighter divider used on long-scrolling editorial body separators.
- **Border Strong** (`{colors.border-strong}`#c1c1c1): A heavier stroke used on disabled outline buttons and form input outlines after focus.
### Text
- **Ink** (`{colors.ink}`#222222): The dominant text color on light surfaces. Display headlines, body paragraphs, primary nav links, and most inline link text. Never pure black.
- **Body** (`{colors.body}`#3f3f3f): A secondary running-text color used inside long-form review and amenity copy where ink would feel too heavy.
- **Muted** (`{colors.muted}`#6a6a6a): Sub-titles inside city link blocks ("Cottage rentals", "Villa rentals"), inactive product-tab labels, footer category sub-labels, "View all" links.
- **Muted Soft** (`{colors.muted-soft}`#929292): Disabled link text. Used very sparingly.
- **Star Rating** (`{colors.star-rating}`#222222): The same ink token — Airbnb's star icon and "4.81" rating numbers all render in ink rather than a yellow/gold color, which is a deliberate brand choice (yellow stars feel cheap in travel context).
- **On Primary** (`{colors.on-primary}`#ffffff): White text on Rausch CTAs.
### Semantic
- **Error** (`{colors.primary-error-text}`#c13515): Inline error text for form validation. Distinct from Rausch — slightly darker, more saturated red.
- **Error Hover** (`{colors.primary-error-text-hover}`#b32505): Darkens on link hover.
- **Legal Link Blue** (`{colors.legal-link}`#428bff): Inline links inside legal copy (Privacy, Terms). Only used inside the legal sub-band.
### Scrim
- **Scrim** (`{colors.scrim}`#000000 at 50% opacity): The global modal backdrop tone — date picker, login dialog, language picker. Stored as the base hex; opacity is applied at render time.
## Typography
### Font Family
The system runs **Airbnb Cereal VF** for everything — display, body, navigation, captions, microcopy. Fallbacks walk `Circular, -apple-system, system-ui, Roboto, "Helvetica Neue", sans-serif`. **Circular** is the historic in-house typeface still kept as the first non-variable fallback; system stacks back it up.
There is no separate display family. The variable font carries the entire scale.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.rating-display}` | 64px | 700 | 1.1 | -1px | Listing detail rating display ("4.81") |
| `{typography.display-xl}` | 28px | 700 | 1.43 | 0 | Homepage h1 ("Inspiration for future getaways") |
| `{typography.display-lg}` | 22px | 500 | 1.18 | -0.44px | Listing detail h1 ("Close to Fethiye Aliyah Bali Beach…") |
| `{typography.display-md}` | 21px | 700 | 1.43 | 0 | Section heads inside listing detail ("What this place offers") |
| `{typography.display-sm}` | 20px | 600 | 1.20 | -0.18px | Sub-section titles ("Things to know") |
| `{typography.title-md}` | 16px | 600 | 1.25 | 0 | City link block titles ("Wilmington", "Athens") |
| `{typography.title-sm}` | 16px | 500 | 1.25 | 0 | Footer column heads ("Support", "Hosting", "Airbnb") |
| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default running-text inside listing copy |
| `{typography.body-sm}` | 14px | 400 | 1.43 | 0 | Card meta lines, dates, prices, distance text |
| `{typography.caption}` | 14px | 500 | 1.29 | 0 | Search field segment labels ("Where", "When", "Who") |
| `{typography.caption-sm}` | 13px | 400 | 1.23 | 0 | Footer legal line ("© 2026 Airbnb, Inc.") |
| `{typography.badge}` | 11px | 600 | 1.18 | 0 | "Guest favorite" floating badge text |
| `{typography.micro-label}` | 12px | 700 | 1.33 | 0 | Card amenity micro-labels ("Inline 6") |
| `{typography.uppercase-tag}` | 8px | 700 | 1.25 | 0.32px (uppercase) | "NEW" badge on product nav tabs |
| `{typography.button-md}` | 16px | 500 | 1.25 | 0 | Primary CTA button labels |
| `{typography.button-sm}` | 14px | 500 | 1.29 | 0 | Pill button labels (category strip) |
| `{typography.link}` | 14px | 400 | 1.43 | 0 | Inline body links |
| `{typography.nav-link}` | 16px | 600 | 1.25 | 0 | Top product-nav labels (Homes, Experiences, Services) |
### Principles
Display weights stay modest. The homepage h1 at 28px / 700 is deliberately small — it tucks under the search bar so photography and the city-link grid carry visual hierarchy. The listing-detail h1 at 22px / 500 is even quieter; the listing photo banner does the work above it.
The single typographically loud moment in the entire system is the **rating display** (`{typography.rating-display}` — 64px / 700) on listing pages. That is the only place the system trusts type alone to carry hierarchy — rating numbers are a peak trust signal, so they get the loudest treatment.
### Note on Font Substitutes
If Airbnb Cereal VF and Circular are unavailable, **Inter** is the closest open-source substitute. Adjust display headlines down by ~2% in line-height to match Cereal's slightly tighter cap height; otherwise the proportions transfer cleanly.
## Layout
### Spacing System
- **Base unit:** 4px (with 2px micro-step).
- **Tokens:** `{spacing.xxs}` 2px · `{spacing.xs}` 4px · `{spacing.sm}` 8px · `{spacing.md}` 12px · `{spacing.base}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 64px.
- **Section padding (vertical):** `{spacing.section}` (64px) for major page bands; tighter than typical SaaS marketing (8096px) because marketplace pages need higher card density per scroll.
- **Card internal padding:** `{spacing.lg}` (24px) for `{component.host-card}` and `{component.reservation-card}`; `{spacing.base}` (16px) for property-card meta block; `{spacing.sm}` (8px) for caption / date-row gutters.
- **Gutters:** `{spacing.base}` (16px) between cards in the homepage city grid; `{spacing.lg}` (24px) inside footer column gutters; `{spacing.xs}` (4px) on dense category-strip dividers.
### Grid & Container
- **Max content width:** ~1280px centered on the homepage and editorial pages. Listing detail pages cap closer to 1080px to keep the photo banner and reservation rail readable.
- **City link grid (homepage footer):** 6-column grid at desktop with each cell housing a city name in `{typography.title-md}` and a category sub-label in `{typography.body-sm}` muted.
- **Listing detail:** 2-column with photo / amenity body on the left (~64% width) and a sticky reservation card (`{component.reservation-card}`) on the right (~32%).
- **Footer:** 3-column link list (Support / Hosting / Airbnb) at desktop, collapsing to 1-column on mobile.
### Whitespace Philosophy
The system gives editorial bands 64px of vertical breathing room but compresses card grids — property and city-link cards sit just 16px apart. The contrast is intentional: the page reads as "open hero, dense marketplace below," reinforcing the marketplace nature without overwhelming the visitor at the fold.
## Elevation
The system has essentially **one shadow tier** plus the flat baseline.
- **Flat (no shadow):** Body, hero, footer, all editorial bands — 95% of surfaces.
- **Card hover float:** `box-shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0` — applied to property cards on pointer hover, the search bar at rest, and the dropdown menus (account menu, language picker, date picker). This is the single shadow definition in the entire system.
- **Modal scrim:** `{colors.scrim}` rendered at 50% opacity — the global modal backdrop. Used on date pickers, login dialogs, language picker.
There are no progressive elevation tiers — the system either has the one shadow or none. Depth comes from photography, the white-on-white surface separation, and rounded-corner clipping rather than from layered shadows.
## Components
### Buttons
**`button-primary`** — Rausch fill, white text, 8px radius, 14×24px padding, 48px height, weight 500. The most common CTA across the system: "Reserve", "Continue", "Search", account-flow primaries.
**`button-primary-active`** — The press state. Background flips to `{colors.primary-active}`. No transform, no shadow change.
**`button-primary-disabled`** — Pale Rausch tint at #ffd1da with white text. Cursor not-allowed.
**`button-secondary`** — White fill with ink text and a 1px ink outline. 8px radius. Used for "Save", "Cancel", and inverse CTAs over Rausch surfaces.
**`button-tertiary-text`** — Plain ink text, no surface, no border. Underlined on hover. Used for "Show more" type links and modal close labels.
**`button-pill-rausch`** — A pill-shaped Rausch CTA used on featured cells (e.g., "Become a host" sub-CTA) — 9999px radius, 10×20px padding, 14px label.
### Search Surface
**`search-bar-pill`** — The signature global search bar. White fill, 9999px radius, 64px height, 1px hairline 1px-shadow border. Internally divided by vertical hairline rules into `{component.search-field-segment}` cells (Where / When / Who). Each segment holds an uppercase caption label above a placeholder line in `{typography.caption}`.
**`search-orb`** — The circular Rausch orb terminating the right edge of the search bar. 48×48px, fully rounded, white magnifying-glass icon centered. The hottest single color moment on the homepage.
### Top Navigation
**`top-nav`** — White surface, 80px height, 1px bottom hairline. The Airbnb wordmark sits flush left, the three product tabs (Homes / Experiences / Services) sit in the dead center, and account utilities (host link, language globe, account menu) sit flush right.
**`product-tab-active`** — Ink label in `{typography.nav-link}`, 32px hand-illustrated icon, 2px ink underline rule beneath the icon-label pair.
**`product-tab-inactive`** — Muted label, illustrated icon, no underline. Becomes active on click.
**`new-tag`** — A tiny rounded-pill badge (`{rounded.full}`) anchored top-right of an icon, carrying the uppercase "NEW" label in `{typography.uppercase-tag}` (8px / 700 with 0.32px tracking, uppercase). Used on Experiences and Services to signal recency.
### Listing Cards
**`property-card`** — A photo-first card. 1:1 aspect-ratio image with `{rounded.md}` corner clipping, image carousel dots overlay, "Guest favorite" floating badge top-left (`{component.guest-favorite-badge}`), and a heart icon top-right (`{component.icon-button-circle}` in default outlined state, Rausch-filled when saved). Beneath the image: 45 lines of meta — title (`{typography.title-md}`), distance / dates (`{typography.body-sm}` muted), and price ("$X night") right-aligned.
**`property-card-photo`** — The photo plate itself, separated as a token because some surfaces (wishlist, search results) reuse just the photo without the meta block.
**`experience-card`** — A taller-aspect card (4:5) for experience listings. Same `{rounded.md}` clipping, floating "NEW" badge top-left, heart top-right, and a single-line title beneath.
**`guest-favorite-badge`** — White rounded pill (`{rounded.full}`) at 11px / 600 weight. Sits over the photo with the system's only shadow tier applied for elevation.
### Listing Detail
**`rating-display-card`** — The signature listing-detail moment. A 64px / 700 rating number ("4.81") flanked left and right by tiny laurel-wreath SVG ornaments. Beneath the rating: "Guest favorite" tagline and a row of ink stat columns. The largest typographic weight in the whole system.
**`amenity-row`** — A 1-column list of amenity icons + ink labels in `{typography.body-md}`. 12px row padding, no border between rows; section is closed by a 1px hairline divider above and below.
**`reviews-card`** — A 2-column grid of review excerpts. Each column holds an author row (avatar, name, date) above a 3-line excerpt with "Show more" tertiary link.
**`host-card`** — A white card with `{rounded.md}` rounding and 24px padding holding a host avatar, name, "Superhost" badge, response-rate stat, and a "Contact host" `{component.button-secondary}`.
**`reservation-card`** — The sticky right-rail card on listing detail pages. White surface, `{rounded.md}` rounding, 1px hairline border, 1px shadow tier elevation, 24px padding. Contains: nightly price (`{typography.display-md}` ink), date-range selector, guest-count stepper, "Reserve" primary CTA full-width, and a fee breakdown stack beneath in `{typography.body-sm}`.
### Date Picker
**`date-picker-day`** — A 40×40px circular cell carrying the day number in `{typography.body-sm}`. Default state is transparent fill, ink text.
**`date-picker-day-selected`** — Ink fill, white text, full circle (`{rounded.full}`). Range states between two selected days carry a `{colors.surface-soft}` lozenge background that connects them.
### Forms
**`text-input`** — White surface, 1px hairline outline, `{rounded.sm}` 8px radius, 56px height, 14×12px padding. Stacked label above (in `{typography.caption}` muted), placeholder text in `{typography.body-md}` muted. On focus, the border thickens to 2px ink and the border color flips to `{colors.ink}` — no glow, no ring.
### Footer
**`footer-light`** — White surface (matches the page canvas — Airbnb has no contrast footer), 48×80px padding. Three columns of link blocks (Support / Hosting / Airbnb), separated by generous 24px gutters. Each column heads with a `{typography.title-sm}` ink label and stacks `{component.footer-link}` rows in `{typography.body-sm}` ink.
**`legal-band`** — A bottom strip beneath the footer columns carrying the copyright line, language picker (globe icon + "English (US)" link), currency picker, and social icons (Facebook, X, Instagram). All text in muted `{colors.muted}` at `{typography.caption-sm}`.
## Responsive Behavior
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 744px | Top nav collapses to logo + hamburger; product tabs hide behind a sheet; search bar collapses to a single tappable pill; property cards stack 1-up; city grid 1-column; listing detail collapses reservation card to a sticky bottom bar. |
| Tablet | 7441128px | Top nav keeps product tabs but search bar narrows; property cards 2-up; city grid 23 column; reservation card stays sticky right-rail at narrower width. |
| Desktop | 11281440px | Full top nav with three product tabs centered; search bar at full pill width with all 3 segments visible; property cards 4-up; city grid 6-column; listing detail 2-column with reservation rail. |
| Wide | > 1440px | Content width caps at 1440px on listing/search pages and ~1280px on editorial; gutters absorb the rest. |
### Touch Targets
- Primary CTAs at minimum 48×48px (above WCAG AAA).
- Search orb is 48×48px circular — the most-tapped element on the page.
- Heart save button is 32×32px circular — borderline for AAA but compensated by a generous 12px padding inside the photo card.
- Date-picker day cells are 40×40px circular.
### Collapsing Strategy
- Top product tabs collapse into a hamburger sheet below 744px.
- Search bar's 3 segments collapse into a single-tap entry that opens a full-screen search overlay on mobile.
- Property and city-link grids drop column counts cleanly at each breakpoint — never reflow rows; always reduce columns.
- Reservation card on listing detail switches from sticky right-rail to a sticky bottom bar on mobile, carrying just the "Reserve" CTA + nightly price summary.
## Known Gaps
- **Hover state colors:** intentionally not documented per the global no-hover policy — Airbnb's actual `:hover` styling for property cards is a subtle elevation lift, but precise extraction is unreliable.
- **Loading states / skeleton screens:** not visible on the extracted surfaces.
- **Map view styling:** the search-results map uses Mapbox-tinted tiles with custom Rausch markers; not captured here.
- **Form input error states:** error text color (`{colors.primary-error-text}`) is documented, but the full input outline + helper-text combination on validation failure was not visible in the captured surfaces.
- **Sub-brand palettes:** Luxe (`{colors.luxe}`) and Plus (`{colors.plus}`) are documented as tokens, but their full sub-system (typography overrides, surface treatment) lives on separate sub-domains and is not captured here.

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,838 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspiration of Airbnb</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=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #ff385c;
--primary-active: #e00b41;
--primary-disabled: #ffd1da;
--error: #c13515;
--luxe: #460479;
--plus: #92174d;
--ink: #222222;
--body: #3f3f3f;
--muted: #6a6a6a;
--muted-soft: #929292;
--hairline: #dddddd;
--hairline-soft: #ebebeb;
--border-strong: #c1c1c1;
--canvas: #ffffff;
--surface-soft: #f7f7f7;
--surface-strong: #f2f2f2;
--on-primary: #ffffff;
--legal-link: #428bff;
--shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0;
--display: 'Inter', 'Airbnb Cereal VF', Circular, -apple-system, system-ui, Roboto, 'Helvetica Neue', sans-serif;
--text: 'Inter', 'Airbnb Cereal VF', Circular, -apple-system, system-ui, Roboto, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--canvas); color: var(--ink); font-family: var(--text); font-size: 16px; line-height: 1.5; font-weight: 400; }
.nav { position: sticky; top: 0; z-index: 100; background: var(--canvas); border-bottom: 1px solid var(--hairline); padding: 0 48px; height: 80px; }
.nav-links { list-style: none; display: flex; gap: 32px; align-items: center; }
.nav-links a { color: var(--muted); text-decoration: none; font-size: 16px; font-weight: 600; }
.nav-links a.active { color: var(--ink); border-bottom: 2px solid var(--ink); padding-bottom: 6px; }
.nav-cta { background: transparent; color: var(--ink); border: none; padding: 12px 16px; font-weight: 600; font-size: 14px; cursor: pointer; font-family: inherit; border-radius: 9999px; }
.hero { padding: 64px 48px; background: var(--canvas); display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 64px; align-items: center; max-width: 1440px; margin: 0 auto; }
.hero-content { max-width: 720px; }
.hero h1 { font-family: var(--display); font-size: 48px; font-weight: 700; line-height: 1.1; color: var(--ink); margin: 0 0 24px; letter-spacing: -1px; }
.hero p { font-size: 18px; color: var(--body); max-width: 540px; margin: 0 0 32px; line-height: 1.5; font-weight: 400; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.btn-hero-primary { background: var(--primary); color: var(--on-primary); border: none; padding: 14px 24px; height: 48px; font-weight: 500; font-size: 16px; cursor: pointer; font-family: inherit; border-radius: 8px; }
.btn-hero-secondary { background: var(--canvas); color: var(--ink); border: 1px solid var(--ink); padding: 13px 23px; height: 48px; font-weight: 500; font-size: 16px; cursor: pointer; font-family: inherit; border-radius: 8px; }
.hero-art { aspect-ratio: 1; background: var(--surface-soft); border-radius: 14px; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end; padding: 24px; }
.hero-art::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, #ffe0e6 0%, #fff0f3 50%, var(--surface-soft) 100%); }
.hero-art .orb { position: absolute; right: 24px; bottom: 24px; width: 64px; height: 64px; border-radius: 9999px; background: var(--primary); display: flex; align-items: center; justify-content: center; color: var(--on-primary); font-weight: 600; box-shadow: var(--shadow); }
.hero-art .heart { position: absolute; top: 24px; right: 24px; width: 40px; height: 40px; border-radius: 9999px; background: var(--canvas); display: flex; align-items: center; justify-content: center; color: var(--primary); font-size: 20px; box-shadow: var(--shadow); z-index: 1; }
.hero-art .badge { position: absolute; top: 24px; left: 24px; background: var(--canvas); border-radius: 9999px; padding: 4px 10px; font-size: 11px; font-weight: 600; color: var(--ink); box-shadow: var(--shadow); z-index: 1; }
.hero-art .label { position: relative; z-index: 1; color: var(--ink); font-weight: 600; font-size: 20px; }
.hero-art .sublabel { position: relative; z-index: 1; color: var(--muted); font-size: 14px; margin-top: 4px; font-weight: 400; }
section { max-width: 1440px; margin: 0 auto; padding: 64px 48px; border-top: 1px solid var(--hairline); }
.section-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.32px; color: var(--muted); font-weight: 700; margin-bottom: 12px; }
.section-heading { font-family: var(--display); font-size: 28px; font-weight: 700; color: var(--ink); margin-bottom: 16px; line-height: 1.43; }
.section-intro { font-size: 16px; color: var(--body); max-width: 720px; margin-bottom: 48px; font-weight: 400; line-height: 1.5; }
.palette-group { margin-bottom: 56px; }
.palette-group h3 { font-size: 14px; font-weight: 600; color: var(--ink); margin-bottom: 20px; }
.palette-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
.swatch { border: 1px solid var(--hairline); background: var(--canvas); border-radius: 14px; overflow: hidden; }
.swatch-color { height: 100px; }
.swatch-meta { padding: 14px 16px; }
.swatch-name { font-weight: 600; font-size: 13px; color: var(--ink); margin-bottom: 4px; }
.swatch-hex { font-family: ui-monospace, monospace; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.swatch-role { font-size: 13px; color: var(--body); line-height: 1.5; font-weight: 400; }
.type-row { display: grid; grid-template-columns: 320px 1fr; gap: 32px; align-items: baseline; padding: 20px 0; border-bottom: 1px solid var(--hairline-soft); }
.type-row:last-child { border-bottom: none; }
.type-meta { font-size: 12px; color: var(--muted); line-height: 1.5; font-weight: 400; }
.type-meta strong { display: block; color: var(--ink); font-weight: 600; font-size: 12px; margin-bottom: 4px; }
.type-sample { color: var(--ink); }
.button-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px; }
.button-cell { border: 1px solid var(--hairline); padding: 28px; background: var(--canvas); border-radius: 14px; min-width: 0; }
.button-label { font-size: 12px; color: var(--ink); font-weight: 600; margin-bottom: 12px; }
.button-meta { font-size: 12px; color: var(--muted); margin-top: 14px; line-height: 1.5; font-weight: 400; }
.btn { display: inline-block; padding: 14px 24px; height: 48px; font-size: 16px; font-weight: 500; border: none; cursor: pointer; font-family: inherit; line-height: 1.25; border-radius: 8px; }
.btn-primary { background: var(--primary); color: var(--on-primary); }
.btn-primary-active { background: var(--primary-active); color: var(--on-primary); }
.btn-primary-disabled { background: var(--primary-disabled); color: var(--on-primary); cursor: not-allowed; }
.btn-secondary { background: var(--canvas); color: var(--ink); border: 1px solid var(--ink); padding: 13px 23px; }
.btn-tertiary-text { background: transparent; color: var(--ink); padding: 0; height: auto; text-decoration: underline; font-size: 16px; font-weight: 500; }
.btn-pill { background: var(--primary); color: var(--on-primary); border-radius: 9999px; padding: 10px 20px; height: auto; font-size: 14px; font-weight: 500; }
/* Search bar pill */
.search-bar { display: inline-flex; align-items: center; background: var(--canvas); border: 1px solid var(--hairline); border-radius: 9999px; box-shadow: var(--shadow); height: 64px; padding: 0 8px 0 0; max-width: 100%; overflow: hidden; }
.search-segment { padding: 12px 24px; border-right: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 2px; min-width: 140px; }
.search-segment:last-of-type { border-right: none; }
.search-segment-label { font-size: 12px; font-weight: 600; color: var(--ink); }
.search-segment-value { font-size: 14px; color: var(--muted); font-weight: 400; }
.search-orb { width: 48px; height: 48px; border-radius: 9999px; background: var(--primary); color: var(--on-primary); border: none; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; font-weight: 600; }
/* Product tabs */
.product-tabs { display: inline-flex; gap: 32px; align-items: center; }
.product-tab { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 6px 0; cursor: pointer; position: relative; color: var(--muted); font-weight: 600; font-size: 16px; }
.product-tab.active { color: var(--ink); border-bottom: 2px solid var(--ink); }
.product-tab-icon { width: 32px; height: 32px; border-radius: 9999px; background: linear-gradient(135deg, #fff 0%, var(--surface-strong) 100%); display: flex; align-items: center; justify-content: center; font-size: 16px; position: relative; }
.new-tag { position: absolute; top: -8px; right: -16px; background: var(--canvas); color: var(--ink); padding: 2px 6px; border-radius: 9999px; font-size: 8px; font-weight: 700; letter-spacing: 0.32px; text-transform: uppercase; box-shadow: var(--shadow); }
/* Property cards */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 24px; min-width: 0; }
.card-grid > * { min-width: 0; }
.property-card { background: var(--canvas); display: flex; flex-direction: column; gap: 8px; }
.property-card-photo { width: 100%; aspect-ratio: 1/1; background: linear-gradient(135deg, #ddd, #bbb); border-radius: 14px; position: relative; overflow: hidden; }
.property-card-photo .badge { position: absolute; top: 12px; left: 12px; background: var(--canvas); border-radius: 9999px; padding: 4px 10px; font-size: 11px; font-weight: 600; color: var(--ink); box-shadow: var(--shadow); }
.property-card-photo .heart { position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border-radius: 9999px; background: rgba(0,0,0,0.15); color: var(--canvas); display: flex; align-items: center; justify-content: center; font-size: 18px; }
.property-card-meta { display: flex; flex-direction: column; gap: 2px; padding: 0 4px; }
.property-card-row { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.property-card-title { font-size: 16px; font-weight: 600; color: var(--ink); }
.property-card-rating { font-size: 14px; color: var(--ink); font-weight: 400; }
.property-card-meta-line { font-size: 14px; color: var(--muted); font-weight: 400; }
.property-card-price { font-size: 14px; color: var(--ink); font-weight: 600; margin-top: 4px; }
.property-card-price .nightly { font-weight: 400; color: var(--ink); }
/* Experience cards */
.experience-card-photo { width: 100%; aspect-ratio: 4/5; background: linear-gradient(135deg, #f8a, #f57); border-radius: 14px; position: relative; overflow: hidden; }
.experience-card-photo .badge { position: absolute; top: 12px; left: 12px; background: var(--canvas); padding: 2px 6px; border-radius: 9999px; font-size: 8px; font-weight: 700; color: var(--ink); letter-spacing: 0.32px; text-transform: uppercase; box-shadow: var(--shadow); }
/* Rating display */
.rating-display-card { display: flex; align-items: center; justify-content: center; gap: 24px; padding: 32px 0; }
.rating-laurel { width: 32px; height: 80px; background: var(--ink); -webkit-mask: radial-gradient(ellipse 80% 50% at 100% 50%, transparent 50%, #000 51%); mask: radial-gradient(ellipse 80% 50% at 100% 50%, transparent 50%, #000 51%); opacity: 0.85; }
.rating-laurel.right { transform: scaleX(-1); }
.rating-number { font-family: var(--display); font-size: 64px; font-weight: 700; color: var(--ink); line-height: 1.1; letter-spacing: -1px; }
.rating-tagline { text-align: center; color: var(--muted); font-size: 14px; max-width: 480px; margin: 0 auto 24px; }
.rating-stats { display: grid; grid-template-columns: repeat(5, 1fr); border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); padding: 16px 0; }
.rating-stat { text-align: center; padding: 8px; border-right: 1px solid var(--hairline); }
.rating-stat:last-child { border-right: none; }
.rating-stat-value { font-size: 18px; font-weight: 700; color: var(--ink); }
.rating-stat-label { font-size: 12px; color: var(--muted); margin-top: 4px; }
/* Amenity rows */
.amenity-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; }
.amenity-row { display: flex; align-items: center; gap: 16px; padding: 12px 0; font-size: 16px; color: var(--ink); }
.amenity-icon { width: 24px; height: 24px; border-radius: 4px; background: var(--surface-strong); flex-shrink: 0; }
/* Reviews card */
.reviews-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
.review-card { display: flex; flex-direction: column; gap: 12px; }
.review-author-row { display: flex; align-items: center; gap: 12px; }
.review-avatar { width: 40px; height: 40px; border-radius: 9999px; background: linear-gradient(135deg, var(--primary), var(--luxe)); flex-shrink: 0; }
.review-author-name { font-size: 14px; font-weight: 600; color: var(--ink); }
.review-author-date { font-size: 13px; color: var(--muted); }
.review-excerpt { font-size: 14px; color: var(--ink); line-height: 1.5; }
/* Host & reservation cards */
.listing-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 48px; align-items: start; }
.host-card, .reservation-card { background: var(--canvas); border: 1px solid var(--hairline); border-radius: 14px; padding: 24px; box-shadow: var(--shadow); }
.host-row { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }
.host-avatar { width: 56px; height: 56px; border-radius: 9999px; background: linear-gradient(135deg, var(--primary), #c13515); }
.host-name { font-size: 16px; font-weight: 600; color: var(--ink); }
.host-meta { font-size: 14px; color: var(--muted); }
.reservation-price { font-size: 21px; font-weight: 700; color: var(--ink); margin-bottom: 16px; }
.reservation-price .nightly { font-size: 16px; font-weight: 400; color: var(--ink); }
.reservation-date-row { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--ink); border-radius: 8px; }
.reservation-date-cell { padding: 10px 12px; border-right: 1px solid var(--ink); }
.reservation-date-cell:last-child { border-right: none; }
.reservation-date-label { font-size: 10px; font-weight: 700; color: var(--ink); letter-spacing: 0.32px; text-transform: uppercase; }
.reservation-date-value { font-size: 14px; color: var(--ink); margin-top: 2px; }
.reservation-fees { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
.reservation-fee-row { display: flex; justify-content: space-between; color: var(--ink); }
/* Date picker */
.date-picker { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; max-width: 320px; }
.date-day-head { font-size: 12px; color: var(--muted); text-align: center; padding: 4px 0; font-weight: 400; }
.date-day { width: 40px; height: 40px; border-radius: 9999px; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; color: var(--ink); cursor: pointer; }
.date-day.selected { background: var(--ink); color: var(--canvas); }
.date-day.muted { color: var(--muted-soft); }
/* Forms */
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.form-cell { min-width: 0; position: relative; }
.text-input { width: 100%; background: var(--canvas); color: var(--ink); border: 1px solid var(--hairline); padding: 24px 12px 8px; height: 56px; font-size: 16px; font-family: inherit; font-weight: 400; border-radius: 8px; outline: none; }
.text-input:focus, .text-input.focused { border: 2px solid var(--ink); padding: 23px 11px 7px; }
.form-floating-label { position: absolute; top: 6px; left: 13px; font-size: 12px; color: var(--muted); font-weight: 500; pointer-events: none; }
/* City link grid */
.city-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px 24px; }
.city-block { display: flex; flex-direction: column; gap: 2px; }
.city-name { font-size: 16px; font-weight: 600; color: var(--ink); }
.city-category { font-size: 14px; color: var(--muted); }
/* Category strip */
.category-strip { display: flex; gap: 32px; border-bottom: 1px solid var(--hairline); margin-bottom: 24px; flex-wrap: wrap; padding-bottom: 0; }
.category-tab { padding: 12px 0; color: var(--muted); font-weight: 500; font-size: 14px; cursor: pointer; border-bottom: 2px solid transparent; }
.category-tab.active { color: var(--ink); border-bottom-color: var(--ink); }
/* Spacing scale */
.spacing-row { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
.spacing-block { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.spacing-bar { background: var(--ink); height: 28px; }
.spacing-label { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
/* Radius */
.radius-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.radius-block { width: 96px; height: 96px; background: var(--surface-soft); border: 1px solid var(--hairline); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--ink); font-weight: 600; font-family: ui-monospace, monospace; }
/* Elevation */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 24px; }
.elevation-card { padding: 24px; font-size: 13px; color: var(--ink); border: 1px solid var(--hairline); background: var(--canvas); border-radius: 14px; }
.elevation-card h5 { font-weight: 600; color: var(--ink); margin-bottom: 6px; font-size: 12px; }
.elevation-card p { color: var(--muted); font-size: 13px; font-weight: 400; line-height: 1.5; }
.elevation-card.elevated { box-shadow: var(--shadow); }
/* Responsive table */
.responsive-table { width: 100%; border-collapse: collapse; margin-bottom: 32px; font-size: 14px; }
.responsive-table th, .responsive-table td { text-align: left; padding: 14px 16px; border-bottom: 1px solid var(--hairline); }
.responsive-table th { font-weight: 600; color: var(--ink); font-size: 12px; text-transform: uppercase; letter-spacing: 0.32px; }
.responsive-table td { color: var(--body); font-weight: 400; }
.device-ladder { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 32px; flex-wrap: wrap; }
.device-bar { background: var(--surface-soft); border: 1px solid var(--hairline); color: var(--muted); text-align: center; padding: 8px; font-size: 11px; font-family: ui-monospace, monospace; flex-shrink: 0; border-radius: 8px; }
.responsive-sub { margin-top: 32px; }
.responsive-sub h4 { font-family: var(--display); font-size: 18px; font-weight: 600; color: var(--ink); margin-bottom: 12px; }
.responsive-sub ul { padding-left: 20px; color: var(--body); font-size: 14px; font-weight: 400; line-height: 1.5; }
.footer { background: var(--canvas); color: var(--ink); padding: 48px; font-size: 13px; font-weight: 400; border-top: 1px solid var(--hairline); }
.footer .footer-inner { max-width: 1440px; margin: 0 auto; }
.footer .columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; margin-bottom: 32px; }
.footer .col-head { font-size: 16px; font-weight: 500; color: var(--ink); margin-bottom: 12px; }
.footer .col-link { font-size: 14px; line-height: 2; color: var(--ink); display: block; text-decoration: none; font-weight: 400; }
.footer .legal { padding-top: 24px; border-top: 1px solid var(--hairline); font-size: 13px; color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.footer .legal a { color: var(--ink); text-decoration: none; }
.cell, .cell > *, .demo, .button-cell, .component-cell { min-width: 0; }
.cell.wide, .button-cell.wide, .component-cell.wide { grid-column: span 2; }
@media (max-width: 1024px) {
section { padding: 48px 32px; }
.section-heading { font-size: 24px; }
.nav > :last-child { display: none; }
.hero { grid-template-columns: 1fr; gap: 32px; padding: 48px 32px; }
.hero h1 { font-size: 36px; }
.hero-art, .hero-art-photo { display: none; }
.type-row { grid-template-columns: 1fr; gap: 8px; }
.type-row .type-meta { padding-bottom: 4px; }
.footer .columns { grid-template-columns: repeat(2, 1fr); }
.listing-grid { grid-template-columns: 1fr; gap: 24px; }
.reviews-grid { grid-template-columns: 1fr; }
.amenity-list { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.cell.wide, .button-cell.wide, .component-cell.wide { grid-column: span 1; }
.nav { padding: 0 48px; height: auto; min-height: 56px; }
.nav-links { display: none; }
.hero { padding: 32px 20px; }
.hero h1 { font-size: 28px; }
.hero p { font-size: 15px; }
section { padding: 40px 20px; }
.section-heading { font-size: 22px; }
.palette-grid, .button-grid, .card-grid, .form-grid, .elevation-grid, .city-grid { grid-template-columns: 1fr !important; gap: 16px; }
.rating-stats { grid-template-columns: repeat(2, 1fr); }
.rating-stat:nth-child(2n) { border-right: none; }
.spacing-row, .radius-row { gap: 12px; }
.responsive-table { font-size: 12px; }
.responsive-table th, .responsive-table td { padding: 10px 8px; }
.device-ladder { gap: 8px; }
.footer { padding: 32px 16px; }
.footer .columns { grid-template-columns: 1fr; gap: 16px; }
.search-bar { display: flex; flex-direction: column; align-items: stretch; height: auto; border-radius: 14px; padding: 8px; }
.search-segment { border-right: none; border-bottom: 1px solid var(--hairline); }
}
/* getdesign.md nav additions */
.nav-left { display: flex; align-items: center; gap: 20px; }
.nav-brand-link, .nav-brand-link:link, .nav-brand-link:visited, .nav-brand-link:hover, .nav-brand-link:active { text-decoration: none; color: inherit; }
.nav-brand { font-family: var(--display); font-weight: 700; font-size: 16px; color: var(--ink); }
.nav-github, .nav-github:link, .nav-github:visited, .nav-github:hover, .nav-github:active {
display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600;
color: inherit; text-decoration: none; padding: 5px 10px;
border: 1px solid rgba(128, 128, 128, 0.3); border-radius: 6px;
opacity: 0.75; transition: opacity 200ms ease, border-color 200ms ease;
font-family: inherit; line-height: 1; white-space: nowrap;
}
.nav-github:hover { opacity: 1; border-color: rgba(128, 128, 128, 0.55); }
.nav-github svg { display: block; flex-shrink: 0; }
/* getdesign.md nav centering */
.nav { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
.nav > .nav-left { justify-self: start; }
.nav > .nav-links { justify-self: center; }
.nav > :last-child { justify-self: end; }
/* Shared VoltAgent footer content */
.footer { text-align: left; }
.footer-shell { max-width: 1200px; margin: 0 auto; }
.footer-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 32px; }
.footer-col h6 { margin: 0 0 14px; color: inherit; font: inherit; font-weight: 700; font-size: 14px; line-height: 1.2; }
.footer-col span { display: block; margin: 0 0 10px; color: inherit; opacity: 0.72; font-size: 14px; line-height: 1.35; }
.footer-credit { margin-top: 48px; padding-top: 24px; border-top: 1px solid currentColor; color: inherit; opacity: 0.78; font-size: 13px; line-height: 1.4; }
.footer-credit a { color: inherit; text-decoration: none; font-weight: 600; }
.footer-credit img { border-radius: 3px; vertical-align: -2px; margin-right: 3px; }
@media (max-width: 760px) {
.footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
}
</style>
<style data-axhub-source-preview-safety>
html,
body {
max-width: 100% !important;
overflow-x: hidden !important;
}
a,
button,
[role="button"],
input,
select,
textarea {
pointer-events: none !important;
}
pre,
code {
max-width: 100% !important;
white-space: pre-wrap !important;
overflow-wrap: anywhere !important;
word-break: break-word !important;
}
</style>
</head>
<body>
<nav class="nav">
<div class="nav-left">
<a href="https://getdesign.md/" target="_blank" rel="noopener noreferrer" class="nav-brand-link"><span class="nav-brand">getdesign.md</span></a>
<a class="nav-github" href="https://github.com/VoltAgent/awesome-design-md" target="_blank" rel="noopener noreferrer" aria-label="awesome-design-md on GitHub">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2 .37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
awesome-design-md
</a>
</div>
<ul class="nav-links">
<li><a href="#colors">Colors</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#components">Components</a></li>
<li><a href="#responsive">Responsive</a></li>
</ul>
<button class="nav-cta">Become a host</button>
</nav>
<div class="hero">
<div class="hero-content">
<h1>Design System Inspiration of Airbnb</h1>
<p>A warm, generous consumer marketplace anchored on a clean white canvas with Airbnb Rausch (#ff385c) as the single brand voltage. Cereal VF runs at modest weights so photography and rounded shapes carry the visual heft. Buttons are softly rounded, the search bar is a full pill, and the search-orb glows in Rausch.</p>
<div class="hero-actions">
<button class="btn-hero-primary">Search</button>
<button class="btn-hero-secondary">Become a host</button>
</div>
</div>
<div class="hero-art">
<div class="badge">Guest favorite</div>
<div class="heart"></div>
<div class="orb"></div>
<div class="label">Cottage in Wilmington</div>
<div class="sublabel">$182 night · ★ 4.92</div>
</div>
</div>
<section id="colors">
<div class="section-label">01 — COLOR PALETTE</div>
<h2 class="section-heading">Rausch + ink + soft greys</h2>
<p class="section-intro">A single accent (Rausch) carries every primary moment. The rest of the palette is white canvas, ink text, and three tiers of grey hairlines. Sub-brand purple and magenta are scoped to Luxe and Plus only.</p>
<div class="palette-group">
<h3>Brand</h3>
<div class="palette-grid">
<div class="swatch"><div class="swatch-color" style="background:#ff385c;"></div><div class="swatch-meta"><div class="swatch-name">primary (Rausch)</div><div class="swatch-hex">#ff385c</div><div class="swatch-role">All primary CTAs, search orb, heart save state, brand wordmark.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#e00b41;"></div><div class="swatch-meta"><div class="swatch-name">primary-active</div><div class="swatch-hex">#e00b41</div><div class="swatch-role">Press state on Rausch CTAs.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#ffd1da;"></div><div class="swatch-meta"><div class="swatch-name">primary-disabled</div><div class="swatch-hex">#ffd1da</div><div class="swatch-role">Disabled CTA tint.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#460479;"></div><div class="swatch-meta"><div class="swatch-name">luxe</div><div class="swatch-hex">#460479</div><div class="swatch-role">Sub-brand accent — Airbnb Luxe only.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#92174d;"></div><div class="swatch-meta"><div class="swatch-name">plus</div><div class="swatch-hex">#92174d</div><div class="swatch-role">Sub-brand accent — Airbnb Plus only.</div></div></div>
</div>
</div>
<div class="palette-group">
<h3>Surface</h3>
<div class="palette-grid">
<div class="swatch"><div class="swatch-color" style="background:#ffffff; border-bottom:1px solid var(--hairline);"></div><div class="swatch-meta"><div class="swatch-name">canvas</div><div class="swatch-hex">#ffffff</div><div class="swatch-role">Default page surface — every public page.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#f7f7f7;"></div><div class="swatch-meta"><div class="swatch-name">surface-soft</div><div class="swatch-hex">#f7f7f7</div><div class="swatch-role">Disabled fields, sub-nav hover, filter band.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#f2f2f2;"></div><div class="swatch-meta"><div class="swatch-name">surface-strong</div><div class="swatch-hex">#f2f2f2</div><div class="swatch-role">Circular icon button surface.</div></div></div>
</div>
</div>
<div class="palette-group">
<h3>Hairlines & Borders</h3>
<div class="palette-grid">
<div class="swatch"><div class="swatch-color" style="background:#dddddd;"></div><div class="swatch-meta"><div class="swatch-name">hairline</div><div class="swatch-hex">#dddddd</div><div class="swatch-role">Default 1px divider. Search bar, footer, tables.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#ebebeb;"></div><div class="swatch-meta"><div class="swatch-name">hairline-soft</div><div class="swatch-hex">#ebebeb</div><div class="swatch-role">Lighter divider for editorial body.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#c1c1c1;"></div><div class="swatch-meta"><div class="swatch-name">border-strong</div><div class="swatch-hex">#c1c1c1</div><div class="swatch-role">Disabled outline buttons, focused input outlines.</div></div></div>
</div>
</div>
<div class="palette-group">
<h3>Text</h3>
<div class="palette-grid">
<div class="swatch"><div class="swatch-color" style="background:#222222;"></div><div class="swatch-meta"><div class="swatch-name">ink</div><div class="swatch-hex">#222222</div><div class="swatch-role">Display, body, primary nav links.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#3f3f3f;"></div><div class="swatch-meta"><div class="swatch-name">body</div><div class="swatch-hex">#3f3f3f</div><div class="swatch-role">Long-form review and amenity copy.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#6a6a6a;"></div><div class="swatch-meta"><div class="swatch-name">muted</div><div class="swatch-hex">#6a6a6a</div><div class="swatch-role">Sub-titles, footer sub-labels, "View all" links.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#929292;"></div><div class="swatch-meta"><div class="swatch-name">muted-soft</div><div class="swatch-hex">#929292</div><div class="swatch-role">Disabled link text. Used sparingly.</div></div></div>
</div>
</div>
<div class="palette-group">
<h3>Semantic</h3>
<div class="palette-grid">
<div class="swatch"><div class="swatch-color" style="background:#c13515;"></div><div class="swatch-meta"><div class="swatch-name">error</div><div class="swatch-hex">#c13515</div><div class="swatch-role">Inline form-validation error text.</div></div></div>
<div class="swatch"><div class="swatch-color" style="background:#428bff;"></div><div class="swatch-meta"><div class="swatch-name">legal-link</div><div class="swatch-hex">#428bff</div><div class="swatch-role">Inline legal links (Privacy, Terms).</div></div></div>
</div>
</div>
</section>
<section id="typography">
<div class="section-label">02 — TYPOGRAPHY SCALE</div>
<h2 class="section-heading">Airbnb Cereal VF · modest weights</h2>
<p class="section-intro">Display sizes sit at 2228px in weight 500/600 — the system trusts photography over typographic muscle. Inter is used here as a substitute for Cereal VF.</p>
<div class="type-row"><div class="type-meta"><strong>rating-display</strong>64px / 700 / 1.1 / -1px</div><div class="type-sample" style="font-size:64px; font-weight:700; line-height:1.1; letter-spacing:-1px;">4.81</div></div>
<div class="type-row"><div class="type-meta"><strong>display-xl</strong>28px / 700 / 1.43</div><div class="type-sample" style="font-size:28px; font-weight:700;">Inspiration for future getaways</div></div>
<div class="type-row"><div class="type-meta"><strong>display-lg</strong>22px / 500 / 1.18 / -0.44px</div><div class="type-sample" style="font-size:22px; font-weight:500; line-height:1.18; letter-spacing:-0.44px;">Close to Fethiye Aliyah Bali Beach</div></div>
<div class="type-row"><div class="type-meta"><strong>display-md</strong>21px / 700 / 1.43</div><div class="type-sample" style="font-size:21px; font-weight:700;">What this place offers</div></div>
<div class="type-row"><div class="type-meta"><strong>display-sm</strong>20px / 600 / 1.20 / -0.18px</div><div class="type-sample" style="font-size:20px; font-weight:600;">Things to know</div></div>
<div class="type-row"><div class="type-meta"><strong>title-md</strong>16px / 600 / 1.25</div><div class="type-sample" style="font-size:16px; font-weight:600;">Wilmington</div></div>
<div class="type-row"><div class="type-meta"><strong>title-sm</strong>16px / 500 / 1.25</div><div class="type-sample" style="font-size:16px; font-weight:500;">Support</div></div>
<div class="type-row"><div class="type-meta"><strong>body-md</strong>16px / 400 / 1.5</div><div class="type-sample" style="font-size:16px; font-weight:400; line-height:1.5;">A quiet villa, garden, and direct beach access. Walk to the village square in eight minutes.</div></div>
<div class="type-row"><div class="type-meta"><strong>body-sm</strong>14px / 400 / 1.43</div><div class="type-sample" style="font-size:14px; font-weight:400;">Cottage rentals · Mar 1218 · $182 night</div></div>
<div class="type-row"><div class="type-meta"><strong>caption</strong>14px / 500 / 1.29</div><div class="type-sample" style="font-size:14px; font-weight:500;">Where · When · Who</div></div>
<div class="type-row"><div class="type-meta"><strong>caption-sm</strong>13px / 400 / 1.23</div><div class="type-sample" style="font-size:13px; font-weight:400;">© 2026 Airbnb, Inc. · Privacy · Terms · Your Privacy Choices</div></div>
<div class="type-row"><div class="type-meta"><strong>badge</strong>11px / 600 / 1.18</div><div class="type-sample" style="font-size:11px; font-weight:600;">Guest favorite</div></div>
<div class="type-row"><div class="type-meta"><strong>uppercase-tag</strong>8px / 700 / 1.25 / 0.32px (uppercase)</div><div class="type-sample" style="font-size:8px; font-weight:700; letter-spacing:0.32px; text-transform:uppercase;">NEW</div></div>
<div class="type-row"><div class="type-meta"><strong>button-md</strong>16px / 500 / 1.25</div><div class="type-sample" style="font-size:16px; font-weight:500;">Reserve</div></div>
<div class="type-row"><div class="type-meta"><strong>nav-link</strong>16px / 600 / 1.25</div><div class="type-sample" style="font-size:16px; font-weight:600;">Homes · Experiences · Services</div></div>
</section>
<section id="components">
<div class="section-label">03 — BUTTON VARIANTS</div>
<h2 class="section-heading">Rausch primary, ink outline secondary</h2>
<p class="section-intro">All buttons are 8px corner radius (`{rounded.sm}`). Pill variants step up to fully-rounded for category strips and featured cells.</p>
<div class="button-grid">
<div class="button-cell"><div class="button-label">button-primary</div><button class="btn btn-primary">Reserve</button><div class="button-meta">Rausch / on-primary white / 8px radius</div></div>
<div class="button-cell"><div class="button-label">button-primary-active</div><button class="btn btn-primary-active">Pressed</button><div class="button-meta">primary-active deeper red</div></div>
<div class="button-cell"><div class="button-label">button-primary-disabled</div><button class="btn btn-primary-disabled" disabled>Reserve</button></div>
<div class="button-cell"><div class="button-label">button-secondary</div><button class="btn btn-secondary">Save</button><div class="button-meta">canvas / ink / 1px ink outline</div></div>
<div class="button-cell"><div class="button-label">button-tertiary-text</div><button class="btn btn-tertiary-text">Show more</button><div class="button-meta">Underlined ink — modal close, "Show more"</div></div>
<div class="button-cell"><div class="button-label">button-pill-rausch</div><button class="btn btn-pill">Become a host</button><div class="button-meta">Pill 9999px radius, 14px label</div></div>
</div>
</section>
<section>
<div class="section-label">04 — TOP NAV + PRODUCT TABS</div>
<h2 class="section-heading">Three product tabs: Homes · Experiences · Services</h2>
<p class="section-intro">Each tab carries a 32px hand-illustrated icon. Active tab uses a 2px ink underline. NEW tag pin is a tiny rounded pill in uppercase 8px / 700.</p>
<div class="product-tabs">
<div class="product-tab active"><div class="product-tab-icon">🏠</div>Homes</div>
<div class="product-tab"><div class="product-tab-icon">🎈<span class="new-tag">NEW</span></div>Experiences</div>
<div class="product-tab"><div class="product-tab-icon">🛎<span class="new-tag">NEW</span></div>Services</div>
</div>
</section>
<section>
<div class="section-label">05 — SEARCH BAR</div>
<h2 class="section-heading">Pill-shaped global search with three segments</h2>
<p class="section-intro">9999px radius, 64px height, white surface, 1px hairline border with the system's single shadow tier applied. Internally divided into Where / When / Who segments. Right edge terminates in a circular Rausch search orb.</p>
<div class="search-bar">
<div class="search-segment">
<div class="search-segment-label">Where</div>
<div class="search-segment-value">Search destinations</div>
</div>
<div class="search-segment">
<div class="search-segment-label">When</div>
<div class="search-segment-value">Add dates</div>
</div>
<div class="search-segment">
<div class="search-segment-label">Who</div>
<div class="search-segment-value">Add guests</div>
</div>
<button class="search-orb" aria-label="Search"></button>
</div>
</section>
<section>
<div class="section-label">06 — CATEGORY STRIP</div>
<h2 class="section-heading">Inline category tabs</h2>
<p class="section-intro">Beneath the search bar on listing pages: a horizontal row of tap-targets in `{typography.button-sm}` muted, with a 2px ink underline marking the active selection.</p>
<div class="category-strip">
<div class="category-tab active">Popular</div>
<div class="category-tab">Arts &amp; culture</div>
<div class="category-tab">Beach</div>
<div class="category-tab">Mountains</div>
<div class="category-tab">Outdoors</div>
<div class="category-tab">Things to do</div>
<div class="category-tab">Travel tips &amp; inspiration</div>
<div class="category-tab">Airbnb-friendly apartments</div>
</div>
</section>
<section>
<div class="section-label">07 — PROPERTY CARDS</div>
<h2 class="section-heading">Photo-first property cards</h2>
<p class="section-intro">A 1:1 photo plate with `{rounded.md}` 14px corner clipping, "Guest favorite" floating badge top-left, heart icon top-right, and a four-line meta block beneath: title, location, dates, price.</p>
<div class="card-grid">
<div class="property-card">
<div class="property-card-photo">
<div class="badge">Guest favorite</div>
<div class="heart"></div>
</div>
<div class="property-card-meta">
<div class="property-card-row"><div class="property-card-title">Wilmington</div><div class="property-card-rating">★ 4.92</div></div>
<div class="property-card-meta-line">Cottage rentals</div>
<div class="property-card-meta-line">Mar 12 18</div>
<div class="property-card-price">$182 <span class="nightly">night</span></div>
</div>
</div>
<div class="property-card">
<div class="property-card-photo" style="background:linear-gradient(135deg,#ddd,#999);">
<div class="badge">Guest favorite</div>
<div class="heart"></div>
</div>
<div class="property-card-meta">
<div class="property-card-row"><div class="property-card-title">Key West</div><div class="property-card-rating">★ 4.81</div></div>
<div class="property-card-meta-line">Monthly rentals</div>
<div class="property-card-meta-line">Apr 2 22</div>
<div class="property-card-price">$240 <span class="nightly">night</span></div>
</div>
</div>
<div class="property-card">
<div class="property-card-photo" style="background:linear-gradient(135deg,#bbb,#777);">
<div class="badge">Guest favorite</div>
<div class="heart"></div>
</div>
<div class="property-card-meta">
<div class="property-card-row"><div class="property-card-title">Athens</div><div class="property-card-rating">★ 4.95</div></div>
<div class="property-card-meta-line">Condo rentals</div>
<div class="property-card-meta-line">May 4 11</div>
<div class="property-card-price">$167 <span class="nightly">night</span></div>
</div>
</div>
<div class="property-card">
<div class="property-card-photo" style="background:linear-gradient(135deg,#ccc,#888);">
<div class="heart"></div>
</div>
<div class="property-card-meta">
<div class="property-card-row"><div class="property-card-title">Pocono Mountains</div><div class="property-card-rating">★ 4.74</div></div>
<div class="property-card-meta-line">House rentals</div>
<div class="property-card-meta-line">Jun 18 25</div>
<div class="property-card-price">$298 <span class="nightly">night</span></div>
</div>
</div>
</div>
</section>
<section>
<div class="section-label">08 — EXPERIENCE CARDS</div>
<h2 class="section-heading">4:5 aspect cards with NEW tag</h2>
<p class="section-intro">Taller-aspect than property cards. Single-line title beneath the photo. New experiences carry a tiny NEW pin top-left.</p>
<div class="card-grid">
<div class="property-card">
<div class="experience-card-photo"><div class="badge">NEW</div><div class="heart"></div></div>
<div class="property-card-meta">
<div class="property-card-title">Sunset cooking class · Athens</div>
<div class="property-card-meta-line">From $48 / person</div>
</div>
</div>
<div class="property-card">
<div class="experience-card-photo" style="background:linear-gradient(135deg,#79c,#358);"><div class="badge">NEW</div><div class="heart"></div></div>
<div class="property-card-meta">
<div class="property-card-title">Half-day surf lesson · Pocono</div>
<div class="property-card-meta-line">From $72 / person</div>
</div>
</div>
<div class="property-card">
<div class="experience-card-photo" style="background:linear-gradient(135deg,#bda,#586);"><div class="heart"></div></div>
<div class="property-card-meta">
<div class="property-card-title">Olive grove walk · Crete</div>
<div class="property-card-meta-line">From $35 / person</div>
</div>
</div>
<div class="property-card">
<div class="experience-card-photo" style="background:linear-gradient(135deg,#fac,#a36);"><div class="badge">NEW</div><div class="heart"></div></div>
<div class="property-card-meta">
<div class="property-card-title">Pottery studio · Lisbon</div>
<div class="property-card-meta-line">From $58 / person</div>
</div>
</div>
</div>
</section>
<section>
<div class="section-label">09 — RATING DISPLAY</div>
<h2 class="section-heading">The signature listing-detail moment</h2>
<p class="section-intro">A 64px / 700 rating number flanked by laurel-wreath ornaments. The single typographically loud moment in the entire system. Guest-favorite tagline and stat row beneath.</p>
<div class="rating-display-card">
<div class="rating-laurel"></div>
<div class="rating-number">4.81</div>
<div class="rating-laurel right"></div>
</div>
<p class="rating-tagline">Guest favorite — One of the most loved homes on Airbnb based on ratings, reviews, and reliability.</p>
<div class="rating-stats">
<div class="rating-stat"><div class="rating-stat-value">4.9</div><div class="rating-stat-label">Cleanliness</div></div>
<div class="rating-stat"><div class="rating-stat-value">4.8</div><div class="rating-stat-label">Accuracy</div></div>
<div class="rating-stat"><div class="rating-stat-value">4.9</div><div class="rating-stat-label">Check-in</div></div>
<div class="rating-stat"><div class="rating-stat-value">4.8</div><div class="rating-stat-label">Communication</div></div>
<div class="rating-stat"><div class="rating-stat-value">4.7</div><div class="rating-stat-label">Value</div></div>
</div>
</section>
<section>
<div class="section-label">10 — AMENITIES + REVIEWS</div>
<h2 class="section-heading">What this place offers</h2>
<p class="section-intro">Two-column amenity list at desktop, 1-column at mobile. Each row pairs a 24×24px icon plate with the amenity label in `{typography.body-md}`.</p>
<div class="amenity-list">
<div class="amenity-row"><div class="amenity-icon"></div>Wifi · 200 Mbps</div>
<div class="amenity-row"><div class="amenity-icon"></div>Beach access — beachfront</div>
<div class="amenity-row"><div class="amenity-icon"></div>Free parking on premises</div>
<div class="amenity-row"><div class="amenity-icon"></div>Air conditioning</div>
<div class="amenity-row"><div class="amenity-icon"></div>Kitchen</div>
<div class="amenity-row"><div class="amenity-icon"></div>Workspace — dedicated</div>
<div class="amenity-row"><div class="amenity-icon"></div>Pool</div>
<div class="amenity-row"><div class="amenity-icon"></div>Pets allowed</div>
</div>
<h3 style="font-family:var(--display); font-size:21px; font-weight:700; color:var(--ink); margin: 48px 0 16px;">Reviews</h3>
<div class="reviews-grid">
<div class="review-card">
<div class="review-author-row"><div class="review-avatar"></div><div><div class="review-author-name">Mira</div><div class="review-author-date">2 weeks ago · 1 year on Airbnb</div></div></div>
<p class="review-excerpt">A quiet villa, garden, and direct beach access. The host was responsive and the kitchen was perfectly equipped for a long stay.</p>
</div>
<div class="review-card">
<div class="review-author-row"><div class="review-avatar" style="background:linear-gradient(135deg,#460479,#92174d);"></div><div><div class="review-author-name">Daniel</div><div class="review-author-date">1 month ago · 3 years on Airbnb</div></div></div>
<p class="review-excerpt">Photos were honest. The neighborhood is walkable in the evening and the host left a hand-drawn map of the local market.</p>
</div>
</div>
</section>
<section>
<div class="section-label">11 — HOST + RESERVATION CARDS</div>
<h2 class="section-heading">Listing detail: 2-column with sticky reservation rail</h2>
<p class="section-intro">Left column carries amenities, reviews, and host card. Right column is the sticky `{component.reservation-card}` — nightly price, date selector, guest stepper, primary CTA, fee breakdown.</p>
<div class="listing-grid">
<div class="host-card">
<div class="host-row">
<div class="host-avatar"></div>
<div>
<div class="host-name">Hosted by Eylem</div>
<div class="host-meta">Superhost · 4 years hosting · Speaks English, Turkish</div>
</div>
</div>
<p style="font-size:14px; color:var(--body); line-height:1.5; margin-bottom: 16px;">Eylem is a Superhost — Superhosts are experienced, highly rated hosts who are committed to providing great stays.</p>
<button class="btn btn-secondary">Contact host</button>
</div>
<div class="reservation-card">
<div class="reservation-price">$182 <span class="nightly">night</span></div>
<div class="reservation-date-row">
<div class="reservation-date-cell">
<div class="reservation-date-label">Check-in</div>
<div class="reservation-date-value">Mar 12, 2026</div>
</div>
<div class="reservation-date-cell">
<div class="reservation-date-label">Checkout</div>
<div class="reservation-date-value">Mar 18, 2026</div>
</div>
</div>
<div style="margin-top: 12px; border:1px solid var(--ink); border-radius: 8px; padding: 10px 12px;">
<div class="reservation-date-label">Guests</div>
<div class="reservation-date-value">2 adults</div>
</div>
<button class="btn btn-primary" style="width: 100%; margin-top: 16px;">Reserve</button>
<div class="reservation-fees">
<div class="reservation-fee-row"><span>$182 × 6 nights</span><span>$1,092</span></div>
<div class="reservation-fee-row"><span>Cleaning fee</span><span>$48</span></div>
<div class="reservation-fee-row"><span>Service fee</span><span>$165</span></div>
<div class="reservation-fee-row" style="border-top: 1px solid var(--hairline); padding-top: 12px; margin-top: 4px; font-weight: 600;"><span>Total before taxes</span><span>$1,305</span></div>
</div>
</div>
</div>
</section>
<section>
<div class="section-label">12 — DATE PICKER</div>
<h2 class="section-heading">Circular day cells, ink fill on selected</h2>
<p class="section-intro">40×40px circular cells. Selected days get an ink fill with white text; range states sit on a soft-grey lozenge.</p>
<div class="date-picker">
<div class="date-day-head">Mo</div><div class="date-day-head">Tu</div><div class="date-day-head">We</div><div class="date-day-head">Th</div><div class="date-day-head">Fr</div><div class="date-day-head">Sa</div><div class="date-day-head">Su</div>
<div class="date-day muted">25</div><div class="date-day muted">26</div><div class="date-day muted">27</div><div class="date-day muted">28</div><div class="date-day">1</div><div class="date-day">2</div><div class="date-day">3</div>
<div class="date-day">4</div><div class="date-day">5</div><div class="date-day">6</div><div class="date-day">7</div><div class="date-day">8</div><div class="date-day">9</div><div class="date-day">10</div>
<div class="date-day">11</div><div class="date-day selected">12</div><div class="date-day">13</div><div class="date-day">14</div><div class="date-day">15</div><div class="date-day">16</div><div class="date-day">17</div>
<div class="date-day selected">18</div><div class="date-day">19</div><div class="date-day">20</div><div class="date-day">21</div><div class="date-day">22</div><div class="date-day">23</div><div class="date-day">24</div>
<div class="date-day">25</div><div class="date-day">26</div><div class="date-day">27</div><div class="date-day">28</div><div class="date-day">29</div><div class="date-day">30</div><div class="date-day">31</div>
</div>
</section>
<section>
<div class="section-label">13 — FORMS</div>
<h2 class="section-heading">Floating-label text inputs</h2>
<p class="section-intro">56px height, 1px hairline outline that thickens to 2px ink on focus. The label floats to the top inside the input.</p>
<div class="form-grid">
<div class="form-cell"><input class="text-input focused" type="text" value="2 adults"><div class="form-floating-label">Guests</div></div>
<div class="form-cell"><input class="text-input" type="text" placeholder=""><div class="form-floating-label">Email address</div></div>
<div class="form-cell"><input class="text-input" type="text" placeholder=""><div class="form-floating-label">Phone number</div></div>
<div class="form-cell wide"><input class="text-input" type="text" placeholder=""><div class="form-floating-label">Message to host</div></div>
</div>
</section>
<section>
<div class="section-label">14 — CITY LINK GRID</div>
<h2 class="section-heading">Inspiration for future getaways</h2>
<p class="section-intro">Homepage's signature grid. 6-column at desktop, each cell is a city name in `{typography.title-md}` over a category sub-label in `{typography.body-sm}` muted.</p>
<div class="city-grid">
<div class="city-block"><div class="city-name">Wilmington</div><div class="city-category">Cottage rentals</div></div>
<div class="city-block"><div class="city-name">Key West</div><div class="city-category">Monthly rentals</div></div>
<div class="city-block"><div class="city-name">Cleveland</div><div class="city-category">Villa rentals</div></div>
<div class="city-block"><div class="city-name">London</div><div class="city-category">Condo rentals</div></div>
<div class="city-block"><div class="city-name">Barcelona</div><div class="city-category">Condo rentals</div></div>
<div class="city-block"><div class="city-name">Memphis</div><div class="city-category">Vacation rentals</div></div>
<div class="city-block"><div class="city-name">Pocono Mountains</div><div class="city-category">House rentals</div></div>
<div class="city-block"><div class="city-name">Athens</div><div class="city-category">Condo rentals</div></div>
<div class="city-block"><div class="city-name">Maui</div><div class="city-category">Villa rentals</div></div>
<div class="city-block"><div class="city-name">St. Petersburg</div><div class="city-category">Vacation rentals</div></div>
<div class="city-block"><div class="city-name">North Myrtle Beach</div><div class="city-category">Villa rentals</div></div>
<div class="city-block"><div class="city-name">Portland</div><div class="city-category">Apartment rentals</div></div>
</div>
</section>
<section>
<div class="section-label">15 — SPACING SCALE</div>
<p class="section-intro">4px base with a 2px micro-step. Major sections at 64px.</p>
<div class="spacing-row">
<div class="spacing-block"><div class="spacing-bar" style="width:2px;"></div><div class="spacing-label">xxs · 2px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:4px;"></div><div class="spacing-label">xs · 4px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:8px;"></div><div class="spacing-label">sm · 8px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:12px;"></div><div class="spacing-label">md · 12px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:16px;"></div><div class="spacing-label">base · 16px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:24px;"></div><div class="spacing-label">lg · 24px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:32px;"></div><div class="spacing-label">xl · 32px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:48px;"></div><div class="spacing-label">xxl · 48px</div></div>
<div class="spacing-block"><div class="spacing-bar" style="width:64px;"></div><div class="spacing-label">section · 64px</div></div>
</div>
</section>
<section>
<div class="section-label">16 — BORDER RADIUS</div>
<p class="section-intro">Soft, generous rounding. Buttons at 8px, cards at 14px, search bar fully pill (9999px).</p>
<div class="radius-row">
<div class="radius-block" style="border-radius:0;">0 · none</div>
<div class="radius-block" style="border-radius:4px;">4 · xs</div>
<div class="radius-block" style="border-radius:8px;">8 · sm</div>
<div class="radius-block" style="border-radius:14px;">14 · md</div>
<div class="radius-block" style="border-radius:20px;">20 · lg</div>
<div class="radius-block" style="border-radius:32px;">32 · xl</div>
<div class="radius-block" style="border-radius:9999px;">full</div>
</div>
</section>
<section>
<div class="section-label">17 — ELEVATION & DEPTH</div>
<p class="section-intro">Essentially one shadow tier. Most surfaces are flat — depth comes from photography and rounded clipping.</p>
<div class="elevation-grid">
<div class="elevation-card"><h5>Flat / Canvas</h5><p>Body, hero, footer, all editorial bands — 95% of surfaces.</p></div>
<div class="elevation-card elevated"><h5>Card hover float</h5><p>The single shadow tier — search bar at rest, hovered property cards, dropdown menus.</p></div>
<div class="elevation-card"><h5>Rounded clipping</h5><p>Photo cards rely on `{rounded.md}` corner clipping for "card" feel without a shadow.</p></div>
</div>
</section>
<section id="responsive">
<div class="section-label">18 — RESPONSIVE BEHAVIOR</div>
<table class="responsive-table">
<thead><tr><th>Name</th><th>Width</th><th>Key Changes</th></tr></thead>
<tbody>
<tr><td>Mobile</td><td>&lt; 744px</td><td>Hamburger nav; product tabs hide behind sheet; search bar collapses to a single tappable pill; property cards 1-up; reservation collapses to sticky bottom bar.</td></tr>
<tr><td>Tablet</td><td>7441128px</td><td>Top nav keeps tabs; search bar narrows; property cards 2-up; reservation card stays sticky right-rail.</td></tr>
<tr><td>Desktop</td><td>11281440px</td><td>Full top nav with three tabs centered; full-width search bar; property cards 4-up; city grid 6-column.</td></tr>
<tr><td>Wide</td><td>&gt; 1440px</td><td>Listing/search content caps at 1440px; editorial caps at 1280px.</td></tr>
</tbody>
</table>
<div class="device-ladder">
<div class="device-bar" style="width:60px; height:120px;">375<br>mobile</div>
<div class="device-bar" style="width:90px; height:140px;">600<br>small phone</div>
<div class="device-bar" style="width:140px; height:160px;">744<br>tablet</div>
<div class="device-bar" style="width:200px; height:180px;">1128<br>laptop</div>
<div class="device-bar" style="width:260px; height:200px;">1280<br>desktop</div>
<div class="device-bar" style="width:320px; height:220px;">1440<br>wide</div>
</div>
<div class="responsive-sub"><h4>Touch Targets</h4><ul><li>Primary CTAs at minimum 48 × 48px (above WCAG AAA).</li><li>Search orb is 48 × 48px circular — the most-tapped element on the page.</li><li>Heart save button 32 × 32px circular — borderline AAA, compensated by 12px photo padding.</li><li>Date-picker day cells are 40 × 40px circular.</li></ul></div>
<div class="responsive-sub"><h4>Collapsing Strategy</h4><ul><li>Top product tabs collapse into a hamburger sheet below 744px.</li><li>Search bar's 3 segments collapse into a single-tap entry on mobile.</li><li>Property and city grids drop column counts cleanly — never reflow rows.</li><li>Reservation card switches from sticky right-rail to sticky bottom bar on mobile.</li></ul></div>
</section>
<footer class="footer">
<div class="footer-shell">
<div class="footer-grid">
<div class="footer-col">
<h6>VoltAgent Framework</h6>
<span>TypeScript agents</span>
<span>Tool orchestration</span>
<span>Multi-agent workflows</span>
</div>
<div class="footer-col">
<h6>VoltOps LLM Observability</h6>
<span>LLM tracing</span>
<span>Live debugging</span>
<span>Visual timelines</span>
</div>
<div class="footer-col">
<h6>Production</h6>
<span>Evaluations</span>
<span>Prompt management</span>
<span>Guardrails</span>
</div>
<div class="footer-col">
<h6>Platform</h6>
<span>VoltOps console</span>
<span>Open source core</span>
<span>Agent engineering</span>
</div>
</div>
<div class="footer-credit">
Maintained by <a href="https://github.com/VoltAgent/voltagent" target="_blank" rel="noopener noreferrer"><img src="https://github.com/VoltAgent.png?size=32" alt="VoltAgent" width="14" height="14">VoltAgent</a> team
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,49 @@
{
"palette": [
"#000000",
"#ff385c",
"#e00b41",
"#c13515",
"#ffd1da",
"#b32505",
"#460479",
"#92174d",
"#222222",
"#3f3f3f",
"#6a6a6a",
"#929292"
],
"typography": {
"display": "Airbnb Cereal VF",
"body": "Circular",
"mono": "ui-monospace",
"hints": [
"Airbnb Cereal VF",
"Circular",
"ui-monospace",
"Roboto",
"Helvetica Neue",
"Inter"
]
},
"sourceCategory": "marketplace",
"radius": {
"control": "8px",
"card": "14px",
"preview": "32px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "2px",
"xs": "4px",
"sm": "8px",
"md": "12px",
"base": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "64px",
"source": "design-md"
}
}

View File

@@ -0,0 +1,41 @@
/**
* @name Airbnb 主题 - Airbnb
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/official-homepage.webp?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,35 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Airbnb. */
.dmb-page {
--dmb-accent: #ff385c;
--dmb-accent-contrast: #ffffff;
--dmb-link: #e00b41;
--dmb-muted: #c13515;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-radius-xs: 8px;
--dmb-radius-control: 8px;
--dmb-radius-card: 14px;
--dmb-radius-preview: 32px;
--dmb-radius-pill: 9999px;
--dmb-border: #dddddd;
--dmb-border-sample-bg: #f7f7f7;
--dmb-spacing-xxs: 2px;
--dmb-spacing-xs: 4px;
--dmb-spacing-sm: 8px;
--dmb-spacing-md: 12px;
--dmb-spacing-base: 16px;
--dmb-spacing-lg: 24px;
--dmb-spacing-xl: 32px;
--dmb-spacing-xxl: 48px;
--dmb-spacing-section: 64px;
--dmb-font-display: "Airbnb Cereal VF", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Circular", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,297 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "getdesign.md",
"originalDetailUrl": "https://getdesign.md/airbnb/design-md",
"fallbackRawUrl": "https://raw.githubusercontent.com/VoltAgent/awesome-design-md/main/design-md/airbnb/DESIGN.md",
"mirrorSource": "github:VoltAgent/awesome-design-md",
"websiteUrl": "https://airbnb.com/"
},
"identity": {
"id": "P0-59",
"slug": "airbnb",
"brand": "Airbnb",
"titleZh": "Airbnb 主题",
"titleEn": "Airbnb",
"descriptionZh": "Airbnb 的 Design.md 主题展示,围绕平台市场、媒体内容、暖色调、消费品牌组织页面。",
"descriptionEn": "Travel marketplace. Warm coral accent, photography-driven, rounded UI."
},
"tags": {
"sourceTags": [],
"generatedTags": [
"marketplace",
"request-flow",
"media",
"warm"
],
"designTags": [],
"distributionTags": [
"平台市场",
"媒体内容",
"暖色调",
"消费品牌",
"电商零售",
"品牌展示",
"移动端",
"内容货架",
"趣味互动"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"sourcePath": ".local/design-md-batch-regression/getdesign/airbnb/DESIGN.md"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/getdesign/airbnb/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": null,
"previewHtml": {
"type": "local-preview",
"path": "assets/official-homepage.webp",
"publicPath": "assets/official-homepage.webp",
"importPath": "./assets/official-homepage.webp?url",
"sourcePath": "assets/official-homepage.webp"
}
},
"tokens": {
"palette": [
"#000000",
"#ff385c",
"#e00b41",
"#c13515",
"#ffd1da",
"#b32505",
"#460479",
"#92174d",
"#222222",
"#3f3f3f",
"#6a6a6a",
"#929292"
],
"typography": {
"display": "Airbnb Cereal VF",
"body": "Circular",
"mono": "ui-monospace",
"hints": [
"Airbnb Cereal VF",
"Circular",
"ui-monospace",
"Roboto",
"Helvetica Neue",
"Inter"
]
},
"radius": {
"control": "8px",
"card": "14px",
"preview": "32px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "2px",
"xs": "4px",
"sm": "8px",
"md": "12px",
"base": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "64px",
"source": "design-md"
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/official-homepage.webp"
}
],
"display": {
"brand": "Airbnb 主题",
"brandAlias": "Airbnb",
"description": "Airbnb 的 Design.md 主题展示,围绕平台市场、媒体内容、暖色调、消费品牌组织页面。",
"descriptionEn": "Travel marketplace. Warm coral accent, photography-driven, rounded UI.",
"variant": "consumer-commerce",
"distributionTags": [
"平台市场",
"媒体内容",
"暖色调",
"消费品牌",
"电商零售",
"品牌展示",
"移动端",
"内容货架",
"趣味互动"
],
"fontStylesheets": [],
"palette": [
{
"color": "#000000",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#ff385c",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#e00b41",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#ffffff"
},
{
"color": "#c13515",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#ffffff"
},
{
"color": "#ffd1da",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#b32505",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#ffffff"
},
{
"color": "#460479",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#ffffff"
},
{
"color": "#92174d",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#ffffff"
},
{
"color": "#222222",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#ffffff"
},
{
"color": "#3f3f3f",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#6a6a6a",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#ffffff"
},
{
"color": "#929292",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#ffffff"
}
],
"shadows": [
{
"label": "阴影 1 - Shadow 1",
"value": "box-shadow: rgba(0,0,0,0.02) 0 0 0 1px, rgba(0,0,0,0.04) 0 2px 6px, rgba(0,0,0,0.1) 0 4px 8px",
"cssValue": "box-shadow: rgba(0,0,0,0.02) 0 0 0 1px, rgba(0,0,0,0.04) 0 2px 6px, rgba(0,0,0,0.1) 0 4px 8px",
"description": "The design system caps elevation at one shadow tier (`box-shadow: rgba(0,0,0,0.02) 0 0 0 1px, rgba(0,0,0,0.04) 0 2px 6px, rgba(0,0,0,0.1) 0 4px 8px`) — used on hover-floated cards and search/account dropdowns."
},
{
"label": "阴影 2 - Shadow 2",
"value": "box-shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0",
"cssValue": "box-shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0",
"description": "Card hover float: `box-shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0` — applied to property cards on pointer hover, the search bar at rest, and the dropdown menus (account menu, language picker, date picker). This is the single shadow definition in the entire system."
}
],
"borders": [
{
"label": "边框 1 - Border 1",
"value": "#dddddd",
"cssValue": "#dddddd",
"description": "hairline: \"#dddddd\""
},
{
"label": "边框 2 - Border 2",
"value": "#ebebeb",
"cssValue": "#ebebeb",
"description": "hairline-soft: \"#ebebeb\""
},
{
"label": "边框 3 - Border 3",
"value": "#c1c1c1",
"cssValue": "#c1c1c1",
"description": "border-strong: \"#c1c1c1\""
}
],
"typography": [
"Airbnb Cereal VF",
"Circular",
"ui-monospace",
"Roboto",
"Helvetica Neue",
"Inter"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/official-homepage.webp"
}
],
"panels": [
{
"eyebrow": "品牌",
"title": "第一眼识别明确",
"body": "适合消费品牌、电商、媒体和产品展示页。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"radius": {
"control": "8px",
"card": "14px",
"preview": "32px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "2px",
"xs": "4px",
"sm": "8px",
"md": "12px",
"base": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "64px",
"source": "design-md"
}
},
"status": {
"displayPage": "doing",
"collectionErrors": []
}
}

3
src/themes/airbnb/tw.css Normal file
View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Airbnb. */

View File

@@ -0,0 +1,554 @@
---
version: alpha
name: Airtable-design-analysis
description: A sober, editorial workflow-software interface anchored on white canvas and dark-ink type, where brand voltage comes from full-bleed signature cards in coral, dark green, peach, and dark navy that punctuate long-scroll explainer pages. Primary actions use a near-black pill CTA; secondary actions sit in a white outlined button. Type runs Haas Grotesk in modest weights — never bold for its own sake.
colors:
primary: "#181d26"
primary-active: "#0d1218"
ink: "#181d26"
body: "#333840"
muted: "#41454d"
hairline: "#dddddd"
border-strong: "#9297a0"
canvas: "#ffffff"
surface-soft: "#f8fafc"
surface-strong: "#e0e2e6"
surface-dark: "#181d26"
surface-dark-elevated: "#1d1f25"
signature-coral: "#aa2d00"
signature-forest: "#0a2e0e"
signature-cream: "#f5e9d4"
signature-peach: "#fcab79"
signature-mint: "#a8d8c4"
signature-yellow: "#f4d35e"
signature-mustard: "#d9a441"
on-primary: "#ffffff"
on-dark: "#ffffff"
link: "#1b61c9"
link-active: "#1a3866"
info: "#254fad"
info-border: "#458fff"
success: "#006400"
success-border: "#39bf45"
pricing-ink: "#1d1f25"
typography:
display-xl:
fontFamily: "Haas Groot Disp, Haas, sans-serif"
fontSize: 48px
fontWeight: 500
lineHeight: 1.1
letterSpacing: 0
display-lg:
fontFamily: "Haas Groot Disp, Haas, sans-serif"
fontSize: 40px
fontWeight: 400
lineHeight: 1.2
letterSpacing: 0
display-md:
fontFamily: "Haas Groot Disp, Haas, sans-serif"
fontSize: 32px
fontWeight: 400
lineHeight: 1.2
letterSpacing: 0
title-lg:
fontFamily: "Haas, sans-serif"
fontSize: 24px
fontWeight: 400
lineHeight: 1.35
letterSpacing: 0.12px
title-md:
fontFamily: "Haas Groot Disp, Haas, sans-serif"
fontSize: 20px
fontWeight: 400
lineHeight: 1.5
letterSpacing: 0
title-sm:
fontFamily: "Haas, sans-serif"
fontSize: 18px
fontWeight: 500
lineHeight: 1.4
letterSpacing: 0
label-md:
fontFamily: "Haas, sans-serif"
fontSize: 16px
fontWeight: 500
lineHeight: 1.4
letterSpacing: 0
button:
fontFamily: "Haas, sans-serif"
fontSize: 16px
fontWeight: 500
lineHeight: 1.4
letterSpacing: 0
body-md:
fontFamily: "Haas, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.25
letterSpacing: 0
caption:
fontFamily: "Haas, sans-serif"
fontSize: 14px
fontWeight: 500
lineHeight: 1.35
letterSpacing: 0.16px
legal:
fontFamily: "Haas, sans-serif"
fontSize: 13.12px
fontWeight: 600
lineHeight: 1.2
letterSpacing: 0
pricing-display:
fontFamily: "Inter Display, system-ui, sans-serif"
fontSize: 44.8px
fontWeight: 475
lineHeight: 1.1
letterSpacing: 0
pricing-section:
fontFamily: "Inter Display, system-ui, sans-serif"
fontSize: 28px
fontWeight: 475
lineHeight: 1.2
letterSpacing: 0
pricing-card-title:
fontFamily: "Inter Display, system-ui, sans-serif"
fontSize: 20px
fontWeight: 475
lineHeight: 1.3
letterSpacing: 0
rounded:
xs: 2px
sm: 6px
md: 10px
lg: 12px
pill: 9999px
full: 9999px
spacing:
xxs: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 32px
xxl: 48px
section: 96px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button}"
rounded: "{rounded.lg}"
padding: 16px 24px
button-primary-active:
backgroundColor: "{colors.primary-active}"
textColor: "{colors.on-primary}"
rounded: "{rounded.lg}"
button-secondary:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.lg}"
padding: 16px 24px
button-secondary-on-dark:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.lg}"
padding: 16px 24px
button-legal:
backgroundColor: "{colors.link}"
textColor: "{colors.on-primary}"
typography: "{typography.legal}"
rounded: "{rounded.xs}"
padding: 12px 10px
button-icon-circular:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
rounded: "{rounded.full}"
size: 40px
button-pricing-pill:
backgroundColor: "{colors.canvas}"
textColor: "{colors.pricing-ink}"
typography: "{typography.button}"
rounded: "{rounded.pill}"
padding: 12px 24px
text-link:
backgroundColor: transparent
textColor: "{colors.link}"
typography: "{typography.body-md}"
top-nav:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
height: 64px
hero-band:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.display-lg}"
padding: 96px
signature-coral-card:
backgroundColor: "{colors.signature-coral}"
textColor: "{colors.on-primary}"
typography: "{typography.display-md}"
rounded: "{rounded.lg}"
padding: 48px
signature-forest-card:
backgroundColor: "{colors.signature-forest}"
textColor: "{colors.on-primary}"
typography: "{typography.display-md}"
rounded: "{rounded.lg}"
padding: 48px
hero-card-dark:
backgroundColor: "{colors.surface-dark}"
textColor: "{colors.on-dark}"
typography: "{typography.display-md}"
rounded: "{rounded.lg}"
padding: 48px
feature-card-tabbed:
backgroundColor: "{colors.surface-soft}"
textColor: "{colors.ink}"
typography: "{typography.title-lg}"
rounded: "{rounded.lg}"
padding: 32px
cream-callout-card:
backgroundColor: "{colors.signature-cream}"
textColor: "{colors.ink}"
typography: "{typography.title-lg}"
rounded: "{rounded.md}"
padding: 24px
demo-grid-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.label-md}"
rounded: "{rounded.md}"
padding: 16px
logo-strip:
backgroundColor: "{colors.canvas}"
textColor: "{colors.muted}"
typography: "{typography.body-md}"
padding: 32px
article-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.title-sm}"
rounded: "{rounded.md}"
padding: 16px
topic-filter-rail:
backgroundColor: "{colors.canvas}"
textColor: "{colors.body}"
typography: "{typography.body-md}"
width: 240px
text-input:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.sm}"
padding: 12px 16px
height: 44px
text-input-focus:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
rounded: "{rounded.sm}"
pricing-tier-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.pricing-ink}"
typography: "{typography.pricing-card-title}"
rounded: "{rounded.md}"
padding: 32px
pricing-tier-card-featured:
backgroundColor: "{colors.surface-soft}"
textColor: "{colors.pricing-ink}"
typography: "{typography.pricing-card-title}"
rounded: "{rounded.md}"
padding: 32px
pricing-comparison-row:
backgroundColor: "{colors.canvas}"
textColor: "{colors.body}"
typography: "{typography.body-md}"
padding: 12px
cta-band-light:
backgroundColor: "{colors.surface-strong}"
textColor: "{colors.ink}"
typography: "{typography.display-md}"
rounded: "{rounded.lg}"
padding: 48px
footer:
backgroundColor: "{colors.canvas}"
textColor: "{colors.body}"
typography: "{typography.body-md}"
padding: 64px
---
## Overview
Airtable's marketing surfaces are quietly editorial. The base atmosphere is white canvas, dark ink type, generous whitespace, and a near-black pill CTA — nothing is fighting for attention until a section needs to. The brand voltage doesn't come from gradient washes or accent walls; it comes from **full-bleed signature cards** in `{colors.signature-coral}`, `{colors.signature-forest}`, and `{colors.surface-dark}` that punctuate long-scroll explainer pages every two or three screens. Between those signature bands, the page reads like a print magazine: a headline, supporting copy, a small image cluster, then breathing room.
Type voice is Haas Grotesk at modest weights (400 for display, 500 for sub-titles and buttons). Display headlines never go bolder than 500 — emphasis comes from size and color contrast, not from weight. Body copy stays at 14px / 400 throughout. The pricing surface runs its own dialect: **Inter Display** at unusual mid-weights (475 / 575) and **pill-shaped buttons** (`{rounded.pill}`) that don't appear on any other page — a deliberate sub-system signaling "this page is about commercial precision."
**Key Characteristics:**
- Primary CTA is `{colors.primary}` (near-black ink) with white text and a `{rounded.lg}` (12px) corner — it reads as confident and final, never decorative.
- Secondary CTA is a `{colors.canvas}` button with `{colors.ink}` text and a hairline outline. The two together form Airtable's signature button pair.
- Hero is white canvas. There is no atmospheric gradient, no mesh, no background flourish. The brand strength comes from the type and the buttons sitting in clean whitespace.
- Brand voltage lives in **signature surface cards**: `{colors.signature-coral}`, `{colors.signature-forest}`, and `{colors.surface-dark}` carry full-bleed product callouts every few screens.
- Demo-card grids carry product UI fragments on `{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-cream}` and other warm pastel surfaces.
- Section rhythm: white canvas → coral signature card → white body → cream callout band → dark navy CTA → light gray CTA banner → footer. The canvas resets between every signature surface.
- Border radius is hierarchical: `{rounded.lg}` (12px) for primary CTAs and large signature cards, `{rounded.md}` (10px) for content cards and demo grids, `{rounded.sm}` (6px) for inputs, `{rounded.full}` for icon buttons. Pricing buttons jump to `{rounded.pill}` to mark themselves as a separate dialect.
- Vertical rhythm is `{spacing.section}` (96px) between major bands — universal across every page.
## Colors
### Brand & Accent
- **Primary** (`{colors.primary}`#181d26): The dominant brand color. Used for the primary CTA background, h1/h2 display type, and the `{component.surface-dark}` band. Not "blue, then black" — black IS the primary throughout the marketing system.
- **Primary Active** (`{colors.primary-active}`#0d1218): The press state on primary buttons.
### Surface
- **Canvas** (`{colors.canvas}`#ffffff): The default page surface; the floor of every editorial body.
- **Surface Soft** (`{colors.surface-soft}`#f8fafc): Tabbed feature cards and the featured pricing tier.
- **Surface Strong** (`{colors.surface-strong}`#e0e2e6): The light gray "Start building with Airtable" CTA banner near the footer.
- **Surface Dark** (`{colors.surface-dark}`#181d26): The dark navy CTA cards used mid-page (for example "The path to 10× every person in your organization").
- **Surface Dark Elevated** (`{colors.surface-dark-elevated}`#1d1f25): The articles-page hero base behind the rainbow-stripe overlay.
- **Hairline** (`{colors.hairline}`#dddddd): The 1px border tone for input outlines, table dividers, secondary-button outlines.
### Text
- **Ink** (`{colors.ink}`#181d26): The strongest text — h1/h2 display type and primary button text-on-light. Same hex as `{colors.primary}` because they are the same role expressed at type and button layers.
- **Body** (`{colors.body}`#333840): The default running-text color.
- **Muted** (`{colors.muted}`#41454d): Footer links, breadcrumbs, captions.
- **Border Strong** (`{colors.border-strong}`#9297a0): The 1px outline color on disabled secondary buttons.
- **On Primary / On Dark** (`{colors.on-primary}`#ffffff): The text color on primary buttons and dark surfaces.
### Signature Card Surfaces
These are the colors that carry Airtable's brand voltage. They appear as full-bleed, full-card surfaces — never as accents on a small element.
- **Coral** (`{colors.signature-coral}`#aa2d00): The largest signature card on the homepage ("Production apps in prototype speed"). Full-bleed dark coral with white type.
- **Forest** (`{colors.signature-forest}`#0a2e0e): A deep-green signature card used in the homepage demo-grid cluster.
- **Cream** (`{colors.signature-cream}`#f5e9d4): The cream callout band ("The path to 10× every person in your organization") — a soft beige surface holding dark type and product UI fragments.
- **Peach** (`{colors.signature-peach}`#fcab79), **Mint** (`{colors.signature-mint}`#a8d8c4), **Yellow** (`{colors.signature-yellow}`#f4d35e), **Mustard** (`{colors.signature-mustard}`#d9a441): Demo-card surfaces that carry small product UI fragments inside the multi-card grid sections.
### Semantic
- **Link** (`{colors.link}`#1b61c9): Inline body links and anchor text. Darker on press to `{colors.link-active}` (#1a3866). Despite the `--theme_button-background-primary` CSS-variable name, this color is **not** the primary button color — it is the link color.
- **Info** (`{colors.info}`#254fad) and **Info Border** (`{colors.info-border}`#458fff): Inline info badges and focused-input outline.
- **Success** (`{colors.success}`#006400) and **Success Border** (`{colors.success-border}`#39bf45): Confirmation states.
## Typography
### Font Family
The system runs **Haas / Haas Groot Disp** (Airtable's licensed display + text type). Haas Groot Disp covers display sizes (h1 / h2); Haas Grotesk covers everything 24px and below. The fallback stack walks `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif`.
The pricing surface runs a separate **Inter Display** stack at mid-weights (475 / 575) — a deliberate sub-system signaling commercial precision.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.display-xl}` | 48px | 500 | 1.1 | 0 | Articles page h2 — second-tier editorial headline |
| `{typography.display-lg}` | 40px | 400 | 1.2 | 0 | Homepage h1 hero |
| `{typography.display-md}` | 32px | 400 | 1.2 | 0 | Platform-page h2 — feature-section headlines |
| `{typography.title-lg}` | 24px | 400 | 1.35 | 0.12px | Section titles |
| `{typography.title-md}` | 20px | 400 | 1.5 | 0 | Sub-section titles in tabbed feature cards |
| `{typography.title-sm}` | 18px | 500 | 1.4 | 0 | Article-card titles |
| `{typography.label-md}` | 16px | 500 | 1.4 | 0 | Demo-card titles, list labels |
| `{typography.button}` | 16px | 500 | 1.4 | 0 | Standard CTA button labels |
| `{typography.body-md}` | 14px | 400 | 1.25 | 0 | Body copy, footer links, top-nav items |
| `{typography.caption}` | 14px | 500 | 1.35 | 0.16px | Light captions and meta text |
| `{typography.legal}` | 13.12px | 600 | 1.2 | 0 | Cookie/legal CTA buttons |
| `{typography.pricing-display}` | 44.8px | 475 | 1.1 | 0 | Pricing-page h1 |
| `{typography.pricing-section}` | 28px | 475 | 1.2 | 0 | Pricing-page section heads |
| `{typography.pricing-card-title}` | 20px | 475 | 1.3 | 0 | Pricing tier card plan name |
### Principles
The Haas system prefers weight 400 for display sizes — a 40px h1 is **not** bold. Visual emphasis is delegated to size, color contrast, and the signature surface cards. Where the system does want weight, it pivots to 500 (sub-titles, buttons, article titles), never 600 or 700 in the editorial body. The only true bold (600) lives in `{typography.legal}` — a sign that boldness is reserved for terms-of-service surfaces, not marketing.
The pricing-page sub-system uses Inter Display at `font-weight: 475` — a custom mid-weight between regular (400) and medium (500), shipped as a variable font.
### Note on Font Substitutes
If Haas Groot Disp and Haas Grotesk are unavailable, **Inter Display** (variable) is the closest open-source substitute for both — adjust line-height down by ~5% to match Haas's tighter cap-height. For the pricing sub-system, use Inter Display directly. On macOS / iOS, **system-ui** is sufficient; on Windows, the chain falls through to Segoe UI, which is a usable but slightly cooler substitute.
## Layout
### Spacing System
- **Base unit:** 4px (all spacing snaps to 4-multiples).
- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 96px.
- **Section padding (vertical):** `{spacing.section}` (96px) is the universal vertical rhythm constant — every major editorial band on every page uses 96px top + 96px bottom internal padding.
- **Card internal padding:** `{spacing.xl}` (32px) for tabbed feature cards and pricing tier cards; `{spacing.xxl}` (48px) inside signature coral / forest / dark cards; `{spacing.lg}` (24px) for cream callouts and demo-grid cards.
- **Gutters:** `{spacing.lg}` (24px) between cards in 3-up grids; `{spacing.md}` (16px) inside denser logo strips and footer column gutters.
### Grid & Container
- **Max content width:** ~1280px centered, with `{spacing.xxl}` (48px) horizontal breathing room.
- **Editorial body:** Single 8/12-column at large breakpoints, collapsing to single-column on mobile.
- **Demo-card grids:** 3 or 4 columns at desktop, 2 at tablet, 1 at mobile. Card sizes are deliberately uneven within the grid to dodge a uniform "spec sheet" feel.
- **Logo strip:** 6 monochrome partner logos in a single row at desktop; wraps to 3-up on mobile.
### Whitespace Philosophy
Airtable uses whitespace as the dominant atmospheric tool. Hero sections sit in 96px+ of pure whitespace above and below the headline + sub-headline pair, with no decoration in that whitespace. The hero is intentionally calm — there is no gradient, no aurora, no atmospheric mesh behind the type. The system trusts whitespace alone to do the framing.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| Flat | No shadow, no border | Body sections, top nav, footer |
| Soft hairline | 1px `{colors.hairline}` border | Inputs, sub-nav rails, comparison-table dividers, secondary buttons |
| Button rest | Soft drop with subtle blue-tinted glow at low alpha | Primary CTA buttons (the blue tint is a holdover from the link color and reads as a faint accent under the dark button) |
| Button focus | Outer 2px blue ring at higher alpha | Keyboard focus state on primary buttons |
| Card flat | No shadow; relies on color contrast against the surface band | Signature coral / forest / dark cards, cream callouts, demo-grid cards |
The elevation philosophy is **color-block first, shadow second**. Shadows are minimal; depth is delegated to the contrast between white canvas and signature surface cards. There is no soft-glow / atmospheric-shadow / heavy-elevation language anywhere in the marketing system.
### Decorative Depth
- **Vertical rainbow stripes** appear on the articles hero only — multi-color vertical bands sitting on `{colors.surface-dark-elevated}`. This is a single-page treatment, not a system-wide signature.
- **Photography-as-depth** in the demo-card grid: every card carries a real product UI screenshot or mockup, contributing depth through legible artifact density rather than decorative effects.
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| `{rounded.xs}` | 2px | Cookie-consent and legal CTA buttons — system-required surfaces |
| `{rounded.sm}` | 6px | Text inputs, small inline buttons |
| `{rounded.md}` | 10px | Secondary content cards, article cards, cream callouts |
| `{rounded.lg}` | 12px | Primary CTA buttons, signature surface cards, tabbed feature cards |
| `{rounded.pill}` | 9999px | Pricing-page CTA buttons (sub-system only) |
| `{rounded.full}` | 9999px / 50% | Circular icon buttons, avatar surfaces |
### Photography Geometry
Product UI screenshots inside demo-card grids retain native aspect ratios (typically 4:3 or 16:10) and crop into `{rounded.md}` containers. Hero illustrations bleed full-width with no rounding. Article-card thumbnails use 16:9 with `{rounded.md}` corners. Avatars in testimonials use `{rounded.full}` (perfect circles). Pricing comparison table images stay rectangular with no rounding.
## Components
> **No hover states documented.** Per the global no-hover policy (Step 6), every component spec below documents only Default and Active/Pressed states. Variants live as separate entries in the `components:` front matter.
**`top-nav`** — A 64px-tall white bar pinned to the top of every page. Airtable wordmark sits at left; primary horizontal menu (Platform, Solutions, Resources, Enterprise, Pricing) sits center-left in `{typography.body-md}`; the right cluster carries a "Book Demo" outline link, "Sign up for free" `{component.button-primary}`, and "Log In" text link. The nav stays light on every page — Airtable does not invert the nav over dark sections.
### Buttons
**`button-primary`** — The signature primary CTA. Background `{colors.primary}` (near-black), text `{colors.on-primary}`, type `{typography.button}`, padding 16px × 24px, rounded `{rounded.lg}` (12px). This is the "Get started for free" / "Sign up for free" button visible on every hero. It reads as confident and final — not decorative — which is why the system uses it sparingly (one per viewport).
- Active state: `button-primary-active` darkens to `{colors.primary-active}` (#0d1218).
**`button-secondary`** — White outline button (e.g. "Book demo"). Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.button}`, rounded `{rounded.lg}` (12px), 1px hairline outline. Sits next to `{component.button-primary}` as the "less-committed" choice.
**`button-secondary-on-dark`** — Same shape as `{component.button-secondary}` but used on signature coral / forest / dark surfaces. Background `{colors.canvas}`, text `{colors.ink}` — the white button stays white over dark surfaces because the system never inverts to a translucent on-dark style on the marketing site.
**`button-pricing-pill`** — The pricing-page CTA family. Background `{colors.canvas}`, text `{colors.pricing-ink}`, rounded `{rounded.pill}` (9999px), padding 12px × 24px. The only place pill-shape appears in the marketing system. Treat it as part of the pricing sub-system signaling.
**`button-legal`** — Cookie-consent and legal-banner CTAs. Background `{colors.link}`, text `{colors.on-primary}`, type `{typography.legal}` (13.12px / 600), rounded `{rounded.xs}` (2px), padding 12px × 10px. The 2px corner radius and 600 weight signal "this is a required system surface," not a designed brand surface.
**`button-icon-circular`** — 40px × 40px circular button with `{colors.canvas}` background, hairline border, and `{colors.ink}` icon. Used for carousel controls, "share", and "back" affordances.
**`text-link`** — Inline body links in `{colors.link}` (#1b61c9, the actual link blue). No underline by default. Type inherits `{typography.body-md}`.
### Cards & Containers
**`hero-band`** — The full-page-width white-canvas hero. No surface card, no border, no shadow, no atmospheric gradient — just the headline, sub-headline, and primary + secondary button pair sitting in 96px of whitespace. Vertical padding `{spacing.section}` (96px).
**`signature-coral-card`** — The large full-bleed coral card on the homepage ("Production apps in prototype speed"). Background `{colors.signature-coral}` (#aa2d00, a dark coral / oxide red), text `{colors.on-primary}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xxl}` (48px). Carries an h2 in `{typography.display-md}`, supporting copy in `{typography.body-md}`, and `{component.button-secondary-on-dark}` as the CTA.
**`signature-forest-card`** — A deep green signature card (`{colors.signature-forest}`#0a2e0e) used as a demo-grid sibling to the coral card on the homepage.
**`hero-card-dark`** — The dark navy mid-page CTA card (e.g. "The path to 10× every person in your organization"). Background `{colors.surface-dark}` (#181d26), text `{colors.on-dark}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xxl}` (48px). The same color as `{colors.primary}` because the system uses ink as both type color and signature dark surface.
**`feature-card-tabbed`** — Light-cream cards (e.g. the "Coke / Pelosi / Conde Nast / Time Inc" tabbed feature card on the homepage). Background `{colors.surface-soft}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xl}` (32px). Left rail carries vertically-stacked tab labels in `{typography.title-md}`; right pane shows the active tab's content (illustration + body copy + small CTA).
**`cream-callout-card`** — Beige callout cards (`{colors.signature-cream}`). Rounded `{rounded.md}` (10px), internal padding `{spacing.lg}` (24px). Carry product UI fragments or stat callouts — softer than the dark/coral signature cards but still a deliberate brand surface.
**`demo-grid-card`** — Used in multi-card grids that punctuate every page. Background `{colors.canvas}` or one of the demo-grid surfaces (`{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-yellow}`, `{colors.signature-mustard}`), rounded `{rounded.md}` (10px), internal padding `{spacing.md}` (16px). Each card frames a product UI fragment. Card heights vary deliberately to dodge a uniform "spec sheet" feel.
**`logo-strip`** — Horizontal monochrome partner-logo row (HBO, Netflix, Amazon, Time, Conde Nast). Logos render in `{colors.muted}`, surface is `{colors.canvas}`, vertical padding `{spacing.xl}` (32px). 6 logos at desktop, 3 at mobile.
**`article-card`** — The trending-stories grid on the articles page. Background `{colors.canvas}`, rounded `{rounded.md}` (10px), internal padding `{spacing.md}` (16px). Each card carries a colorful illustrated thumbnail (16:9), a small uppercase category tag, an `{typography.title-sm}` title, and a meta line. 3-up at desktop.
**`topic-filter-rail`** — The left rail on the articles page. 240px wide, `{colors.canvas}` background, `{typography.body-md}`, vertically grouped category headings ("Marketing", "Product", "Project management", "Operations") with sub-bullets. Active item carries a small numeric count badge.
### Inputs & Forms
**`text-input`** — Standard text input. Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body-md}`, rounded `{rounded.sm}` (6px), padding 12px × 16px, height 44px. 1px hairline border in `{colors.hairline}`.
**`text-input-focus`** — Focus state. Border thickens or recolors to `{colors.info-border}`.
### Pricing Sub-System
**`pricing-tier-card`** — Standard tier card. Background `{colors.canvas}`, text `{colors.pricing-ink}`, type `{typography.pricing-card-title}` for the plan name, rounded `{rounded.md}` (10px), internal padding `{spacing.xl}` (32px). Carries the plan name, a price block in `{typography.pricing-display}` (44.8px / 475), feature checklist, and a `{component.button-pricing-pill}` at the bottom.
**`pricing-tier-card-featured`** — The featured tier (typically "Team" or "Business"). Background shifts to `{colors.surface-soft}`. No accent border, no badge — the background tone shift is the only signal.
**`pricing-comparison-row`** — Each row of the long comparison table at the bottom of the pricing page. Labels in the left column; checkmarks or values across 4 plan columns. 12px vertical padding per row, hairline divider between rows.
### Navigation Variants
**`footer`** — Light surface (`{colors.canvas}`), 6-column link list at desktop covering Platform / Solutions / Resources / Learn / Company sub-trees. Vertical padding `{spacing.section}` divided across upper link block and lower legal row. Type `{typography.body-md}`.
**`cta-band-light`** — The light gray "Start building with Airtable" CTA strip near the footer. Background `{colors.surface-strong}` (#e0e2e6), text `{colors.ink}`, rounded `{rounded.lg}` (12px), padding `{spacing.xxl}` (48px). Carries an h2 in `{typography.display-md}` and a `{component.button-primary}`.
### Signature Components
**Articles Vertical Rainbow Stripe Hero** — The articles-page hero treatment. Multi-color vertical bands at varying widths sitting on `{colors.surface-dark-elevated}`. The h1 + sub-head + CTA cluster sits center-left on top of the stripes. This is a single-page hero treatment, not a system-wide signature — do not promote it to a multi-page pattern.
## Do's and Don'ts
### Do
- Keep `{component.button-primary}` near-black. The brand's primary CTA is `{colors.primary}`, not the link blue. Mixing them up turns a confident hero into a confused one.
- Reserve `{component.button-primary}` for one primary action per viewport. The system is designed for scarcity at the brand-action layer.
- Use `{component.button-secondary}` (white with hairline outline) as the natural pair with `{component.button-primary}`. The two together form Airtable's signature button row.
- Trust whitespace as the hero atmosphere. Hero bands are intentionally calm — no gradient, no mesh, no atmospheric backdrop. Going against this reads as off-brand.
- Use `{component.signature-coral-card}`, `{component.signature-forest-card}`, and `{component.hero-card-dark}` to break editorial monotony. These are the brand's voltage moments.
- Keep `{component.demo-grid-card}` heights uneven within a grid. Uniform heights feel like a spec sheet.
- Treat the pricing surface as its own dialect: keep `{typography.pricing-display}`, `{typography.pricing-card-title}`, and `{component.button-pricing-pill}` together. Mixing them with Haas Grotesk button type breaks the sub-system's voice.
- Anchor every editorial band with `{spacing.section}` (96px) vertical padding.
### Don't
- Don't make `{colors.link}` (#1b61c9) the primary button color. It is the link color. The primary button is `{colors.primary}` (#181d26, near-black). Treating link-blue as the brand action is the most common mistake when reading Airtable's CSS variables.
- Don't add a gradient backdrop to the hero. Airtable's hero is white, full stop. Mesh, aurora, spotlight gradients all read as "another SaaS template" — not Airtable.
- Don't bold display-weight type. `{typography.display-xl}` and `{typography.display-lg}` are intentionally weight 400 / 500 — going to 700 reads as marketing-page-template.
- Don't use `{rounded.pill}` outside the pricing surface. It's a sub-system signal, not a general radius option.
- Don't repeat the same surface mode in two consecutive bands. The editorial pacing depends on rhythm: white → signature card → white → cream → dark → white. Two whites in a row read as a typography blog.
- Don't add hover state styling beyond what the system already encodes. The system documents Default and Active/Pressed only.
- Don't introduce additional accent colors beyond the documented signature card palette. The system's voltage already uses coral, forest, dark navy, cream, peach, mint, yellow, and mustard.
## Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 768px | Single-column body; top nav collapses to hamburger; demo-grid drops to 1-up; signature cards stay full-bleed; logo strip wraps to 2 rows; footer collapses to single-column |
| Tablet | 7681024px | 2-up demo-grid; top nav stays horizontal but tightens; cream-callout cards stack 2-up; pricing comparison table becomes horizontally scrollable |
| Desktop | 10241440px | 3-up demo-grid (and 4-up for tighter content); full top-nav with all menu items visible; pricing tier cards render 4-across |
| Wide | > 1440px | Same as Desktop with more outer breathing room; max content width caps at ~1280px and the page adds outer margin rather than scaling type up |
### Touch Targets
- `{component.button-primary}` and siblings render at 48 × 48px minimum (16px vertical padding + 16px line-height) — comfortably above WCAG AAA's 44 × 44.
- `{component.button-icon-circular}` is exactly 40 × 40px — slightly under WCAG's recommended 44, but the centered icon and dot-radius compensate visually.
- `{component.text-input}` height is 44px.
### Collapsing Strategy
- Top nav collapses to a hamburger at < 768px; the menu opens as a full-screen sheet rather than a dropdown.
- Card grids reduce columns rather than scaling cards down.
- The `{component.feature-card-tabbed}` re-stacks the tab rail above the content pane on mobile.
- The pricing comparison table converts to horizontally-scrollable swipe at < 1024px; the four plan headers stay visible while body rows scroll.
### Image Behavior
- Demo-card UI screenshots crop to fit their container rather than scaling up.
- Hero illustrations bleed full-width on mobile, losing horizontal margin.
- Signature card images (inside coral / forest / dark cards) compress to their card width without cropping.
## Iteration Guide
1. Focus on ONE component at a time. Reference its YAML key directly (`{component.button-primary}`, `{component.signature-coral-card}`).
2. When adding a new component, decide first which sub-system it belongs to: the main editorial system (Haas, `{rounded.lg}`/`{rounded.md}`) or the pricing sub-system (Inter Display, `{rounded.pill}`).
3. Variants of an existing component (`-active`, `-disabled`, `-focus`) live as separate entries in `components:` — never as nested state objects.
4. Use `{token.refs}` everywhere prose mentions a color, a radius, a typography role, or a spacing value. Hex codes appear at most once next to the reference.
5. Never document hover. The system documents Default and Active/Pressed states only.
6. Run `npx @google/design.md lint DESIGN.md` after edits — `broken-ref`, `contrast-ratio`, and `orphaned-tokens` warnings flag issues automatically.
7. When in doubt about emphasis: bigger type before bolder type, signature surface card before solid accent.
## Known Gaps
- The exact hex values of pastel demo-grid surfaces (`{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-yellow}`, `{colors.signature-mustard}`) are inferred from screenshot pixel sampling. Some product launches may swap these surfaces seasonally.
- Hover behavior across all components is not documented (per global no-hover policy).
- Animation and transition timings are not in scope.
- Form validation states beyond `text-input-focus` are not extracted — error and success states for inputs would need a dedicated form page to confirm.
- The pricing comparison table's checkmark glyph and column-divider widths are described structurally but not formalized as tokens.
- The CSS variable `--theme_button-background-primary: #1b61c9` exists at `:root` but is not used as the primary CTA color anywhere on the marketing site. It maps to the link/info color role instead. Documented here so future extractions don't re-trip over the misleading variable name.

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"palette": [
"#181d26",
"#0d1218",
"#333840",
"#41454d",
"#dddddd",
"#9297a0",
"#ffffff",
"#f8fafc",
"#e0e2e6",
"#1d1f25",
"#aa2d00",
"#0a2e0e"
],
"typography": {
"display": "Haas Groot Disp",
"body": "Haas",
"mono": "ui-monospace",
"hints": [
"Haas Groot Disp",
"Haas",
"ui-monospace",
"Inter Display",
"Inter",
"Roboto"
]
},
"sourceCategory": "productivity-saas",
"radius": {
"control": "6px",
"card": "12px",
"preview": "12px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "4px",
"xs": "8px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "96px",
"source": "design-md"
}
}

View File

@@ -0,0 +1,41 @@
/**
* @name Airtable 主题 - Airtable
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/official-homepage.webp?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,34 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Airtable. */
.dmb-page {
--dmb-accent: #181d26;
--dmb-accent-contrast: #ffffff;
--dmb-link: #0d1218;
--dmb-muted: #333840;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-radius-xs: 6px;
--dmb-radius-control: 6px;
--dmb-radius-card: 12px;
--dmb-radius-preview: 12px;
--dmb-radius-pill: 9999px;
--dmb-border: #dddddd;
--dmb-border-sample-bg: #f8fafc;
--dmb-spacing-xxs: 4px;
--dmb-spacing-xs: 8px;
--dmb-spacing-sm: 12px;
--dmb-spacing-md: 16px;
--dmb-spacing-lg: 24px;
--dmb-spacing-xl: 32px;
--dmb-spacing-xxl: 48px;
--dmb-spacing-section: 96px;
--dmb-font-display: "Haas Groot Disp", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Haas", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,310 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "getdesign.md",
"originalDetailUrl": "https://getdesign.md/airtable/design-md",
"fallbackRawUrl": "https://raw.githubusercontent.com/VoltAgent/awesome-design-md/main/design-md/airtable/DESIGN.md",
"mirrorSource": "github:VoltAgent/awesome-design-md",
"websiteUrl": "https://airtable.com/"
},
"identity": {
"id": "P0-61",
"slug": "airtable",
"brand": "Airtable",
"titleZh": "Airtable 主题",
"titleEn": "Airtable",
"descriptionZh": "Airtable 的 Design.md 主题展示围绕媒体内容、SaaS 产品、开发工具、浅色界面组织页面。",
"descriptionEn": "Spreadsheet-database hybrid. Colorful, friendly, structured data aesthetic."
},
"tags": {
"sourceTags": [],
"generatedTags": [
"request-flow",
"media"
],
"designTags": [],
"distributionTags": [
"媒体内容",
"SaaS 产品",
"开发工具",
"浅色界面",
"B2B 产品",
"效率工具",
"专业工具"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"sourcePath": ".local/design-md-batch-regression/getdesign/airtable/DESIGN.md"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/getdesign/airtable/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": null,
"previewHtml": {
"type": "local-preview",
"path": "assets/official-homepage.webp",
"publicPath": "assets/official-homepage.webp",
"importPath": "./assets/official-homepage.webp?url",
"sourcePath": "assets/official-homepage.webp"
}
},
"tokens": {
"palette": [
"#181d26",
"#0d1218",
"#333840",
"#41454d",
"#dddddd",
"#9297a0",
"#ffffff",
"#f8fafc",
"#e0e2e6",
"#1d1f25",
"#aa2d00",
"#0a2e0e"
],
"typography": {
"display": "Haas Groot Disp",
"body": "Haas",
"mono": "ui-monospace",
"hints": [
"Haas Groot Disp",
"Haas",
"ui-monospace",
"Inter Display",
"Inter",
"Roboto"
]
},
"radius": {
"control": "6px",
"card": "12px",
"preview": "12px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "4px",
"xs": "8px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "96px",
"source": "design-md"
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/official-homepage.webp"
}
],
"display": {
"brand": "Airtable 主题",
"brandAlias": "Airtable",
"description": "Airtable 的 Design.md 主题展示围绕媒体内容、SaaS 产品、开发工具、浅色界面组织页面。",
"descriptionEn": "Spreadsheet-database hybrid. Colorful, friendly, structured data aesthetic.",
"variant": "saas-devtool",
"distributionTags": [
"媒体内容",
"SaaS 产品",
"开发工具",
"浅色界面",
"B2B 产品",
"效率工具",
"专业工具"
],
"fontStylesheets": [],
"palette": [
{
"color": "#181d26",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#0d1218",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#333840",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#ffffff"
},
{
"color": "#41454d",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#ffffff"
},
{
"color": "#dddddd",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#9297a0",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#171717"
},
{
"color": "#ffffff",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#171717"
},
{
"color": "#f8fafc",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#171717"
},
{
"color": "#e0e2e6",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#171717"
},
{
"color": "#1d1f25",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#aa2d00",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#ffffff"
},
{
"color": "#0a2e0e",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#ffffff"
}
],
"shadows": [],
"borders": [
{
"label": "边框 1 - Border 1",
"value": "#dddddd",
"cssValue": "#dddddd",
"description": "hairline: \"#dddddd\""
},
{
"label": "边框 2 - Border 2",
"value": "#458fff",
"cssValue": "#458fff",
"description": "info-border: \"#458fff\""
},
{
"label": "边框 3 - Border 3",
"value": "#39bf45",
"cssValue": "#39bf45",
"description": "success-border: \"#39bf45\""
},
{
"label": "边框 4 - Border 4",
"value": "#9297a0",
"cssValue": "#9297a0",
"description": "border-strong: \"#9297a0\""
},
{
"label": "边框 5 - Border 5",
"value": "#254fad",
"cssValue": "#254fad",
"description": "info: \"#254fad\""
},
{
"label": "边框 6 - Border 6",
"value": "#006400",
"cssValue": "#006400",
"description": "success: \"#006400\""
}
],
"typography": [
"Haas Groot Disp",
"Haas",
"ui-monospace",
"Inter Display",
"Inter",
"Roboto"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/official-homepage.webp"
}
],
"panels": [
{
"eyebrow": "产品",
"title": "开发工具界面",
"body": "适合工作台、文档、控制台和开发者产品。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议Keep {component.button-primary} near-black. The brand's primary CTA is {colors.primary}, not the link blue. Mixing them up turns a confident hero into a confused one。",
"建议Reserve {component.button-primary} for one primary action per viewport. The system is designed for scarcity at the brand-action layer。",
"建议Use {component.button-secondary} (white with hairline outline) as the natural pair with {component.button-primary}. The two together form Airtable's signature button row。",
"建议Trust whitespace as the hero atmosphere. Hero bands are intentionally calm — no gradient, no mesh, no atmospheric backdrop. Going against this reads as off-brand。",
"建议Use {component.signature-coral-card}, {component.signature-forest-card}, and {component.hero-card-dark} to break editorial monotony. These are the brand's voltage moments。"
],
"dont": [
"避免make {colors.link} (#1b61c9) the primary button color. It is the link color. The primary button is {colors.primary} (#181d26, near-black). Treating link-blue as the brand action is the most common mistake when reading Airtable's CSS variables。",
"避免add a gradient backdrop to the hero. Airtable's hero is white, full stop. Mesh, aurora, spotlight gradients all read as \"another SaaS template\" — not Airtable。",
"避免bold display-weight type. {typography.display-xl} and {typography.display-lg} are intentionally weight 400 / 500 — going to 700 reads as marketing-page-template。",
"避免use {rounded.pill} outside the pricing surface. It's a sub-system signal, not a general radius option。",
"避免repeat the same surface mode in two consecutive bands. The editorial pacing depends on rhythm: white → signature card → white → cream → dark → white. Two whites in a row read as a typography blog。"
]
},
"radius": {
"control": "6px",
"card": "12px",
"preview": "12px",
"pill": "9999px",
"source": "design-md"
},
"spacing": {
"xxs": "4px",
"xs": "8px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"xxl": "48px",
"section": "96px",
"source": "design-md"
}
},
"status": {
"displayPage": "doing",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Airtable. */

View File

@@ -0,0 +1,47 @@
# Alison Roman 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
Alison Roman 的 Design.md 主题展示,围绕暖色调、编辑/机构、浅色界面、媒体内容组织页面。
Explore Alison Roman's light Media design system: Parchment Cream #f6f0e1, Ink Black #290a08 colors, Jannon Neo, Modale Antique typography, and DESIGN.md...
## Tags
- 暖色调
- 编辑/机构
- 浅色界面
- 媒体内容
- 品牌展示
- 高级质感
- 内容出版
- 排版系统
- 品牌叙事
## Colors
- color-1: `#f6f0e1`
- color-2: `#290a08`
- color-3: `#e5e7eb`
- color-4: `#810c00`
- color-5: `#ffffff`
- color-6: `#fff3cc`
- color-7: `#fffaec`
- color-8: `#143930`
- color-9: `#f8f2de`
- color-10: `#456859`
- color-11: `#f8f5f5`
- color-12: `#cfc6c7`
## Typography
- display: Jannon Neo
- body: Modale Antique
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/b2ace2c1-d6ee-4d57-915e-901224cded11

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,27 @@
{
"palette": [
"#f6f0e1",
"#290a08",
"#e5e7eb",
"#810c00",
"#ffffff",
"#fff3cc",
"#fffaec",
"#143930",
"#f8f2de",
"#456859",
"#f8f5f5",
"#cfc6c7"
],
"typography": {
"display": "Jannon Neo",
"body": "Modale Antique",
"mono": "ui-monospace",
"hints": [
"Jannon Neo",
"Modale Antique",
"ui-monospace"
]
},
"sourceCategory": "Media"
}

View File

@@ -0,0 +1,41 @@
/**
* @name Alison Roman 主题 - Alison Roman
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Alison Roman. */
.dmb-page {
--dmb-accent: #f6f0e1;
--dmb-accent-contrast: #171717;
--dmb-link: #290a08;
--dmb-muted: #290a08;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Jannon Neo", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Modale Antique", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,250 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/b2ace2c1-d6ee-4d57-915e-901224cded11",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-004",
"slug": "alison-roman",
"brand": "Alison Roman",
"titleZh": "Alison Roman 主题",
"titleEn": "Alison Roman",
"descriptionZh": "Alison Roman 的 Design.md 主题展示,围绕暖色调、编辑/机构、浅色界面、媒体内容组织页面。",
"descriptionEn": "Explore Alison Roman's light Media design system: Parchment Cream #f6f0e1, Ink Black #290a08 colors, Jannon Neo, Modale Antique typography, and DESIGN.md..."
},
"tags": {
"sourceTags": [
"alison-roman-design-system",
"alison-roman-website-design",
"alison-roman-colors",
"alison-roman-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"media-design-system",
"parchment-cream",
"ink-black",
"paper-white",
"canvas-dust",
"jannon-neo",
"modale-antique",
"apple-system"
],
"generatedTags": [
"warm",
"editorial",
"image-assets",
"light",
"media",
"ai"
],
"designTags": [
"Ink Black",
"Paper White",
"Canvas Dust",
"light"
],
"distributionTags": [
"暖色调",
"编辑/机构",
"浅色界面",
"媒体内容",
"品牌展示",
"高级质感",
"内容出版",
"排版系统",
"品牌叙事"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/alison-roman/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/ae45e02c-c237-4235-b076-72821aff1d2c.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#f6f0e1",
"#290a08",
"#e5e7eb",
"#810c00",
"#ffffff",
"#fff3cc",
"#fffaec",
"#143930",
"#f8f2de",
"#456859",
"#f8f5f5",
"#cfc6c7"
],
"typography": {
"display": "Jannon Neo",
"body": "Modale Antique",
"mono": "ui-monospace",
"hints": [
"Jannon Neo",
"Modale Antique",
"ui-monospace"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "Alison Roman 主题",
"brandAlias": "Alison Roman",
"description": "Alison Roman 的 Design.md 主题展示,围绕暖色调、编辑/机构、浅色界面、媒体内容组织页面。",
"descriptionEn": "Explore Alison Roman's light Media design system: Parchment Cream #f6f0e1, Ink Black #290a08 colors, Jannon Neo, Modale Antique typography, and DESIGN.md...",
"variant": "editorial-agency",
"distributionTags": [
"暖色调",
"编辑/机构",
"浅色界面",
"媒体内容",
"品牌展示",
"高级质感",
"内容出版",
"排版系统",
"品牌叙事"
],
"fontStylesheets": [],
"palette": [
{
"color": "#f6f0e1",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#171717"
},
{
"color": "#290a08",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#e5e7eb",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#810c00",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#ffffff"
},
{
"color": "#ffffff",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#fff3cc",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#171717"
},
{
"color": "#fffaec",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#171717"
},
{
"color": "#143930",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#ffffff"
},
{
"color": "#f8f2de",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#171717"
},
{
"color": "#456859",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#f8f5f5",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#171717"
},
{
"color": "#cfc6c7",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#171717"
}
],
"shadows": [],
"borders": [],
"typography": [
"Jannon Neo",
"Modale Antique",
"ui-monospace"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "内容",
"title": "版式节奏突出",
"body": "适合编辑、机构、作品集和品牌叙事页面。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
]
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Alison Roman. */

View File

@@ -0,0 +1,46 @@
# Alpine Hearing Protection 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
Alpine Hearing Protection 的 Design.md 主题展示围绕浅色界面、电商零售、AI、SaaS 产品组织页面。
Explore Alpine Hearing Protection's light E-commerce design system: Inkwell #200e0e, Crimson Accent #ed212d colors, Antarctica, GTStandard-M typography, and...
## Tags
- 浅色界面
- 电商零售
- AI
- SaaS 产品
- 开发工具
- B2B 产品
- 效率工具
- 专业工具
## Colors
- color-1: `#200e0e`
- color-2: `#ed212d`
- color-3: `#ffffff`
- color-4: `#f3e7e2`
- color-5: `#f8f0ec`
- color-6: `#d2cfcf`
- color-7: `#fde3d6`
- color-8: `#000000`
- color-9: `#d9d9d9`
- color-10: `#202020`
- color-11: `#9ac9b5`
- color-12: `#dbb0b3`
## Typography
- display: Inter
- body: Inter
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/76c761d8-4af1-4ed5-ba93-eeb60f7006b5

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,26 @@
{
"palette": [
"#200e0e",
"#ed212d",
"#ffffff",
"#f3e7e2",
"#f8f0ec",
"#d2cfcf",
"#fde3d6",
"#000000",
"#d9d9d9",
"#202020",
"#9ac9b5",
"#dbb0b3"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
},
"sourceCategory": "E-commerce"
}

View File

@@ -0,0 +1,41 @@
/**
* @name Alpine Hearing Protection 主题 - Alpine Hearing Protection
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Alpine Hearing Protection. */
.dmb-page {
--dmb-accent: #200e0e;
--dmb-accent-contrast: #ffffff;
--dmb-link: #ed212d;
--dmb-muted: #ed212d;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,254 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/76c761d8-4af1-4ed5-ba93-eeb60f7006b5",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-003",
"slug": "alpine-hearing-protection",
"brand": "Alpine Hearing Protection",
"titleZh": "Alpine Hearing Protection 主题",
"titleEn": "Alpine Hearing Protection",
"descriptionZh": "Alpine Hearing Protection 的 Design.md 主题展示围绕浅色界面、电商零售、AI、SaaS 产品组织页面。",
"descriptionEn": "Explore Alpine Hearing Protection's light E-commerce design system: Inkwell #200e0e, Crimson Accent #ed212d colors, Antarctica, GTStandard-M typography, and..."
},
"tags": {
"sourceTags": [
"alpine-hearing-protection-design-system",
"alpine-hearing-protection-website-design",
"alpine-hearing-protection-colors",
"alpine-hearing-protection-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"e-commerce-design-system",
"inkwell",
"crimson-accent",
"pale-peach",
"sage-green",
"antarctica",
"gtstandard-m"
],
"generatedTags": [
"image-assets",
"light",
"e-commerce",
"ai"
],
"designTags": [
"Inkwell",
"Sage Green",
"light"
],
"distributionTags": [
"浅色界面",
"电商零售",
"AI",
"SaaS 产品",
"开发工具",
"B2B 产品",
"效率工具",
"专业工具"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/alpine-hearing-protection/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/cf220dd8-6640-42d6-b642-86e9cfa66a88.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#200e0e",
"#ed212d",
"#ffffff",
"#f3e7e2",
"#f8f0ec",
"#d2cfcf",
"#fde3d6",
"#000000",
"#d9d9d9",
"#202020",
"#9ac9b5",
"#dbb0b3"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "Alpine Hearing Protection 主题",
"brandAlias": "Alpine Hearing Protection",
"description": "Alpine Hearing Protection 的 Design.md 主题展示围绕浅色界面、电商零售、AI、SaaS 产品组织页面。",
"descriptionEn": "Explore Alpine Hearing Protection's light E-commerce design system: Inkwell #200e0e, Crimson Accent #ed212d colors, Antarctica, GTStandard-M typography, and...",
"variant": "saas-devtool",
"distributionTags": [
"浅色界面",
"电商零售",
"AI",
"SaaS 产品",
"开发工具",
"B2B 产品",
"效率工具",
"专业工具"
],
"fontStylesheets": [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
],
"palette": [
{
"color": "#200e0e",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#ed212d",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#ffffff",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#f3e7e2",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#171717"
},
{
"color": "#f8f0ec",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#d2cfcf",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#171717"
},
{
"color": "#fde3d6",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#171717"
},
{
"color": "#000000",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#ffffff"
},
{
"color": "#d9d9d9",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#171717"
},
{
"color": "#202020",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#9ac9b5",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#171717"
},
{
"color": "#dbb0b3",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#171717"
}
],
"shadows": [],
"borders": [],
"typography": [
"Inter",
"ui-monospace"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "产品",
"title": "开发工具界面",
"body": "适合工作台、文档、控制台和开发者产品。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议:基于 Refero 采集图和 metadata 还原 Alpine Hearing Protection 的颜色、字体和页面密度,优先服务 浅色界面、电商零售、AI 场景。",
"建议:保留 Alpine Hearing Protection 的来源标签和视觉 token不把它混入旧来源的泛化模板。"
],
"dont": [
"避免:不要把 Alpine Hearing Protection 套用到与 浅色界面、电商零售、AI 明显无关的产品语境。",
"避免:不要用通用行业模板覆盖 Alpine Hearing Protection 的采集图风格;需要保留原图里的版式密度、留白和视觉重心。"
]
}
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Alpine Hearing Protection. */

View File

@@ -0,0 +1,48 @@
# Altitude 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
Altitude 的 Design.md 主题展示围绕Web3、暗色界面、金融科技、AI组织页面。
Explore Altitude's dark Fintech design system: Deep Charcoal #181818, Graphite #262626 colors, Libre Baskerville, Inter typography, and DESIGN.md for AI agents.
## Tags
- Web3
- 暗色界面
- 金融科技
- AI
- 数据仪表盘
- 数据分析
- B2B 产品
- 运营监控
- 专业工具
- 高密度信息
## Colors
- color-1: `#181818`
- color-2: `#262626`
- color-3: `#eeeeee`
- color-4: `#ffffff`
- color-5: `#e4e4e4`
- color-6: `#323232`
- color-7: `#1f1f1f`
- color-8: `#e7e5e4`
- color-9: `#111111`
- color-10: `#5e5d59`
- color-11: `#a4a19b`
- color-12: `#585858`
## Typography
- display: Inter
- body: Inter
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/0e971626-ca51-45ba-acf6-35a53c561b2c

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,26 @@
{
"palette": [
"#181818",
"#262626",
"#eeeeee",
"#ffffff",
"#e4e4e4",
"#323232",
"#1f1f1f",
"#e7e5e4",
"#111111",
"#5e5d59",
"#a4a19b",
"#585858"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
},
"sourceCategory": "Fintech"
}

View File

@@ -0,0 +1,41 @@
/**
* @name Altitude 主题 - Altitude
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Altitude. */
.dmb-page {
--dmb-accent: #181818;
--dmb-accent-contrast: #ffffff;
--dmb-link: #262626;
--dmb-muted: #262626;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

View File

@@ -0,0 +1,261 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/0e971626-ca51-45ba-acf6-35a53c561b2c",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-009",
"slug": "altitude",
"brand": "Altitude",
"titleZh": "Altitude 主题",
"titleEn": "Altitude",
"descriptionZh": "Altitude 的 Design.md 主题展示围绕Web3、暗色界面、金融科技、AI组织页面。",
"descriptionEn": "Explore Altitude's dark Fintech design system: Deep Charcoal #181818, Graphite #262626 colors, Libre Baskerville, Inter typography, and DESIGN.md for AI agents."
},
"tags": {
"sourceTags": [
"altitude-design-system",
"altitude-website-design",
"altitude-colors",
"altitude-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"fintech-design-system",
"deep-charcoal",
"graphite",
"ghost-white",
"cloud-white",
"libre-baskerville",
"inter",
"fira-code"
],
"generatedTags": [
"web3",
"dark",
"fintech",
"image-assets",
"ai"
],
"designTags": [
"Deep Charcoal",
"Ghost White",
"Cloud White",
"dark"
],
"distributionTags": [
"Web3",
"暗色界面",
"金融科技",
"AI",
"数据仪表盘",
"数据分析",
"B2B 产品",
"运营监控",
"专业工具",
"高密度信息"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/altitude/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/8458bf09-ac50-451f-a92e-6fade0eff793.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#181818",
"#262626",
"#eeeeee",
"#ffffff",
"#e4e4e4",
"#323232",
"#1f1f1f",
"#e7e5e4",
"#111111",
"#5e5d59",
"#a4a19b",
"#585858"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "Altitude 主题",
"brandAlias": "Altitude",
"description": "Altitude 的 Design.md 主题展示围绕Web3、暗色界面、金融科技、AI组织页面。",
"descriptionEn": "Explore Altitude's dark Fintech design system: Deep Charcoal #181818, Graphite #262626 colors, Libre Baskerville, Inter typography, and DESIGN.md for AI agents.",
"variant": "dashboard",
"distributionTags": [
"Web3",
"暗色界面",
"金融科技",
"AI",
"数据仪表盘",
"数据分析",
"B2B 产品",
"运营监控",
"专业工具",
"高密度信息"
],
"fontStylesheets": [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
],
"palette": [
{
"color": "#181818",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#262626",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#ffffff"
},
{
"color": "#eeeeee",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#ffffff",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#171717"
},
{
"color": "#e4e4e4",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#323232",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#ffffff"
},
{
"color": "#1f1f1f",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#ffffff"
},
{
"color": "#e7e5e4",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#171717"
},
{
"color": "#111111",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#ffffff"
},
{
"color": "#5e5d59",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#a4a19b",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#171717"
},
{
"color": "#585858",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#ffffff"
}
],
"shadows": [],
"borders": [],
"typography": [
"Inter",
"ui-monospace"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "数据",
"title": "指标与状态清晰",
"body": "适合分析看板、金融科技、监控和运营页面。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议:基于 Refero 采集图和 metadata 还原 Altitude 的颜色、字体和页面密度,优先服务 Web3、暗色界面、金融科技 场景。",
"建议:保留 Altitude 的来源标签和视觉 token不把它混入旧来源的泛化模板。"
],
"dont": [
"避免:不要把 Altitude 套用到与 Web3、暗色界面、金融科技 明显无关的产品语境。",
"避免:不要用通用行业模板覆盖 Altitude 的采集图风格;需要保留原图里的版式密度、留白和视觉重心。"
]
}
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for Altitude. */

46
src/themes/amp/DESIGN.md Normal file
View File

@@ -0,0 +1,46 @@
# amp 主题
> This DESIGN.md was derived from collected metadata because no downloadable source DESIGN.md was available during import.
## Overview
amp 的 Design.md 主题展示围绕浅色界面、AI、暖色调、SaaS 产品组织页面。
Explore amp's light Other design system: Orange Ember #ff6105, Soft Peach #ffdfcd colors, PublicaSans typography, and DESIGN.md for AI agents.
## Tags
- 浅色界面
- AI
- 暖色调
- SaaS 产品
- 开发工具
- B2B 产品
- 效率工具
- 专业工具
## Colors
- color-1: `#ff6105`
- color-2: `#ffdfcd`
- color-3: `#e5e5e5`
- color-4: `#0a0a0a`
- color-5: `#ffffff`
- color-6: `#f3f4f3`
- color-7: `#3c3e3d`
- color-8: `#a89a8b`
- color-9: `#cccccc`
- color-10: `#292b2a`
- color-11: `#e5e7eb`
- color-12: `#fff8e9`
## Typography
- display: Inter
- body: Inter
- mono: ui-monospace
## Source Notes
- 来源styles.refero.design
- 详情页https://styles.refero.design/style/261a4ad3-e835-4f7a-beb7-72187f84d462

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,26 @@
{
"palette": [
"#ff6105",
"#ffdfcd",
"#e5e5e5",
"#0a0a0a",
"#ffffff",
"#f3f4f3",
"#3c3e3d",
"#a89a8b",
"#cccccc",
"#292b2a",
"#e5e7eb",
"#fff8e9"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
},
"sourceCategory": "Other"
}

41
src/themes/amp/index.tsx Normal file
View File

@@ -0,0 +1,41 @@
/**
* @name amp 主题 - amp
*/
import './style.css';
import React from 'react';
import { DesignMdBatchShowcase, type BatchShowcaseConfig } from '../../common/DesignMdBatchShowcase';
import themeData from './theme.json';
import previewAsset0 from './assets/source-preview.jpg?url';
type ThemeDisplayData = Omit<BatchShowcaseConfig, 'previewImages'> & {
previewImages: Array<{ type: string; path: string }>;
};
const display = themeData.display as ThemeDisplayData;
const config: BatchShowcaseConfig = {
brand: display.brand,
brandAlias: display.brandAlias,
source: themeData.source,
description: display.description,
descriptionEn: display.descriptionEn,
variant: display.variant,
distributionTags: display.distributionTags,
fontStylesheets: display.fontStylesheets,
palette: display.palette,
radius: display.radius,
spacing: display.spacing,
typography: display.typography,
previewImages: [
{ type: display.previewImages[0].type, url: previewAsset0 }
],
usageGuidance: display.usageGuidance,
shadows: display.shadows,
borders: display.borders,
panels: display.panels,
};
const Component: React.FC = () => <DesignMdBatchShowcase config={config} />;
export default Component;

19
src/themes/amp/style.css Normal file
View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for amp. */
.dmb-page {
--dmb-accent: #ff6105;
--dmb-accent-contrast: #ffffff;
--dmb-link: #ffdfcd;
--dmb-muted: #ffdfcd;
--dmb-bg: #ffffff;
--dmb-ink: #111318;
--dmb-ink-muted: #3f424b;
--dmb-ink-subtle: #686c76;
--dmb-font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--dmb-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

252
src/themes/amp/theme.json Normal file
View File

@@ -0,0 +1,252 @@
{
"schemaVersion": 1,
"source": {
"sourceName": "styles.refero.design",
"originalDetailUrl": "https://styles.refero.design/style/261a4ad3-e835-4f7a-beb7-72187f84d462",
"fallbackRawUrl": "",
"mirrorSource": "",
"websiteUrl": ""
},
"identity": {
"id": "P1R-019",
"slug": "amp",
"brand": "amp",
"titleZh": "amp 主题",
"titleEn": "amp",
"descriptionZh": "amp 的 Design.md 主题展示围绕浅色界面、AI、暖色调、SaaS 产品组织页面。",
"descriptionEn": "Explore amp's light Other design system: Orange Ember #ff6105, Soft Peach #ffdfcd colors, PublicaSans typography, and DESIGN.md for AI agents."
},
"tags": {
"sourceTags": [
"amp-design-system",
"amp-website-design",
"amp-colors",
"amp-typography",
"design-md",
"ai-agent-design",
"website-design-inspiration",
"other-design-system",
"orange-ember",
"soft-peach",
"warm-glow",
"ash-gray",
"publicasans"
],
"generatedTags": [
"image-assets",
"light",
"ai",
"warm"
],
"designTags": [
"PublicaSans",
"light"
],
"distributionTags": [
"浅色界面",
"AI",
"暖色调",
"SaaS 产品",
"开发工具",
"B2B 产品",
"效率工具",
"专业工具"
]
},
"assets": {
"designMd": {
"path": "DESIGN.md",
"method": "derived_from_metadata"
},
"tailwind": {
"path": "tw.css",
"sourcePath": ".local/design-md-batch-regression/refero/amp/tw.css"
},
"tokens": {
"path": "assets/tokens.json",
"method": "derived_from_design_md_and_metadata"
},
"cover": {
"status": "source_preview_image",
"path": "assets/cover.jpg",
"sourceUrl": "https://images.refero.design/styles/refero.design/image/9f4a748e-c053-469a-adf4-a7d1fde662ad.jpg",
"captureUrl": "",
"sourcePath": "assets/cover.jpg"
},
"previewHtml": {
"type": "local-preview",
"path": "assets/source-preview.jpg",
"publicPath": "assets/source-preview.jpg",
"importPath": "./assets/source-preview.jpg?url",
"sourcePath": "assets/source-preview.jpg"
}
},
"tokens": {
"palette": [
"#ff6105",
"#ffdfcd",
"#e5e5e5",
"#0a0a0a",
"#ffffff",
"#f3f4f3",
"#3c3e3d",
"#a89a8b",
"#cccccc",
"#292b2a",
"#e5e7eb",
"#fff8e9"
],
"typography": {
"display": "Inter",
"body": "Inter",
"mono": "ui-monospace",
"hints": [
"Inter",
"ui-monospace"
]
}
},
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"display": {
"brand": "amp 主题",
"brandAlias": "amp",
"description": "amp 的 Design.md 主题展示围绕浅色界面、AI、暖色调、SaaS 产品组织页面。",
"descriptionEn": "Explore amp's light Other design system: Orange Ember #ff6105, Soft Peach #ffdfcd colors, PublicaSans typography, and DESIGN.md for AI agents.",
"variant": "saas-devtool",
"distributionTags": [
"浅色界面",
"AI",
"暖色调",
"SaaS 产品",
"开发工具",
"B2B 产品",
"效率工具",
"专业工具"
],
"fontStylesheets": [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
],
"palette": [
{
"color": "#ff6105",
"labelZh": "主色",
"labelEn": "Primary",
"textColor": "#ffffff"
},
{
"color": "#ffdfcd",
"labelZh": "主色悬停",
"labelEn": "Primary Hover",
"textColor": "#171717"
},
{
"color": "#e5e5e5",
"labelZh": "辅助色",
"labelEn": "Secondary",
"textColor": "#171717"
},
{
"color": "#0a0a0a",
"labelZh": "强调色",
"labelEn": "Accent",
"textColor": "#ffffff"
},
{
"color": "#ffffff",
"labelZh": "背景色",
"labelEn": "Background",
"textColor": "#171717"
},
{
"color": "#f3f4f3",
"labelZh": "表面色",
"labelEn": "Surface",
"textColor": "#171717"
},
{
"color": "#3c3e3d",
"labelZh": "文本色",
"labelEn": "Text",
"textColor": "#ffffff"
},
{
"color": "#a89a8b",
"labelZh": "边框色",
"labelEn": "Border",
"textColor": "#171717"
},
{
"color": "#cccccc",
"labelZh": "颜色 9",
"labelEn": "Color 9",
"textColor": "#171717"
},
{
"color": "#292b2a",
"labelZh": "颜色 10",
"labelEn": "Color 10",
"textColor": "#ffffff"
},
{
"color": "#e5e7eb",
"labelZh": "颜色 11",
"labelEn": "Color 11",
"textColor": "#171717"
},
{
"color": "#fff8e9",
"labelZh": "颜色 12",
"labelEn": "Color 12",
"textColor": "#171717"
}
],
"shadows": [],
"borders": [],
"typography": [
"Inter",
"ui-monospace"
],
"previewImages": [
{
"type": "local-preview",
"path": "assets/source-preview.jpg"
}
],
"panels": [
{
"eyebrow": "产品",
"title": "开发工具界面",
"body": "适合工作台、文档、控制台和开发者产品。"
},
{
"eyebrow": "Token",
"title": "颜色与字体可复用",
"body": "页面从本地 metadata 与 Design.md 提取 palette、typography 和标签,用于快速预览主题质感。"
},
{
"eyebrow": "组件",
"title": "面向真实界面片段",
"body": "按钮、输入、卡片和间距样例保持克制,便于比较不同主题的系统性差异。"
}
],
"usageGuidance": {
"do": [
"建议:基于 Refero 采集图和 metadata 还原 amp 的颜色、字体和页面密度,优先服务 浅色界面、AI、暖色调 场景。",
"建议:保留 amp 的来源标签和视觉 token不把它混入旧来源的泛化模板。"
],
"dont": [
"避免:不要把 amp 套用到与 浅色界面、AI、暖色调 明显无关的产品语境。",
"避免:不要用通用行业模板覆盖 amp 的采集图风格;需要保留原图里的版式密度、留白和视觉重心。"
]
}
},
"status": {
"displayPage": "done",
"collectionErrors": []
}
}

3
src/themes/amp/tw.css Normal file
View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
/* TODO: Generate Tailwind v4 theme for amp. */

562
src/themes/apple/DESIGN.md Normal file
View File

@@ -0,0 +1,562 @@
---
version: alpha
name: Apple-design-analysis
description: A photography-first interface that turns marketing into a museum gallery. Edge-to-edge product tiles alternate light and dark canvases, framed by SF Pro Display headlines with negative letter-spacing and a single Action Blue (#0066cc) interactive color. UI chrome recedes so the product can speak — no decorative gradients, no shadows on chrome, only the one signature drop-shadow under product imagery resting on a surface.
colors:
primary: "#0066cc"
primary-focus: "#0071e3"
primary-on-dark: "#2997ff"
ink: "#1d1d1f"
body: "#1d1d1f"
body-on-dark: "#ffffff"
body-muted: "#cccccc"
ink-muted-80: "#333333"
ink-muted-48: "#7a7a7a"
divider-soft: "#f0f0f0"
hairline: "#e0e0e0"
canvas: "#ffffff"
canvas-parchment: "#f5f5f7"
surface-pearl: "#fafafc"
surface-tile-1: "#272729"
surface-tile-2: "#2a2a2c"
surface-tile-3: "#252527"
surface-black: "#000000"
surface-chip-translucent: "#d2d2d7"
on-primary: "#ffffff"
on-dark: "#ffffff"
typography:
hero-display:
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
fontSize: 56px
fontWeight: 600
lineHeight: 1.07
letterSpacing: -0.28px
display-lg:
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
fontSize: 40px
fontWeight: 600
lineHeight: 1.1
letterSpacing: 0
display-md:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 34px
fontWeight: 600
lineHeight: 1.47
letterSpacing: -0.374px
lead:
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
fontSize: 28px
fontWeight: 400
lineHeight: 1.14
letterSpacing: 0.196px
lead-airy:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 24px
fontWeight: 300
lineHeight: 1.5
letterSpacing: 0
tagline:
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
fontSize: 21px
fontWeight: 600
lineHeight: 1.19
letterSpacing: 0.231px
body-strong:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 17px
fontWeight: 600
lineHeight: 1.24
letterSpacing: -0.374px
body:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 17px
fontWeight: 400
lineHeight: 1.47
letterSpacing: -0.374px
dense-link:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 17px
fontWeight: 400
lineHeight: 2.41
letterSpacing: 0
caption:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.43
letterSpacing: -0.224px
caption-strong:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 14px
fontWeight: 600
lineHeight: 1.29
letterSpacing: -0.224px
button-large:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 18px
fontWeight: 300
lineHeight: 1.0
letterSpacing: 0
button-utility:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.29
letterSpacing: -0.224px
fine-print:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 12px
fontWeight: 400
lineHeight: 1.0
letterSpacing: -0.12px
micro-legal:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 10px
fontWeight: 400
lineHeight: 1.3
letterSpacing: -0.08px
nav-link:
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
fontSize: 12px
fontWeight: 400
lineHeight: 1.0
letterSpacing: -0.12px
rounded:
none: 0px
xs: 5px
sm: 8px
md: 11px
lg: 18px
pill: 9999px
full: 9999px
spacing:
xxs: 4px
xs: 8px
sm: 12px
md: 17px
lg: 24px
xl: 32px
xxl: 48px
section: 80px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.body}"
rounded: "{rounded.pill}"
padding: 11px 22px
button-primary-focus:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
rounded: "{rounded.pill}"
button-primary-active:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
rounded: "{rounded.pill}"
button-secondary-pill:
backgroundColor: "{colors.canvas}"
textColor: "{colors.primary}"
typography: "{typography.body}"
rounded: "{rounded.pill}"
padding: 11px 22px
button-dark-utility:
backgroundColor: "{colors.ink}"
textColor: "{colors.on-dark}"
typography: "{typography.button-utility}"
rounded: "{rounded.sm}"
padding: 8px 15px
button-pearl-capsule:
backgroundColor: "{colors.surface-pearl}"
textColor: "{colors.ink-muted-80}"
typography: "{typography.caption}"
rounded: "{rounded.md}"
padding: 8px 14px
button-store-hero:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button-large}"
rounded: "{rounded.pill}"
padding: 14px 28px
button-icon-circular:
backgroundColor: "{colors.surface-chip-translucent}"
textColor: "{colors.ink}"
rounded: "{rounded.full}"
size: 44px
text-link:
backgroundColor: transparent
textColor: "{colors.primary}"
typography: "{typography.body}"
text-link-on-dark:
backgroundColor: transparent
textColor: "{colors.primary-on-dark}"
typography: "{typography.body}"
global-nav:
backgroundColor: "{colors.surface-black}"
textColor: "{colors.on-dark}"
typography: "{typography.nav-link}"
height: 44px
sub-nav-frosted:
backgroundColor: "{colors.canvas-parchment}"
textColor: "{colors.ink}"
typography: "{typography.tagline}"
height: 52px
product-tile-light:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.display-lg}"
rounded: "{rounded.none}"
padding: 80px
product-tile-parchment:
backgroundColor: "{colors.canvas-parchment}"
textColor: "{colors.ink}"
typography: "{typography.display-lg}"
rounded: "{rounded.none}"
padding: 80px
product-tile-dark:
backgroundColor: "{colors.surface-tile-1}"
textColor: "{colors.on-dark}"
typography: "{typography.display-lg}"
rounded: "{rounded.none}"
padding: 80px
product-tile-dark-2:
backgroundColor: "{colors.surface-tile-2}"
textColor: "{colors.on-dark}"
rounded: "{rounded.none}"
product-tile-dark-3:
backgroundColor: "{colors.surface-tile-3}"
textColor: "{colors.on-dark}"
rounded: "{rounded.none}"
store-utility-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-strong}"
rounded: "{rounded.lg}"
padding: 24px
configurator-option-chip:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.caption}"
rounded: "{rounded.pill}"
padding: 12px 16px
configurator-option-chip-selected:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
rounded: "{rounded.pill}"
search-input:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.pill}"
padding: 12px 20px
height: 44px
floating-sticky-bar:
backgroundColor: "{colors.canvas-parchment}"
textColor: "{colors.ink}"
typography: "{typography.body}"
height: 64px
padding: 12px 32px
environment-quote-card:
backgroundColor: "{colors.surface-tile-1}"
textColor: "{colors.on-dark}"
typography: "{typography.display-lg}"
rounded: "{rounded.none}"
padding: 80px
footer:
backgroundColor: "{colors.canvas-parchment}"
textColor: "{colors.ink-muted-80}"
typography: "{typography.fine-print}"
padding: 64px
---
## Overview
Apple's web presence is a masterclass in **reverent product photography framed by near-invisible UI**. Every page is a stack of edge-to-edge product "tiles" — alternating light and dark canvases, each centered on a hero headline, a one-line tagline, two tiny blue pill CTAs, and an impossibly crisp product render. Nothing competes with the product. Typography is confident but quiet; color is either pure white, an off-white parchment, or a near-black tile; interactive elements are a single, quiet blue.
Density is unusually low even by contemporary SaaS standards. Each tile occupies roughly one viewport, and there is no decorative chrome — no borders, no gradients, no decorative frames, no shadows on headlines. Elevation appears only when a product image rests on a surface (a single soft `rgba(0, 0, 0, 0.22) 3px 5px 30px` drop for visual weight). The result is a catalog that feels more like a museum gallery: the wall disappears and the artifact takes over.
Store and shop surfaces retain the same chassis but switch modes. The product configurator (iPhone 17 Pro, accessories grid) introduces a tight grid of white utility cards at `{rounded.lg}` (18px) radius with a thin border, paired with a persistent thin sub-nav strip. The environment page leans darker and more editorial. Across all five surfaces the typographic system, spacing rhythm, and the single blue accent are consistent — this is one design language expressed at different volumes.
**Key Characteristics:**
- Photography-first presentation; UI recedes so the product can speak.
- Alternating full-bleed tile sections: white/parchment ↔ near-black, with the color change itself acting as the section divider.
- Single blue accent (`{colors.primary}`#0066cc) carries every interactive element. No second brand color exists.
- Two button grammars: tiny blue pill CTAs (`{rounded.pill}`) and compact utility rects (`{rounded.sm}`).
- SF Pro Display + SF Pro Text — negative letter-spacing at display sizes for the signature "Apple tight" headline feel.
- Whisper-soft elevation used only when a product image needs to breathe — exactly one drop-shadow in the entire system.
- Tight two-row nav: slim `{component.global-nav}` + product-specific `{component.sub-nav-frosted}` with persistent right-aligned primary CTA.
- Section rhythm across multiple pages: light hero → dark product tile → light utility tile → dark tile → parchment footer — a predictable pulse.
## Colors
> **Source pages analyzed:** homepage, environment, store, iPhone 17 Pro buy page, accessories index. The color system is identical across all five surfaces; only the surface-mode mix differs.
### Brand & Accent
- **Action Blue** (`{colors.primary}`#0066cc): The single brand-level interactive color. All text links, all blue pill CTAs ("Learn more", "Buy"), and the focus ring root. This is Apple's quiet but universal "click me" signal. Press state shifts to a slightly darker variant via the active scale transform rather than a hex change.
- **Focus Blue** (`{colors.primary-focus}`#0071e3): A marginally brighter sibling of Action Blue, reserved for the keyboard focus ring on buttons (`outline: 2px solid`).
- **Sky Link Blue** (`{colors.primary-on-dark}`#2997ff): A brighter blue used on dark surfaces for in-copy links and inline callouts, where Action Blue would disappear against the tile background.
### Surface
- **Pure White** (`{colors.canvas}`#ffffff): The dominant canvas. Content, utility cards, store tiles, configurator grids.
- **Parchment** (`{colors.canvas-parchment}`#f5f5f7): The signature Apple off-white. Used for alternating light tiles, footer region, and the default page canvas in store utility sections. Just different enough from white to create rhythm.
- **Pearl Button** (`{colors.surface-pearl}`#fafafc): A near-white used as the fill for secondary "ghost" buttons — lighter than the parchment canvas so the button still reads as a button against `{colors.canvas-parchment}`.
- **Near-Black Tile 1** (`{colors.surface-tile-1}`#272729): The primary dark-tile surface on the homepage product grid.
- **Near-Black Tile 2** (`{colors.surface-tile-2}`#2a2a2c): A micro-step lighter — used where a dark tile sits directly above or below Tile 1 to create the faintest separation.
- **Near-Black Tile 3** (`{colors.surface-tile-3}`#252527): A micro-step darker — used at the bottom of the stack and in embedded video/player frames.
- **Pure Black** (`{colors.surface-black}`#000000): Reserved for true void — video player backgrounds, edge-to-edge photographic overlays, the global nav bar background.
- **Translucent Chip Gray** (`{colors.surface-chip-translucent}`#d2d2d7): The base hex of the translucent gray chip used over photography for circular control buttons. In production, applied at ~64% alpha as `rgba(210, 210, 215, 0.64)`.
### Text
- **Near-Black Ink** (`{colors.ink}`#1d1d1f): The voice of every headline, every body paragraph, and the dark utility button's fill. Chosen instead of pure black to keep the page feeling photographic rather than printed.
- **Body** (`{colors.body}`#1d1d1f): Same hex as ink — Apple uses one near-black tone for all text on light surfaces.
- **Body On Dark** (`{colors.body-on-dark}`#ffffff): All text on dark tiles and on the global nav bar.
- **Body Muted** (`{colors.body-muted}`#cccccc): Secondary copy on dark tiles where pure white would be too loud.
- **Ink Muted 80** (`{colors.ink-muted-80}`#333333): Body text on the white Pearl Button surface — slightly softer than pure black.
- **Ink Muted 48** (`{colors.ink-muted-48}`#7a7a7a): Disabled button text and legal fine-print.
### Hairlines & Borders
- **Divider Soft** (`{colors.divider-soft}`#f0f0f0): The "border" tone on secondary buttons — functions as a ring shadow rather than a hard line. In production, often applied as `rgba(0, 0, 0, 0.04)`.
- **Hairline** (`{colors.hairline}`#e0e0e0): The 1px hairline border on store utility cards and configurator chips.
### Brand Gradient
**No decorative gradients.** Atmospheric depth on product photography (the iPhone 17 Pro camera plate, the Apple Watch bands, AirPods reflections) is inherent to the imagery, not a CSS gradient overlay. The environment page's hero uses photographic atmosphere (mountain vista at dawn) but no gradient tokens are defined. Apple is the rare luxury-brand site with zero gradient-based design tokens.
## Typography
### Font Family
- **Display**: `SF Pro Display, system-ui, -apple-system, sans-serif` — Apple's proprietary display face, optimized for sizes ≥ 19px. Defines the voice of every headline.
- **Body / UI**: `SF Pro Text, system-ui, -apple-system, sans-serif` — the text-optimized variant used for body copy, captions, buttons, and links below 20px.
- **OpenType features**: `font-variant-numeric: numerator` is enabled on numeric links (pricing tables, spec sheets). Display sizes rely on tight tracking rather than contextual ligatures.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.hero-display}` | 56px | 600 | 1.07 | -0.28px | Hero headline; the signature "Apple tight" tracking |
| `{typography.display-lg}` | 40px | 600 | 1.10 | 0 | Tile headlines atop every product tile |
| `{typography.display-md}` | 34px | 600 | 1.47 | -0.374px | Section heads (SF Pro Text at display proportions) |
| `{typography.lead}` | 28px | 400 | 1.14 | 0.196px | Product tile subcopy |
| `{typography.lead-airy}` | 24px | 300 | 1.5 | 0 | Environment-page lead paragraphs (the rare weight 300) |
| `{typography.tagline}` | 21px | 600 | 1.19 | 0.231px | Sub-tile tagline; sub-nav category name |
| `{typography.body-strong}` | 17px | 600 | 1.24 | -0.374px | Inline strong emphasis |
| `{typography.body}` | 17px | 400 | 1.47 | -0.374px | Default paragraph |
| `{typography.dense-link}` | 17px | 400 | 2.41 | 0 | Footer / store utility link lists (relaxed leading) |
| `{typography.caption}` | 14px | 400 | 1.43 | -0.224px | Secondary captions, button text |
| `{typography.caption-strong}` | 14px | 600 | 1.29 | -0.224px | Emphasized captions |
| `{typography.button-large}` | 18px | 300 | 1.0 | 0 | Store hero CTAs (the rare weight 300) |
| `{typography.button-utility}` | 14px | 400 | 1.29 | -0.224px | Utility/nav button labels |
| `{typography.fine-print}` | 12px | 400 | 1.0 | -0.12px | Fine-print, footer body |
| `{typography.micro-legal}` | 10px | 400 | 1.3 | -0.08px | Micro legal disclaimers |
| `{typography.nav-link}` | 12px | 400 | 1.0 | -0.12px | Global nav menu items |
### Principles
- **Negative letter-spacing at display sizes.** Every headline at 17px and up carries a slight tracking tighten (`-0.12 → -0.374px`). This produces the iconic "Apple tight" headline cadence. Never used at 12px or below.
- **Body copy at 17px, not 16px.** Apple breaks the SaaS convention and runs paragraph text at 17px. The extra pixel gives the page an unmistakable "reading, not scanning" pace.
- **Weight 300 is real and rare.** Used deliberately on a handful of large-size reads (`{typography.button-large}` at 18px/300 and `{typography.lead-airy}` at 24px/300). It's not an accident — it's a light-atmosphere cue reserved for moments where the content should feel airy.
- **Weight 600, not 700, for headlines.** Apple's headlines sit at weight 600. Weight 700 is used sparingly for `{typography.tagline}` (21px) when a touch more assertion is needed.
- **Line-height is context-specific.** Display sizes use 1.071.19 (tight). Body uses 1.47. Utility link stacks in the footer/store use an unusually relaxed 2.41 (`{typography.dense-link}`). The 2.41 is not a bug — it's how the footer's dense link columns breathe.
- **Weight 500 is deliberately absent.** The ladder is 300 / 400 / 600 / 700. Mid-weight readings always use 600.
### Note on Font Substitutes
SF Pro is Apple's proprietary system font. When building off-system:
- Use `system-ui, -apple-system, BlinkMacSystemFont` as the first stack entry — on macOS/iOS/Safari this resolves to the real SF Pro.
- For non-Apple platforms, **Inter** (Google Fonts, variable) is the closest open-source equivalent. Inter at weight 600 with `font-feature-settings: "ss03"` approximates SF Pro's rounded "a" character.
- Nudge `letter-spacing` down by `-0.01em` on display sizes to re-create the Apple tight feel; Inter's default tracking runs slightly wider than SF Pro.
- For body text, tighten line-height by `0.03` (from 1.47 → 1.44) when substituting Inter — Inter's taller x-height needs less leading.
## Layout
### Spacing System
- **Base unit:** 8px. Sub-base values (2, 4, 5, 6, 7) are used for tight typographic adjustments; structural layout snaps to 8/12/16/20/24.
- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 17px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 80px.
- **Section vertical padding:** `{spacing.section}` (80px) inside a product tile; tiles stack edge-to-edge with 0 gap (the color change provides the break).
- **Card padding:** `{spacing.lg}` (24px) inside utility grid cards.
- **Button padding:** 811px vertical, 1522px horizontal.
- **Universal rhythm constants:** the 17px body line-height multiplier (~25px line) and 21px tagline size show up on every analyzed page.
### Grid & Container
- **Max content width:** ~980px on text-heavy sections (environment), ~1440px on product grids (store, accessories), full-bleed for product tiles (homepage).
- **Column patterns:** 3 to 5 column utility card grid on store/accessories; 2-column side-by-side tiles on homepage occasional sections; single-column centered stack on product tile heroes.
- **Gutters:** 2024px between cards in a utility grid.
### Whitespace Philosophy
Apple's whitespace is the product's pedestal. Every tile begins with at least 64px of air above its headline and 4864px below. Product renders are never crowded; the nearest content to a product image is at least 40px away. The footer is the only area that breaks this — there, Apple goes deliberately dense to make the full information architecture visible at a glance.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| Flat | No shadow, no border | Full-bleed tiles, global nav, footer, body sections |
| Soft hairline | 1px `rgba(0, 0, 0, 0.08)` border | Utility cards, sub-nav frosted-glass separator |
| Backdrop blur | `backdrop-filter: blur(N)` on Parchment 80% | Sub-nav and the iPhone buy floating sticky bar |
| Product shadow | `rgba(0, 0, 0, 0.22) 3px 5px 30px 0` | Product renders resting on a surface (the only true "shadow" in the system) |
**Shadow philosophy.** Apple uses **exactly one** drop-shadow, and it is applied to photographic product imagery — never to cards, never to buttons, never to text. Elevation in the UI comes from (a) surface-color change (light tile ↔ dark tile) and (b) backdrop-blur on sticky bars. The single shadow is about giving the product weight, not about UI hierarchy.
### Decorative Depth
- **Atmospheric imagery** on the environment page (photographic vista) supplies mood; no CSS gradient involved.
- **Edge-to-edge tile alternation** creates rhythm without borders or shadows — the color change itself is the divider.
- **Backdrop-filter blur** on `{component.sub-nav-frosted}` and `{component.floating-sticky-bar}` creates a "floating over content" effect that's functional, not decorative.
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| `{rounded.none}` | 0px | Full-bleed product tiles (no corner rounding) |
| `{rounded.xs}` | 5px | Inline links when styled as subtle chips (rare) |
| `{rounded.sm}` | 8px | Dark utility buttons (Sign In, Bag), inline card imagery |
| `{rounded.md}` | 11px | White Pearl Button capsules |
| `{rounded.lg}` | 18px | Store utility cards, accessories grid cards |
| `{rounded.pill}` | 9999px | Primary blue pill CTAs, sub-nav buy button, configurator option chips, search input — the signature Apple pill |
| `{rounded.full}` | 9999px / 50% | Circular control chips floating over photography |
### Photography Geometry
- **Hero imagery**: full-bleed, 21:9 or taller on the homepage; 16:9 on environment and shop pages. Product renders are photographic-realistic, often shot on a tinted surface that becomes the tile background.
- **Product renders**: PNG/WebP with transparency; rest on a surface tile and pick up the system shadow.
- **Accessory grid**: square 1:1 crops at `{rounded.lg}` (18px) radius, light neutral backgrounds, product centered with 2040px internal padding.
- **No rounded imagery in hero tiles** — images are full-bleed rectangular. Rounding (`{rounded.sm}`, `{rounded.lg}`) appears only on inline card imagery.
- Lazy-loading via responsive `srcset` and `sizes` across all breakpoints; CDN-optimized WebP.
## Components
### Top Navigation
**`global-nav`** — Persistent, ultra-thin black nav bar pinned to the top of every page. Background `{colors.surface-black}`, height 44px, text `{colors.on-dark}` in `{typography.nav-link}` (12px / 400 / -0.12px tracking). Links are quiet, spaced ~20px apart, running edge-to-edge across the top. Right-aligned cluster: Search, Bag icons — always visible. On mobile, collapses to hamburger at ~834px and the Apple logo centers.
**`sub-nav-frosted`** — Surface-specific nav that sticks below the global nav. Background `{colors.canvas-parchment}` at 80% opacity with backdrop-filter blur, creating a frosted-glass effect. Height 52px. Content on left: product category name ("iPhone", "Store", "Accessories") in `{typography.tagline}` (21px / 600). Content right: inline nav links in `{typography.button-utility}` (14px), ending in a persistent `{component.button-primary}` ("Buy") or a utility link.
### Buttons
**`button-primary`** — The signature Apple action. Background `{colors.primary}` (Action Blue #0066cc), text `{colors.on-primary}` in `{typography.body}` (SF Pro Text 17px / 400), rounded `{rounded.pill}` (full pill — capsule-shaped), padding 11px × 22px. The full-pill radius IS the brand action signal.
- Active state: `{component.button-primary-active}``transform: scale(0.95)` (the system-wide micro-interaction).
- Focus state: `{component.button-primary-focus}` — 2px solid `{colors.primary-focus}` outline.
**`button-secondary-pill`** — Used as the second CTA when two blue pills appear together ("Learn more" / "Buy"). Background transparent, text `{colors.primary}`, 1px solid `{colors.primary}` border, rounded `{rounded.pill}`, padding 11px × 22px. Reads as a "ghost pill."
**`button-dark-utility`** — Global nav actions (Sign In, Bag, language selector). Background `{colors.ink}` (#1d1d1f), text `{colors.on-dark}` in `{typography.button-utility}` (14px / 400 / -0.224px tracking), rounded `{rounded.sm}` (8px), padding 8px × 15px. Active state shrinks via `transform: scale(0.95)`.
**`button-pearl-capsule`** — Product-card secondary button. Background `{colors.surface-pearl}` (#fafafc), text `{colors.ink-muted-80}` in `{typography.caption}` (14px), 3px solid `{colors.divider-soft}` border (functions as a soft ring rather than a visible line), rounded `{rounded.md}` (11px), padding 8px × 14px.
**`button-store-hero`** — A larger primary CTA used on store hero surfaces. Same Action Blue + Paper White as `{component.button-primary}`, but with `{typography.button-large}` (18px / 300 — note the rare weight 300) and slightly more padding (14px × 28px). Used sparingly on the store landing.
**`button-icon-circular`** — Floats over photography. 44 × 44px, background `{colors.surface-chip-translucent}` at ~64% alpha, icon in `{colors.ink}`, rounded `{rounded.full}`. Used for carousel controls, close buttons, and in-image controls (product image thumbnails on the iPhone buy page).
**`text-link`** — Inline body links in `{colors.primary}` (Action Blue). Underlined or non-underlined per context.
**`text-link-on-dark`** — Inline body links on dark tiles in `{colors.primary-on-dark}` (Sky Link Blue #2997ff) — Action Blue would disappear against `{colors.surface-tile-1}`.
### Cards & Containers
**`product-tile-light`** — Full-bleed light tile. Background `{colors.canvas}` (white), text `{colors.ink}`, rounded `{rounded.none}` (0 — tiles touch edges), vertical padding `{spacing.section}` (80px). Centered stack: product name in `{typography.display-lg}` (40px / 600) → one-line tagline in `{typography.lead}` (28px / 400) → two `{component.button-primary}` CTAs ("Learn more" / "Buy") → product render resting on the surface with the system shadow.
**`product-tile-parchment`** — Same as `{component.product-tile-light}` but on `{colors.canvas-parchment}` (#f5f5f7). Used to break two consecutive white tiles.
**`product-tile-dark`** — Full-bleed dark tile. Background `{colors.surface-tile-1}` (#272729), text `{colors.on-dark}`, rounded `{rounded.none}`, vertical padding `{spacing.section}` (80px). Same content stack as the light tile but with `{component.text-link-on-dark}` for inline copy and `{component.button-primary}` (Action Blue still works on the dark surface). Used on the homepage product grid as the alternating dark band.
**`product-tile-dark-2`** — Variant on `{colors.surface-tile-2}` (#2a2a2c). Used where a dark tile sits directly above or below `{component.product-tile-dark}` to create the faintest separation through micro-step lightness change.
**`product-tile-dark-3`** — Variant on `{colors.surface-tile-3}` (#252527). Used at the bottom of the stack and in embedded video/player frames.
**`store-utility-card`** — Used in store grid and accessories grid. Background `{colors.canvas}` (white), 1px solid `{colors.hairline}` border, rounded `{rounded.lg}` (18px), padding `{spacing.lg}` (24px). Top: product image (1:1 crop with `{rounded.sm}` (8px) inner image radius). Below: product name in `{typography.body-strong}` (17px / 600), price in `{typography.body}` (17px / 400), and a `{component.text-link}` ("Buy" or "Learn more"). No shadow by default; product render itself carries the system product-shadow.
**`configurator-option-chip`** — Pill-shaped tappable cell used in the iPhone 17 Pro buy page. Background `{colors.canvas}`, text `{colors.ink}` in `{typography.caption}`, rounded `{rounded.pill}`, padding 12px × 16px. Contains a small product thumbnail + label + price delta. Arranged in a grid of 45 options per row.
**`configurator-option-chip-selected`** — Selected state. Border upgrades to 2px solid `{colors.primary-focus}`. Same shape, same content.
**`environment-quote-card`** — A photographic-canvas hero specific to the environment page. Dark photographic backdrop (mountain vista at dawn) with `{colors.surface-tile-1}` as the fallback color, centered white-text headline in `{typography.display-lg}` (40px), small green "Apple 2030" pictographic logo above the headline, single `{component.button-primary}` below. Padding `{spacing.section}` (80px).
**`floating-sticky-bar`** — Floats at the bottom of the viewport on the iPhone 17 Pro buy page during scroll. Background `{colors.canvas-parchment}` at 80% opacity with `backdrop-filter: blur(N)`, height 64px, padding 12px × 32px. Left: running price total in `{typography.body}`. Right: `{component.button-primary}` ("Add to Bag").
### Inputs & Forms
**`search-input`** — The accessories search input. Background `{colors.canvas}`, text `{colors.ink}` in `{typography.body}` (17px), 1px solid `rgba(0, 0, 0, 0.08)` border, rounded `{rounded.pill}` (full pill — search is also pill-shaped, matching the CTA grammar), padding 12px × 20px, height 44px. Leading icon: search glyph at 14px, muted tint.
Error and validation states were not surfaced in the analyzed pages.
### Footer
**`footer`** — Background `{colors.canvas-parchment}` (#f5f5f7), text `{colors.ink-muted-80}`. Link columns in `{typography.dense-link}` (17px / 400 / 2.41 line-height — the relaxed leading is what makes the dense columns scannable). Column headings in `{typography.caption-strong}` (14px / 600). Legal row at the very bottom in `{typography.fine-print}` (12px / 400) with `{colors.ink-muted-48}` text. Vertical padding 64px.
## Do's and Don'ts
### Do
- Use `{colors.primary}` (Action Blue #0066cc) for every interactive element — links, pill CTAs, focus signals — and nothing else. The single accent is non-negotiable.
- Set headlines in `{typography.hero-display}` or `{typography.display-lg}` with negative letter-spacing (`-0.28 → -0.374px`) to get the signature "Apple tight" cadence.
- Run body copy at `{typography.body}` (17px / 400 / 1.47 / -0.374px) — not 16px. The extra pixel defines the brand's reading pace.
- Alternate `{component.product-tile-light}` (or parchment) and `{component.product-tile-dark}` for full-bleed section rhythm. The color change IS the divider.
- Reserve `{rounded.pill}` for the primary blue CTA and any other element that should read as an "action" (configurator chips, search input, sticky bar CTA).
- Apply the single product-shadow (`rgba(0, 0, 0, 0.22) 3px 5px 30px`) only to product renders resting on a surface — never on cards, buttons, or text.
- Use `transform: scale(0.95)` as the active/press state on every button — it's the system-wide micro-interaction.
- Keep the global nav `{colors.surface-black}` (true black) — it's the only place pure black appears on most pages.
### Don't
- Don't introduce a second accent color; every "click me" signal is `{colors.primary}` (Action Blue).
- Don't add shadows to cards, buttons, or text — shadow is reserved for product imagery.
- Don't use gradients as decorative backgrounds; atmosphere comes from photography.
- Don't set body copy at weight 500 — Apple's ladder is 300 / 400 / 600 / 700, with 500 deliberately absent. Body is always 400; strong inline is 600; display is 600.
- Don't round full-bleed tiles — tiles are rectangular and edge-to-edge; the color change is the divider.
- Don't tighten line-height below 1.47 for body copy — the editorial leading is part of the brand.
- Don't mix radii grammars — use `{rounded.sm}` for compact utility, `{rounded.lg}` for utility cards, `{rounded.pill}` for pills, and nothing in between (except the rare `{rounded.md}` Pearl Button).
- Don't use `{colors.primary-on-dark}` (Sky Link Blue) on light surfaces — it's the dark-tile-only variant. Action Blue is for light surfaces.
## Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Small phone | ≤ 419px | Single-column tiles; sub-nav collapses to category name + primary CTA only; hero typography drops to 28px |
| Phone | 420640px | Single-column stack; product renders scale to 80% of tile width; hero h1 drops to 34px |
| Large phone | 641735px | Tiles transition to tighter padding (48px vertical vs 80px); fine-print wraps |
| Tablet portrait | 736833px | Global nav collapses to hamburger; sub-nav hides category chips, keeps primary CTA |
| Tablet landscape | 8341023px | Global nav returns fully expanded; 3-column utility grids become 2-column |
| Small desktop | 10241068px | Product tiles use 2/3 width with margin gutters; hero h1 stays at 40px |
| Desktop | 10691440px | Full layout; 45 column store grids; 1440px content max |
| Wide desktop | ≥ 1441px | Content locks at 1440px, margins absorb extra width |
The structural breakpoints that matter for agents: 1440px (content lock), 1068px (small-desktop), 833px (tablet landscape switch), 734px (tablet portrait), 640px (phone), 480px (small phone).
### Touch Targets
- Minimum 44 × 44px. `{component.button-primary}` lands at ~44 × 100px (with the full-pill radius making the visible hit area more generous than the label suggests).
- `{component.button-icon-circular}` is exactly 44 × 44px.
- Global nav utility links are smaller (~32 × 80px) — they deliberately sit at a tighter target because they're precision desktop actions, and the mobile hamburger replaces them at ≤ 833px.
### Collapsing Strategy
- **Global nav**: full horizontal link row on desktop → collapses to Apple logo + hamburger + bag icon at 834px and below.
- **Sub-nav**: category name + inline links + primary CTA → category name + primary CTA only at mobile; inline links move into a hamburger tray.
- **Product tiles**: stack from 2-column to 1-column at 834px; vertical padding tightens from 80px → 48px at small-phone.
- **Utility grids** (store, accessories): 5-col → 4-col (1440px) → 3-col (1068px) → 2-col (834px) → 1-col (640px).
- **Hero typography**: `{typography.hero-display}` (56px) → `{typography.display-lg}` (40px) at 1068px → 34px at 640px → 28px at 419px.
### Image Behavior
- All product imagery uses responsive `srcset` with breakpoint-matched crops.
- Hero photography may switch art direction at mobile (e.g., the environment page's vista crops to a taller aspect ratio on mobile, framing the subject differently).
- Product renders maintain their 1:1 or 4:3 aspect ratios across breakpoints; only scale changes.
- Lazy-loading is default; the above-fold hero loads eagerly.
## Iteration Guide
1. Focus on ONE component at a time. Reference its YAML key directly (`{component.product-tile-dark}`, `{component.search-input}`).
2. Variants of an existing component (`-active`, `-focus`, `-2`, `-3`) live as separate entries in `components:`.
3. Use `{token.refs}` everywhere — never inline hex.
4. Never document hover. Default and Active/Pressed states only.
5. Display headlines stay SF Pro Display 600 with negative letter-spacing. Body stays SF Pro Text 400 at 17px. The boundary is unbreakable.
6. The single drop-shadow (`rgba(0, 0, 0, 0.22) 3px 5px 30px`) is reserved for product photography only.
7. When in doubt about emphasis: alternate surface (light → dark tile) before adding chrome.
## Known Gaps
- Form validation and error states were not surfaced on the analyzed pages; only the neutral search input is documented.
- The homepage's embedded video/player frame uses `{colors.surface-black}`; interior player controls are not documented (they're a platform widget, not a web-design token).
- Some component imagery is dynamic (rotating product hero) and its specific copy varies per surface — component specs name the structure, not the rotating content.
- Dark-mode counterparts for store and accessories utility cards were not surfaced on the analyzed pages; the system documented is the daytime/light-dominant variant Apple ships by default.
- Atmospheric photography (environment page mountain vista) is a content asset, not a design token; the documented `{component.environment-quote-card}` describes the structural surface only.
- The exact backdrop-filter blur radius on `{component.sub-nav-frosted}` and `{component.floating-sticky-bar}` is platform-dependent; production CSS uses `saturate(180%) blur(20px)` as a typical baseline but the value isn't formalized as a token.

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Some files were not shown because too many files have changed in this diff Show More