跳转至

Celestia 主题收藏卡

一个「AI 生成收藏集」的展示画廊:每条数据自带整套主题(全屏背景渐变 + 卡底 + 文字/强调色), 点顶部导航切换条目,氛围随之整体变奏 —— 深绿田园、宣纸米白、鎏金玄青、星空蓝、烈日红。 玻璃拟态大卡 + 衬线大字 + 宽字距全大写小标签,杂志内页气质。

要点

  • 把整套主题当数据:每条数据自带 theme 对象(bgMain / cardBg / textMain / textSecondary / accent),组件只负责注入 CSS 变量,换条目 = 换全套配色
  • 三层字阶拉开主次:衬线 8rem 缩写大字是唯一主角,标题衬线 2.6rem,其余一律 9–12px 全大写 + .2em 以上宽字距压低存在感
  • 玻璃拟态卡用半透明卡底 + backdrop-filter: blur(24px) + 1px 微弱边框,同一个边框色复用为 tag 描边 / 分隔线 / 角落装饰
  • 切换做成「调色」而非翻页:body 背景 / 文字给 .8s 过渡、卡内各处 .7s,渐变本身不可过渡也不碍事
  • 大字背后垫一张细线 SVG 星图(星点 + 连线),配 10s 缓浮动画

源码(折叠)

celestia-collection-demo.html(自包含单页)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Celestia 主题收藏卡 demo</title>
<!--
  celestia-collection-demo.html —— 「前端风格收集」的 Celestia 主题收藏卡演示真身。

  吸纳自一段 AI 生成的收藏展示单页(2026-07-09 收录)。原版走 unpkg React +
  Babel standalone(JSX)+ Tailwind CDN + Google Fonts + lucide,按本 wiki
  自包含规矩(skills/writing/mkdocs-wiki/index.md 第 9 节)改写:
  - React 18 UMD + htm 从全站 /vendor/ 引,免 JSX 构建,不走任何 CDN
  - Tailwind 工具类翻成下面的语义化 CSS;每条数据的 theme 注入为 CSS 变量
  - Playfair Display / Inter 降级为本地字体链(Didot / Georgia、system-ui 兜底)
  - 修复原稿贴出时的损坏处:header 左侧品牌区按残留结构重建,底部太阳图标 path 补全
