Migrate to HeroUI v3 and Tailwind CSS v4

- Upgrade from Tailwind CSS v3 to v4 with CSS-first configuration
- Install HeroUI v3 beta packages (@heroui/react, @heroui/styles)
- Migrate PostCSS config to new @tailwindcss/postcss plugin
- Convert tailwind.config.cjs to CSS @theme directive in globals.css
- Replace @tailwindcss/typography with custom prose styles

Component migrations:
- theme-toggle, back-to-top: HeroUI Button with onPress
- post-card, post-list-item: HeroUI Card compound components
- right-sidebar: HeroUI Card, Avatar, Chip
- search-modal: HeroUI Modal with compound structure
- nav-menu: HeroUI Button for mobile controls
- post-list-with-controls: HeroUI Button for sorting/pagination

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 17:47:36 +08:00
parent 1cd9106ad0
commit 6a9296f33d
14 changed files with 3437 additions and 1370 deletions

View File

@@ -1,8 +1,50 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Tailwind CSS v4 - Must be first */
@import "tailwindcss";
/* HeroUI v3 styles - Must be after Tailwind */
@import "@heroui/styles";
/* Tailwind v4 CSS-first theme configuration */
@theme {
/* Custom colors */
--color-accent: var(--color-accent);
--color-accent-soft: var(--color-accent-soft);
--color-accent-text-light: var(--color-accent-text-light);
--color-accent-text-dark: var(--color-accent-text-dark);
/* Custom font families */
--font-family-serif-eng: var(--font-serif-eng), serif;
--font-family-serif-cn: "Songti SC", "Noto Serif TC", "SimSun", serif;
/* Custom transition timing */
--ease-snappy: cubic-bezier(0.32, 0.72, 0, 1);
/* Custom transition durations */
--duration-180: 180ms;
--duration-260: 260ms;
/* Custom box shadows */
--shadow-lifted: 0 12px 30px -14px rgba(15, 23, 42, 0.25);
--shadow-outline: 0 0 0 1px rgba(59, 130, 246, 0.25);
/* Custom keyframes */
--animate-fade-in-up: fade-in-up 0.6s ease-out both;
--animate-float-soft: float-soft 12s ease-in-out infinite;
}
@keyframes fade-in-up {
0% { opacity: 0; transform: translateY(8px) scale(0.98); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes float-soft {
0% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(4px,-6px,0) scale(1.03); }
100% { transform: translate3d(0,0,0) scale(1); }
}
:root {
/* Motion & layout variables */
--motion-duration-short: 180ms;
--motion-duration-medium: 260ms;
--motion-ease-snappy: cubic-bezier(0.32, 0.72, 0, 1);
@@ -13,22 +55,33 @@
--font-weight-semibold: 600;
--font-system-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'PingFang TC', 'PingFang SC', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Segoe UI Variable Text', 'Segoe UI', 'Microsoft JhengHei', 'Microsoft YaHei', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans CJK TC', 'Noto Sans CJK SC', 'Source Han Sans TC', 'Source Han Sans SC', 'Roboto', 'Ubuntu', 'Cantarell', 'Inter', 'Helvetica Neue', Arial, sans-serif;
/* Ink + accent palette */
/* Ink + accent palette - Light mode */
--color-ink-strong: #0f172a;
--color-ink-body: #1f2937;
--color-ink-muted: #475569;
--color-accent: #7c3aed;
--color-accent-soft: #f4f0ff;
--color-accent-text-light: #7c3aed;
--color-accent-text-dark: #a78bfa;
/* Override HeroUI accent with site purple (oklch conversion) */
--accent: oklch(0.55 0.23 290); /* #7c3aed equivalent */
--accent-foreground: white;
font-size: clamp(15px, 0.65vw + 11px, 19px);
}
.dark {
.dark, [data-theme="dark"] {
--color-ink-strong: #e2e8f0;
--color-ink-body: #cbd5e1;
--color-ink-muted: #94a3b8;
--color-accent: #a78bfa;
--color-accent-soft: #1f1a3d;
--color-accent-text-light: #a78bfa;
--color-accent-text-dark: #a78bfa;
/* Override HeroUI accent for dark mode */
--accent: oklch(0.72 0.16 290); /* #a78bfa equivalent */
}
@media (min-width: 2560px) {
@@ -38,13 +91,22 @@
}
body {
@apply bg-white text-gray-900 transition-colors duration-200 ease-snappy dark:bg-gray-950 dark:text-gray-100;
background-color: white;
color: #111827;
transition-property: color, background-color;
transition-duration: 200ms;
transition-timing-function: var(--motion-ease-snappy);
font-size: 1rem;
line-height: var(--line-height-body);
font-family: var(--font-system-sans);
color: var(--color-ink-body);
}
.dark body {
background-color: #030712;
color: #f3f4f6;
}
@keyframes timeline-scroll {
0% {
transform: translate(-50%, -10%);
@@ -104,49 +166,16 @@ body {
.toc-target-highlight {
@apply bg-yellow-50/60 dark:bg-yellow-900/40 transition-colors duration-500;
background-color: rgb(254 252 232 / 0.6);
transition-property: color, background-color;
transition-duration: 500ms;
}
/* Subtle hover for article elements */
.prose blockquote {
@apply transition-transform transition-shadow duration-180 ease-snappy;
border-left: 4px solid var(--color-accent, #2563eb);
background: linear-gradient(135deg, rgba(124, 58, 237, 0.06), rgba(124, 58, 237, 0.1));
padding: 1.2rem 1.5rem;
font-style: italic;
color: rgba(15, 23, 42, 0.78);
position: relative;
}
.dark .prose blockquote {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.12), rgba(124, 58, 237, 0.08));
color: rgba(226, 232, 240, 0.85);
border-left-color: rgba(167, 139, 250, 0.9);
}
.prose blockquote:hover {
@apply -translate-y-0.5 shadow-sm;
}
.prose blockquote::before {
content: '“';
position: absolute;
top: 0.5rem;
left: 0.8rem;
font-size: 3rem;
font-family: 'Times New Roman', 'Noto Serif TC', serif;
color: rgba(37, 99, 235, 0.25);
pointer-events: none;
}
.prose pre {
@apply transition-transform transition-shadow duration-180 ease-snappy;
}
.prose pre:hover {
@apply -translate-y-0.5 shadow-md;
.dark .toc-target-highlight {
background-color: rgb(113 63 18 / 0.4);
}
/* Prose typography styles (replaces @tailwindcss/typography) */
.prose {
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.2rem);
line-height: var(--line-height-body);
@@ -157,18 +186,40 @@ body {
font-size: clamp(2.2rem, 1.4rem + 2.2vw, 3.4rem);
line-height: 1.25;
color: var(--color-ink-strong);
font-weight: 700;
letter-spacing: -0.03em;
font-family: var(--font-serif-eng), "Songti SC", serif;
margin-top: 0;
margin-bottom: 0.8em;
}
.prose h2 {
font-size: clamp(1.8rem, 1.1rem + 1.6vw, 2.8rem);
line-height: 1.3;
color: var(--color-ink-strong);
font-weight: 600;
letter-spacing: -0.02em;
font-family: var(--font-serif-eng), "Songti SC", serif;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.prose h3 {
font-size: clamp(1.4rem, 0.9rem + 1vw, 2rem);
line-height: 1.35;
color: var(--color-ink-strong);
font-weight: 600;
font-family: var(--font-serif-eng), "Songti SC", serif;
margin-top: 1.3em;
margin-bottom: 0.4em;
}
.prose h4, .prose h5, .prose h6 {
color: var(--color-ink-strong);
font-weight: 600;
font-family: var(--font-serif-eng), "Songti SC", serif;
margin-top: 1.2em;
margin-bottom: 0.4em;
}
.prose p,
@@ -176,6 +227,58 @@ body {
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.15rem);
line-height: var(--line-height-body);
color: var(--color-ink-body);
margin-top: 0;
margin-bottom: 1em;
}
.prose ul, .prose ol {
padding-left: 1.5em;
margin-top: 1em;
margin-bottom: 1em;
}
.prose li {
margin-bottom: 0.5em;
}
.prose a {
color: var(--color-accent-text-light);
text-decoration: underline;
text-underline-offset: 2px;
transition: color var(--motion-duration-short) var(--motion-ease-snappy);
}
.prose a:hover {
color: var(--color-accent);
}
.dark .prose a {
color: var(--color-accent-text-dark);
}
.dark .prose a:hover {
color: var(--color-accent);
}
.prose strong, .prose b {
font-weight: 700;
color: var(--color-ink-strong);
}
.prose em {
font-style: italic;
}
.dark .prose {
color: #e2e8f0;
}
.dark .prose strong, .dark .prose b {
color: #f8fafc;
}
.dark .prose em {
color: #f1f5f9;
}
.prose small,
@@ -193,6 +296,205 @@ body {
color: inherit !important;
}
/* Blockquote styles */
.prose blockquote {
transition-property: transform, box-shadow;
transition-duration: 180ms;
transition-timing-function: var(--motion-ease-snappy);
border-left: 4px solid var(--color-accent, #2563eb);
background: linear-gradient(135deg, rgba(124, 58, 237, 0.06), rgba(124, 58, 237, 0.1));
padding: 1.2rem 1.5rem;
font-style: italic;
color: rgba(15, 23, 42, 0.78);
position: relative;
margin: 1.5em 0;
border-radius: 0 0.5rem 0.5rem 0;
}
.dark .prose blockquote {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.12), rgba(124, 58, 237, 0.08));
color: rgba(226, 232, 240, 0.85);
border-left-color: rgba(167, 139, 250, 0.9);
}
.prose blockquote:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.prose blockquote::before {
content: '"';
position: absolute;
top: 0.5rem;
left: 0.8rem;
font-size: 3rem;
font-family: 'Times New Roman', 'Noto Serif TC', serif;
color: rgba(37, 99, 235, 0.25);
pointer-events: none;
}
.prose blockquote p {
margin: 0;
}
/* Pre/code block styles */
.prose pre {
transition-property: transform, box-shadow;
transition-duration: 180ms;
transition-timing-function: var(--motion-ease-snappy);
overflow-x: auto;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
padding: 1rem 1.2rem;
margin: 1.5rem 0;
background-color: #f8fafc;
}
.dark .prose pre {
border-color: #334155;
background-color: #0f172a;
}
.prose pre:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.prose pre > code {
display: grid;
counter-reset: line;
font-size: 0.9em;
line-height: 1.7;
}
.prose pre > code > [data-line] {
padding: 0 1rem;
border-left: 2px solid transparent;
}
.prose pre > code > [data-line]::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1.5rem;
margin-right: 1.5rem;
text-align: right;
color: #94a3b8;
user-select: none;
}
.dark .prose pre > code > [data-line]::before {
color: #475569;
}
/* Highlighted lines */
.prose pre > code > [data-highlighted-line] {
background-color: rgba(59, 130, 246, 0.1);
border-left-color: rgb(59, 130, 246);
}
.dark .prose pre > code > [data-highlighted-line] {
background-color: rgba(96, 165, 250, 0.15);
border-left-color: rgb(96, 165, 250);
}
/* Inline code */
.prose :not(pre) > code {
border-radius: 0.25rem;
background-color: #f1f5f9;
padding: 0.125rem 0.375rem;
font-size: 0.875rem;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
}
.dark .prose :not(pre) > code {
background-color: #1e293b;
color: #e2e8f0;
}
/* Code title (if specified in markdown: ```js title="example.js") */
.prose [data-rehype-pretty-code-title] {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border: 1px solid #e2e8f0;
border-bottom: 0;
background-color: #f1f5f9;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 600;
color: #475569;
margin-bottom: 0;
}
.dark .prose [data-rehype-pretty-code-title] {
border-color: #334155;
background-color: #1e293b;
color: #cbd5e1;
}
.prose [data-rehype-pretty-code-title] + pre {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
/* Horizontal rule */
.prose hr {
border: 0;
border-top: 1px solid #e2e8f0;
margin: 2em 0;
}
.dark .prose hr {
border-top-color: #334155;
}
/* Tables */
.prose table {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
}
.prose th, .prose td {
border: 1px solid #e2e8f0;
padding: 0.75em 1em;
text-align: left;
}
.prose th {
background-color: #f8fafc;
font-weight: 600;
}
.dark .prose th, .dark .prose td {
border-color: #334155;
}
.dark .prose th {
background-color: #1e293b;
}
/* Images */
.prose img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1.5em 0;
}
.prose figure {
margin: 1.5em 0;
}
.prose figcaption {
text-align: center;
color: var(--color-ink-muted);
margin-top: 0.5em;
}
.hero-title {
position: relative;
display: inline-flex;
@@ -436,112 +738,69 @@ body {
/* Additional custom styling for highlights */
.pagefind-ui__result-excerpt mark {
@apply bg-yellow-200 font-semibold text-slate-900 dark:bg-yellow-600 dark:text-slate-100;
background-color: #fef08a;
font-weight: 600;
color: #0f172a;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.dark .pagefind-ui__result-excerpt mark {
background-color: #ca8a04;
color: #f1f5f9;
}
.pagefind-ui__search-input:focus {
@apply ring-2 ring-blue-500 dark:ring-blue-400;
outline: none;
ring: 2px;
ring-color: #3b82f6;
}
/* Code Syntax Highlighting Styles (rehype-pretty-code) */
.prose pre {
@apply overflow-x-auto rounded-lg border border-slate-200 dark:border-slate-700;
padding: 1rem 1.2rem;
margin: 1.5rem 0;
background-color: #f8fafc;
}
.dark .prose pre {
background-color: #0f172a;
}
.prose pre > code {
@apply grid;
counter-reset: line;
font-size: 0.9em;
line-height: 1.7;
}
.prose pre > code > [data-line] {
padding: 0 1rem;
border-left: 2px solid transparent;
}
.prose pre > code > [data-line]::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1.5rem;
margin-right: 1.5rem;
text-align: right;
color: #94a3b8;
user-select: none;
}
.dark .prose pre > code > [data-line]::before {
color: #475569;
}
/* Highlighted lines */
.prose pre > code > [data-highlighted-line] {
background-color: rgba(59, 130, 246, 0.1);
border-left-color: rgb(59, 130, 246);
}
.dark .prose pre > code > [data-highlighted-line] {
background-color: rgba(96, 165, 250, 0.15);
border-left-color: rgb(96, 165, 250);
}
/* Inline code */
.prose :not(pre) > code {
@apply rounded bg-slate-100 px-1.5 py-0.5 text-sm font-semibold text-slate-800 dark:bg-slate-800 dark:text-slate-200;
white-space: nowrap;
}
/* Code title (if specified in markdown: ```js title="example.js") */
.prose [data-rehype-pretty-code-title] {
@apply rounded-t-lg border border-b-0 border-slate-200 bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300;
margin-bottom: 0;
}
.prose [data-rehype-pretty-code-title] + pre {
@apply mt-0 rounded-t-none;
.dark .pagefind-ui__search-input:focus {
ring-color: #60a5fa;
}
/* GitHub-style Callouts/Alerts */
.prose .callout {
@apply my-6 rounded-lg border-l-4 p-4 shadow-sm;
margin: 1.5rem 0;
border-radius: 0.5rem;
border-left-width: 4px;
padding: 1rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
background: linear-gradient(135deg, var(--callout-bg-start), var(--callout-bg-end));
}
.prose .callout-header {
@apply mb-3 flex items-center gap-2;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.prose .callout-icon {
@apply text-2xl;
font-size: 1.5rem;
line-height: 1;
}
.prose .callout-title {
@apply text-sm font-bold uppercase tracking-wider;
color: var(--callout-title-color);
font-size: 0.875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--callout-title-color);
}
.prose .callout-content {
@apply text-sm leading-relaxed;
font-size: 0.875rem;
line-height: 1.625;
}
.prose .callout-content > *:first-child {
@apply mt-0;
margin-top: 0;
}
.prose .callout-content > *:last-child {
@apply mb-0;
margin-bottom: 0;
}
/* NOTE - Blue */
@@ -549,14 +808,14 @@ body {
--callout-bg-start: rgba(59, 130, 246, 0.08);
--callout-bg-end: rgba(59, 130, 246, 0.04);
--callout-title-color: #2563eb;
@apply border-blue-500;
border-left-color: #3b82f6;
}
.dark .prose .callout-note {
--callout-bg-start: rgba(96, 165, 250, 0.12);
--callout-bg-end: rgba(96, 165, 250, 0.06);
--callout-title-color: #93c5fd;
@apply border-blue-400;
border-left-color: #60a5fa;
}
/* TIP - Green */
@@ -564,14 +823,14 @@ body {
--callout-bg-start: rgba(34, 197, 94, 0.08);
--callout-bg-end: rgba(34, 197, 94, 0.04);
--callout-title-color: #16a34a;
@apply border-green-500;
border-left-color: #22c55e;
}
.dark .prose .callout-tip {
--callout-bg-start: rgba(74, 222, 128, 0.12);
--callout-bg-end: rgba(74, 222, 128, 0.06);
--callout-title-color: #86efac;
@apply border-green-400;
border-left-color: #4ade80;
}
/* IMPORTANT - Purple */
@@ -579,14 +838,14 @@ body {
--callout-bg-start: rgba(168, 85, 247, 0.08);
--callout-bg-end: rgba(168, 85, 247, 0.04);
--callout-title-color: #9333ea;
@apply border-purple-500;
border-left-color: #a855f7;
}
.dark .prose .callout-important {
--callout-bg-start: rgba(192, 132, 252, 0.12);
--callout-bg-end: rgba(192, 132, 252, 0.06);
--callout-title-color: #c084fc;
@apply border-purple-400;
border-left-color: #c084fc;
}
/* WARNING - Orange/Yellow */
@@ -594,14 +853,14 @@ body {
--callout-bg-start: rgba(251, 191, 36, 0.08);
--callout-bg-end: rgba(251, 191, 36, 0.04);
--callout-title-color: #d97706;
@apply border-yellow-500;
border-left-color: #f59e0b;
}
.dark .prose .callout-warning {
--callout-bg-start: rgba(253, 224, 71, 0.12);
--callout-bg-end: rgba(253, 224, 71, 0.06);
--callout-title-color: #fde047;
@apply border-yellow-400;
border-left-color: #fde047;
}
/* CAUTION - Red */
@@ -609,12 +868,12 @@ body {
--callout-bg-start: rgba(239, 68, 68, 0.08);
--callout-bg-end: rgba(239, 68, 68, 0.04);
--callout-title-color: #dc2626;
@apply border-red-500;
border-left-color: #ef4444;
}
.dark .prose .callout-caution {
--callout-bg-start: rgba(248, 113, 113, 0.12);
--callout-bg-end: rgba(248, 113, 113, 0.06);
--callout-title-color: #fca5a5;
@apply border-red-400;
border-left-color: #f87171;
}