Migrate to Tailwind CSS v4 with CSS-first configuration
- Replace tailwindcss v3 + autoprefixer with tailwindcss v4 + @tailwindcss/postcss - Migrate tailwind.config.cjs theme to @theme block in globals.css - Add @custom-variant dark for class-based dark mode (next-themes) - Load typography plugin via @plugin directive, replace prose-dark with prose-invert - Convert prose dark mode overrides from JS config to CSS (.dark .prose rules) - Add @source directive for content submodule detection - Replace postcss.config.cjs with postcss.config.mjs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,12 +39,13 @@ No test framework is configured.
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
- Tailwind CSS v3 with `darkMode: 'class'` (toggled by `next-themes`)
|
- Tailwind CSS v4 with CSS-first configuration (no `tailwind.config.cjs`)
|
||||||
|
- Dark mode via `@custom-variant dark` in `styles/globals.css` (class-based, toggled by `next-themes`)
|
||||||
|
- Theme customization via `@theme` block in `styles/globals.css`: colors, fonts, easing, durations, shadows, keyframes, animations
|
||||||
- Accent color system via CSS variables set in `app/layout.tsx` from env vars: `--color-accent`, `--color-accent-soft`, `--color-accent-text-light`, `--color-accent-text-dark`
|
- Accent color system via CSS variables set in `app/layout.tsx` from env vars: `--color-accent`, `--color-accent-soft`, `--color-accent-text-light`, `--color-accent-text-dark`
|
||||||
- Tailwind extends these as `accent`, `accent-soft`, `accent-textLight`, `accent-textDark` in `tailwind.config.cjs`
|
- Typography plugin (`@tailwindcss/typography`) loaded via `@plugin` directive; prose dark mode handled by custom `.dark .prose` CSS overrides
|
||||||
- Typography plugin (`@tailwindcss/typography`) with custom `prose` / `prose-dark` overrides
|
|
||||||
- English headings use Playfair Display serif (`--font-serif-eng`); body uses Inter + CJK fallback stack
|
- English headings use Playfair Display serif (`--font-serif-eng`); body uses Inter + CJK fallback stack
|
||||||
- Config files use CommonJS (`.cjs`): `tailwind.config.cjs`, `postcss.config.cjs`
|
- PostCSS config: `postcss.config.mjs` using `@tailwindcss/postcss`
|
||||||
|
|
||||||
## Content Submodule
|
## Content Submodule
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<article
|
<article
|
||||||
data-toc-content={slug}
|
data-toc-content={slug}
|
||||||
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark"
|
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-invert"
|
||||||
>
|
>
|
||||||
{post.feature_image && (
|
{post.feature_image && (
|
||||||
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default async function StaticPage({ params }: Props) {
|
|||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<article
|
<article
|
||||||
data-toc-content={slug}
|
data-toc-content={slug}
|
||||||
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark"
|
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-invert"
|
||||||
>
|
>
|
||||||
{page.feature_image && (
|
{page.feature_image && (
|
||||||
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
988
package-lock.json
generated
988
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,17 +38,17 @@
|
|||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"autoprefixer": "^10.4.22",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^16.0.3",
|
"eslint-config-next": "^16.0.3",
|
||||||
"pagefind": "^1.4.0",
|
"pagefind": "^1.4.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,50 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@plugin "@tailwindcss/typography";
|
||||||
@tailwind utilities;
|
|
||||||
|
/* Class-based dark mode for next-themes */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
/* Auto-detect content in the submodule */
|
||||||
|
@source "../content";
|
||||||
|
|
||||||
|
/* Tailwind v4 CSS-first theme configuration */
|
||||||
|
@theme {
|
||||||
|
/* Custom colors (CSS variable references) */
|
||||||
|
--color-accent: var(--color-accent);
|
||||||
|
--color-accent-soft: var(--color-accent-soft);
|
||||||
|
--color-accent-textLight: var(--color-accent-text-light);
|
||||||
|
--color-accent-textDark: var(--color-accent-text-dark);
|
||||||
|
|
||||||
|
/* Custom font families */
|
||||||
|
--font-serif-eng: var(--font-serif-eng), serif;
|
||||||
|
--font-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 {
|
:root {
|
||||||
--motion-duration-short: 180ms;
|
--motion-duration-short: 180ms;
|
||||||
@@ -129,7 +173,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose blockquote::before {
|
.prose blockquote::before {
|
||||||
content: '“';
|
content: '\201C';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
left: 0.8rem;
|
left: 0.8rem;
|
||||||
@@ -147,22 +191,45 @@ body {
|
|||||||
@apply -translate-y-0.5 shadow-md;
|
@apply -translate-y-0.5 shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typography plugin prose overrides */
|
||||||
.prose {
|
.prose {
|
||||||
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.2rem);
|
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.2rem);
|
||||||
line-height: var(--line-height-body);
|
line-height: var(--line-height-body);
|
||||||
color: var(--color-ink-body);
|
color: var(--color-ink-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
color: var(--color-accent-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 h1 {
|
.prose h1 {
|
||||||
font-size: clamp(2.2rem, 1.4rem + 2.2vw, 3.4rem);
|
font-size: clamp(2.2rem, 1.4rem + 2.2vw, 3.4rem);
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
color: var(--color-ink-strong);
|
color: var(--color-ink-strong);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
font-family: var(--font-serif-eng), "Songti SC", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h2 {
|
.prose h2 {
|
||||||
font-size: clamp(1.8rem, 1.1rem + 1.6vw, 2.8rem);
|
font-size: clamp(1.8rem, 1.1rem + 1.6vw, 2.8rem);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
color: var(--color-ink-strong);
|
color: var(--color-ink-strong);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: var(--font-serif-eng), "Songti SC", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h3 {
|
.prose h3 {
|
||||||
@@ -183,6 +250,26 @@ body {
|
|||||||
font-size: clamp(0.85rem, 0.2vw + 0.8rem, 0.95rem);
|
font-size: clamp(0.85rem, 0.2vw + 0.8rem, 0.95rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode prose overrides (replaces prose-dark) */
|
||||||
|
.dark .prose {
|
||||||
|
color: var(--color-ink-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose strong,
|
||||||
|
.dark .prose b {
|
||||||
|
color: #f8fafc;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose em {
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h1,
|
||||||
|
.dark .prose h2 {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
.prose h1>a,
|
.prose h1>a,
|
||||||
.prose h2>a,
|
.prose h2>a,
|
||||||
.prose h3>a,
|
.prose h3>a,
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: [
|
|
||||||
"./app/**/*.{ts,tsx}",
|
|
||||||
"./components/**/*.{ts,tsx}",
|
|
||||||
"./content/**/*.{md,mdx}"
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
accent: {
|
|
||||||
DEFAULT: 'var(--color-accent)',
|
|
||||||
soft: 'var(--color-accent-soft)',
|
|
||||||
textLight: 'var(--color-accent-text-light)',
|
|
||||||
textDark: 'var(--color-accent-text-dark)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
'serif-eng': ['var(--font-serif-eng)', 'serif'],
|
|
||||||
'serif-cn': ['"Songti SC"', '"Noto Serif TC"', '"SimSun"', 'serif'],
|
|
||||||
},
|
|
||||||
transitionTimingFunction: {
|
|
||||||
snappy: 'cubic-bezier(0.32, 0.72, 0, 1)'
|
|
||||||
},
|
|
||||||
transitionDuration: {
|
|
||||||
180: '180ms',
|
|
||||||
260: '260ms'
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
lifted: '0 12px 30px -14px rgba(15, 23, 42, 0.25)',
|
|
||||||
outline: '0 0 0 1px rgba(59, 130, 246, 0.25)'
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
'fade-in-up': {
|
|
||||||
'0%': { opacity: '0', transform: 'translateY(8px) scale(0.98)' },
|
|
||||||
'100%': { opacity: '1', transform: 'translateY(0) scale(1)' }
|
|
||||||
},
|
|
||||||
'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)' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'fade-in-up': 'fade-in-up 0.6s ease-out both',
|
|
||||||
'float-soft': 'float-soft 12s ease-in-out infinite'
|
|
||||||
},
|
|
||||||
typography: (theme) => ({
|
|
||||||
DEFAULT: {
|
|
||||||
css: {
|
|
||||||
color: theme('colors.slate.700'),
|
|
||||||
a: {
|
|
||||||
color: 'var(--color-accent-text-light)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'var(--color-accent)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
h1: {
|
|
||||||
fontWeight: '700',
|
|
||||||
letterSpacing: '-0.03em',
|
|
||||||
fontFamily: 'var(--font-serif-eng), "Songti SC", serif',
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
fontWeight: '600',
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
fontFamily: 'var(--font-serif-eng), "Songti SC", serif',
|
|
||||||
},
|
|
||||||
blockquote: {
|
|
||||||
fontStyle: 'normal',
|
|
||||||
borderLeftColor: 'var(--color-accent-soft)',
|
|
||||||
color: theme('colors.slate.700'),
|
|
||||||
backgroundColor: theme('colors.slate.50')
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
backgroundColor: theme('colors.slate.100'),
|
|
||||||
padding: '0.15rem 0.35rem',
|
|
||||||
borderRadius: '0.25rem'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
css: {
|
|
||||||
// Slightly softer than pure white for body text
|
|
||||||
color: theme('colors.slate.200'),
|
|
||||||
a: {
|
|
||||||
color: 'var(--color-accent-text-dark)',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'var(--color-accent)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
strong: {
|
|
||||||
color: theme('colors.slate.50'),
|
|
||||||
fontWeight: '700'
|
|
||||||
},
|
|
||||||
b: {
|
|
||||||
color: theme('colors.slate.50'),
|
|
||||||
fontWeight: '700'
|
|
||||||
},
|
|
||||||
em: {
|
|
||||||
color: theme('colors.slate.100')
|
|
||||||
},
|
|
||||||
h1: {
|
|
||||||
color: theme('colors.slate.50')
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
color: theme('colors.slate.50')
|
|
||||||
},
|
|
||||||
blockquote: {
|
|
||||||
borderLeftColor: 'var(--color-accent)',
|
|
||||||
backgroundColor: theme('colors.slate.800'),
|
|
||||||
color: theme('colors.slate.200'),
|
|
||||||
p: {
|
|
||||||
color: theme('colors.slate.200')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
backgroundColor: theme('colors.slate.800')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography')],
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user