-->
<style>
  /* 三层字阶是这个风格的骨架:
     大 —— 衬线 8rem 缩写字(唯一视觉主角)
     中 —— 衬线 2.6rem 标题
     微 —— 9~12px 全大写 + .2em 以上字距(导航/标签/说明,统一压低存在感) */
  :root {
    --serif: 'Playfair Display', Didot, 'Bodoni MT', Georgia, 'Times New Roman', serif;
    --sans: Inter, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif;
  }
  * { box-sizing: border-box; margin: 0; }
  body {
    font-family: var(--sans);
    -webkit-font-smoothing: antialiased;
    /* 背景渐变本身不可过渡,但 background-color / color 的 .8s 过渡
       足以让主题切换读作"调色"而非"翻页" */
    transition: background .8s ease-in-out, color .8s ease-in-out;
  }
  ::selection { background: rgba(255,255,255,.3); }
  button { font: inherit; background: none; border: none; color: inherit; cursor: pointer; }

  .app { min-height: 100vh; display: flex; flex-direction: column; }

  /* ── 顶部:品牌 | 系列信息+导航 | 占位,三栏对称 ── */
  .header {
    display: grid; grid-template-columns: 12rem 1fr 12rem;
    align-items: start; gap: 16px; padding: 24px; z-index: 10;
  }
  .brand {
    display: flex; align-items: center; gap: 10px;
    font-size: 12px; letter-spacing: .25em;
  }
  .series { display: flex; flex-direction: column; align-items: center; text-align: center; }
  .series-no { font-size: 10px; letter-spacing: .25em; text-transform: uppercase; opacity: .6; margin-bottom: 4px; }
  .series-cat { font-size: 12px; letter-spacing: .2em; text-transform: uppercase; margin-bottom: 22px; }
  .nav-tabs {
    display: flex; justify-content: center; gap: 8px;
    max-width: 100%; overflow-x: auto; padding-bottom: 8px;
    scrollbar-width: none;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  .nav-btn {
    font-size: 10px; letter-spacing: .25em; text-transform: uppercase;
    padding: 6px 12px; border-radius: 4px; border: 1px solid transparent;
    opacity: .5; white-space: nowrap; transition: all .3s;
  }
  .nav-btn:hover { opacity: .8; }
  .nav-btn.active {
    opacity: 1; background: var(--card-border);
    border-color: var(--card-border); backdrop-filter: blur(4px);
  }

  /* ── 中央玻璃卡 ── */
  .stage { flex: 1; display: flex; align-items: center; justify-content: center; padding: 12px 24px 24px; z-index: 1; }
  .card {
    width: 100%; max-width: 1100px; height: 620px;
    display: flex; position: relative; overflow: hidden; border-radius: 4px;
    background: var(--card-bg); border: 1px solid var(--card-border);
    backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
    box-shadow: 0 25px 50px -12px rgba(0,0,0,.25);
    transition: background-color .7s ease-in-out;
  }
  .corner {
    position: absolute; top: 24px; left: 24px; width: 16px; height: 16px;
    border-top: 1px solid var(--card-border); border-left: 1px solid var(--card-border); opacity: .5;
  }

  /* 左半:大字展示区 */
  .card-left {
    width: 50%; padding: 80px; position: relative;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    border-right: 1px solid var(--card-border); transition: border-color .7s;
  }
  .starmap {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: .4; pointer-events: none;
    animation: subtle-float 10s ease-in-out infinite;
  }
  @keyframes subtle-float {
    0%, 100% { transform: translateY(0) scale(1); }
    50% { transform: translateY(-5px) scale(1.02); }
  }
  .abbr-wrap { z-index: 1; text-align: center; }
  .abbr {
    font-family: var(--serif); font-weight: 500;
    font-size: clamp(6rem, 12vw, 8rem); line-height: 1;
    margin-bottom: 24px; filter: drop-shadow(0 4px 3px rgba(0,0,0,.15));
    transition: color .7s;
  }
  .date { font-family: var(--serif); font-style: italic; font-size: 15px; letter-spacing: .08em; color: var(--accent); }
  .modify-btn {
    position: absolute; bottom: 40px; z-index: 1;
    display: flex; align-items: center; gap: 8px;
    font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
    opacity: .5; transition: opacity .3s;
  }
  .modify-btn:hover { opacity: 1; }

  /* 右半:文案区 */
  .card-right { width: 50%; padding: 56px 64px; display: flex; flex-direction: column; justify-content: center; }
  .eyebrow {
    display: flex; align-items: center; gap: 16px;
    font-size: 10px; letter-spacing: .25em; text-transform: uppercase;
    color: var(--text-secondary); margin-bottom: 16px;
  }
  .rule { width: 24px; height: 1px; background: currentColor; opacity: .3; }
  .title { font-family: var(--serif); font-weight: 500; font-size: clamp(2.2rem, 4vw, 3rem); line-height: 1.15; margin-bottom: 8px; transition: color .7s; }
  .subtitle { font-size: 14px; font-weight: 400; letter-spacing: .25em; color: var(--text-secondary); transition: color .7s; }
  .desc { display: grid; gap: 20px; margin: 30px 0 36px; font-size: 13px; line-height: 1.8; color: var(--text-secondary); transition: color .7s; }
  .desc p:first-child { opacity: .8; }
  .desc p:last-child { opacity: .7; }
  .tags { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 40px; }
  .tag {
    font-size: 9px; letter-spacing: .25em; text-transform: uppercase;
    padding: 6px 12px; border-radius: 2px;
    border: 1px solid var(--card-border); color: var(--text-secondary);
  }
  .card-footer { margin-top: auto; border-top: 1px solid var(--card-border); padding-top: 28px; display: grid; gap: 28px; }
  .explore-btn {
    display: flex; justify-content: space-between; width: 100%; text-align: left;
    font-size: 12px; letter-spacing: .2em; text-transform: uppercase;
    color: var(--text-main); transition: opacity .3s;
  }
  .explore-btn:hover { opacity: .7; }
  .palette-row { display: flex; align-items: center; justify-content: space-between; }
  .micro-label { font-size: 10px; letter-spacing: .25em; text-transform: uppercase; color: var(--text-secondary); }
  .palette { display: flex; gap: 12px; }
  .swatch {
    width: 20px; height: 20px; border-radius: 50%;
    box-shadow: 0 1px 2px rgba(0,0,0,.25); cursor: pointer; transition: transform .2s;
  }
  .swatch:hover { transform: scale(1.15); }

  /* 左下角系统图标 */
  .sys-icon {
    position: fixed; bottom: 24px; left: 24px;
    width: 32px; height: 32px; border-radius: 50%; border: 1px solid currentColor;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-main); opacity: .4; cursor: pointer; transition: opacity .3s;
  }
  .sys-icon:hover { opacity: 1; }

  @media (max-width: 860px) {
    .header { grid-template-columns: 1fr; }
    .brand, .brand-balance { display: none; }
    .card { flex-direction: column; height: auto; }
    .card-left { width: 100%; min-height: 380px; padding: 48px; border-right: none; border-bottom: 1px solid var(--card-border); }
    .card-right { width: 100%; padding: 40px 32px; }
  }
</style>
<script src="/vendor/react.production.min.js"></script>
<script src="/vendor/react-dom.production.min.js"></script>
<script src="/vendor/htm.umd.js"></script>
</head>
<body>
<div id="root"></div>
<script>
const html = htm.bind(React.createElement);
const { useState, useEffect } = React;

/* 风格的核心资产:每条数据自带整套 theme(全屏背景渐变、卡底、文字/强调色),
   组件只负责把 theme 注入 CSS 变量 —— 换条目 = 换全套配色。 */
const collectionData = [
  {
    id: 'taurus',
    series: 'SERIES 1/1',
    category: 'ZODIAC CLASSICS',
    name: 'Taurus',
    abbr: 'TA',
    date: 'April 20 - May 20',
    title: 'Taurus: The Steadfast Earth',
    subtitle: '金牛座: 坚韧大地',
    descEn: "Grounded in verdant fields, Taurus embodies unwavering strength and a deep appreciation for life's enduring beauty. With a spirit both resilient and sensual, it finds solace in stability and the rich bounty of the earth.",
    descZh: '金牛座扎根于翠绿大地,展现坚定不移的力量和对生命永恒之美的深刻欣赏。其精神既坚韧又感性,在稳定和大地丰饶中寻得慰藉。',
    tags: ['GROUNDED', 'LUXURIOUS', 'RESILIENT'],
    colors: ['#F5EEDB', '#D4B886', '#9E7453', '#7C9385', '#D1C0B1'],
    theme: {
      bgMain: 'radial-gradient(circle at 30% 30%, #3a5446 0%, #1e2c24 100%)',
      cardBg: 'rgba(42, 60, 50, 0.4)',
      cardBorder: 'rgba(255, 255, 255, 0.05)',
      textMain: '#E8E5D7',
      textSecondary: '#8B9B90',
      accent: '#9A8C73'
    }
  },
  {
    id: 'ram',
    series: 'SERIES 2/2',
    category: '瑞兽呈祥:十二生肖古风系列',
    name: '吉羊如意',
    abbr: '吉羊',
    date: 'Year of the Ram: Pastoral Whispers',
    title: 'Serenity of the Auspicious Ram',
    subtitle: '吉羊如意',
    descEn: "Embrace the gentle spirit of the auspicious ram amidst serene pastoral landscapes. A design reflecting warmth, contentment, and nature's tranquil embrace.",
    descZh: '群山环绕,翠草茵茵,吉羊温顺,寓意万事顺遂。色泽米棕,点缀翠绿,营造祥和宁静之境,满载美好与满足。',
    tags: ['GENTLE', 'SERENE', 'CONTENT'],
    colors: ['#423930', '#63846C', '#CDB891', '#A69785', '#84796D'],
    theme: {
      bgMain: 'radial-gradient(circle at top left, #F9F7F1 0%, #EAE2D1 100%)',
      cardBg: 'rgba(247, 243, 232, 0.6)',
      cardBorder: 'rgba(0, 0, 0, 0.03)',
      textMain: '#36312B',
      textSecondary: '#8C857B',
      accent: '#B0A28E',
      abbrColor: '#5A5348'   /* 唯一的亮底主题:大字比正文再压一档,避免糊成一团 */
    }
  },
  {
    id: 'dragon',
    series: 'SERIES 2/2',
    category: '瑞兽呈祥:十二生肖古风系列',
    name: '祥龙御天',
    abbr: '祥龙',
    date: 'Epoch of Celestial Majesty',
    title: "The Imperial Dragon's Ascent",
    subtitle: '祥龙御天',
    descEn: "Witness the divine power of the dragon, guardian of the heavens and bringer of prosperity. Its majestic form commands respect, embodying strength and auspicious fortune.",
    descZh: '见证神龙之威,乃九天守护与繁荣赐予者。其雄伟之姿尽显尊崇,寓意力量与祥瑞。',
    tags: ['REGAL', 'CELESTIAL', 'POWERFUL'],
    colors: ['#DEB548', '#408A5F', '#A32929', '#FFFFFF', '#A9A9A9'],
    theme: {
      bgMain: 'radial-gradient(circle at 70% 30%, #2A2E45 0%, #11111A 100%)',
      cardBg: 'rgba(30, 32, 50, 0.5)',
      cardBorder: 'rgba(255, 255, 255, 0.05)',
      textMain: '#D8B762',
      textSecondary: '#7A7E9A',
      accent: '#823742'
    }
  },
  {
    id: 'aquarius',
    series: 'SERIES 1/1',
    category: 'ZODIAC CLASSICS',
    name: 'Aquarius',
    abbr: 'AQ',
    date: 'January 20 - February 18',
    title: 'Aquarius',
    subtitle: '水瓶座',
    descEn: "The visionary Water Bearer pours forth knowledge, innovation, and humanitarian spirit. Embracing individuality, Aquarians illuminate paths to a brighter future with their unique brilliance.",
    descZh: '宝瓶座,承载智慧与革新的水瓶,倾注人道主义精神。拥抱个性,水瓶座以其独特的才华照亮通往更美好未来的道路。',
    tags: ['INNOVATIVE', 'VISIONARY', 'INDEPENDENT'],
    colors: ['#E2E8F0', '#60A5FA', '#93C5FD', '#64748B', '#CBD5E1'],
    theme: {
      bgMain: 'radial-gradient(circle at 20% 80%, #2C3A5A 0%, #15192B 100%)',
      cardBg: 'rgba(35, 43, 65, 0.4)',
      cardBorder: 'rgba(255, 255, 255, 0.05)',
      textMain: '#F1F5F9',
      textSecondary: '#8B9DBB',
      accent: '#5776A5'
    }
  },
  {
    id: 'leo',
    series: 'SERIES 1/1',
    category: 'ZODIAC CLASSICS',
    name: 'Leo',
    abbr: 'LE',
    date: 'July 23 - August 22',
    title: 'Leo',
    subtitle: '狮子座',
    descEn: "Regal and confident, Leo embodies the sun's radiant energy and generous spirit. Their leadership shines brightly, captivating all with warmth and courage.",
    descZh: '狮子座高贵自信,承载着太阳的璀璨能量和慷慨精神。他们的领导力熠熠生辉,以热情和勇气征服一切。',
    tags: ['REGAL', 'CONFIDENT', 'PASSIONATE'],
    colors: ['#FDE047', '#EA580C', '#450A0A', '#FEF08A', '#D97706'],
    theme: {
      bgMain: 'radial-gradient(circle at 50% 50%, #8A1A1A 0%, #4A0B0B 100%)',
      cardBg: 'rgba(100, 15, 15, 0.3)',
      cardBorder: 'rgba(255, 255, 255, 0.1)',
      textMain: '#FCD34D',
      textSecondary: '#DFA896',
      accent: '#B04739'
    }
  }
];

const StarIcon = () => html`
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
  </svg>`;

const AdjustIcon = () => html`
  <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" />
    <path d="M12 2a10 10 0 0 0 0 20z" />
  </svg>`;

const SunIcon = () => html`
  <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="4" />
    <path d="M12 2v2" /><path d="M12 20v2" />
    <path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
    <path d="M2 12h2" /><path d="M20 12h2" />
    <path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
  </svg>`;

/* 大字背后的细线星图装饰 */
const StarMap = ({ strokeColor }) => html`
  <svg className="starmap" viewBox="0 0 400 400" fill="none">
    <path d="M100 150 L300 120" stroke=${strokeColor} strokeWidth="1" />
    <path d="M120 280 L280 180" stroke=${strokeColor} strokeWidth="1" />
    <path d="M180 100 L180 120" stroke=${strokeColor} strokeWidth="1" />
    <path d="M160 110 C 200 80, 250 80, 260 150" stroke=${strokeColor} strokeWidth="0.5" fill="none" />
    <circle cx="120" cy="110" r="1.5" fill=${strokeColor} />
    <circle cx="310" cy="240" r="2" fill=${strokeColor} opacity="0.5" />
  </svg>`;

const App = () => {
  const [activeId, setActiveId] = useState(collectionData[0].id);
  const data = collectionData.find(d => d.id === activeId);
  const t = data.theme;

  /* 全屏背景渐变挂在 body 上,卡片外的一切随主题变奏 */
  useEffect(() => {
    document.body.style.background = t.bgMain;
    document.body.style.color = t.textMain;
  }, [activeId]);

  const themeVars = {
    '--text-main': t.textMain,
    '--text-secondary': t.textSecondary,
    '--accent': t.accent,
    '--card-bg': t.cardBg,
    '--card-border': t.cardBorder
  };

  return html`
    <div className="app" style=${themeVars}>
      <header className="header">
        <div className="brand"><${StarIcon} /><span>CELESTIA</span></div>

        <div className="series">
          <div className="series-no">${data.series}</div>
          <div className="series-cat">${data.category}</div>
          <nav className="nav-tabs">
            ${collectionData.map(item => html`
              <button key=${item.id}
                      className=${'nav-btn' + (item.id === activeId ? ' active' : '')}
                      onClick=${() => setActiveId(item.id)}>${item.name}</button>`)}
          </nav>
        </div>

        <div className="brand-balance"></div>
      </header>

      <main className="stage">
        <div className="card">
          <div className="corner"></div>

          <div className="card-left">
            <${StarMap} strokeColor=${t.accent} />
            <div className="abbr-wrap">
              <h1 className="abbr" style=${{ color: t.abbrColor || t.textMain }}>${data.abbr}</h1>
              <p className="date">${data.date}</p>
            </div>
            <button className="modify-btn"><${AdjustIcon} /><span>MODIFY DESIGN</span></button>
          </div>

          <div className="card-right">
            <div>
              <p className="eyebrow"><span className="rule"></span>AI GENERATED COLLECTION</p>
              <h2 className="title">${data.title}</h2>
              <h3 className="subtitle">${data.subtitle}</h3>
            </div>

            <div className="desc">
              <p>${data.descEn}</p>
              <p>${data.descZh}</p>
            </div>

            <div className="tags">
              ${data.tags.map(tag => html`<span key=${tag} className="tag">${tag}</span>`)}
            </div>

            <div className="card-footer">
              <button className="explore-btn"><span>EXPLORE ATTRIBUTES</span><span>→</span></button>
              <div className="palette-row">
                <span className="micro-label">COLOR SCHEME</span>
                <div className="palette">
                  ${data.colors.map((c, i) => html`
                    <div key=${i} className="swatch" style=${{ backgroundColor: c }} title=${c}></div>`)}
                </div>
                <span className="micro-label">配色方案</span>
              </div>
            </div>
          </div>
        </div>
      </main>

      <div className="sys-icon"><${SunIcon} /></div>
    </div>`;
};

ReactDOM.createRoot(document.getElementById('root')).render(html`<${App} />`);
</script>
</body>
</html>