chore(infra): update build config and deployment scripts

Update Docker configurations, nginx setup, and shared design tokens. Refresh lockfile.
This commit is contained in:
2026-02-11 11:50:04 +08:00
parent e9897388dc
commit 8ca609a889
10 changed files with 1737 additions and 58 deletions

58
Dockerfile.backend Normal file
View File

@@ -0,0 +1,58 @@
# Payload CMS Dockerfile - Minimal ~200MB build
# Uses node-linker=hoisted to avoid pnpm symlinks in standalone output
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.17.0 --activate
# Copy workspace files
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/backend/package.json ./apps/backend/
COPY packages/shared/package.json ./packages/shared/
# Install dependencies with hoisted node_modules (no symlinks)
# This makes standalone output work correctly
RUN pnpm install --frozen-lockfile --config.node-linker=hoisted
# Copy source files
COPY apps/backend ./apps/backend
COPY packages/shared ./packages/shared
# Build Next.js with standalone output
WORKDIR /app/apps/backend
RUN pnpm run build
# Stage 2: Production (minimal image)
FROM node:22-alpine AS runner
# Monorepo standalone structure: server.js is at apps/backend/server.js
WORKDIR /app/apps/backend
ENV NODE_ENV=production
ENV PORT=3000
# Combine user creation into single layer
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy standalone (preserves monorepo structure with node_modules at root)
COPY --from=builder --chown=nextjs:nodejs /app/apps/backend/.next/standalone ./../../
# Copy static assets (not included in standalone by default)
COPY --from=builder --chown=nextjs:nodejs /app/apps/backend/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/backend/public ./public
USER nextjs
EXPOSE 3000
# Health check for container orchestration
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/', (r) => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))" || exit 1
# Monorepo path: server.js is at /app/apps/backend/server.js
CMD ["node", "server.js"]

30
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,30 @@
# Frontend Dockerfile - Multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm run build
# Production stage - nginx for static site
FROM nginx:alpine AS production
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 4321
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

85
branding.json Normal file
View File

@@ -0,0 +1,85 @@
{
"colorScheme": "light",
"fonts": [
{
"family": "sans-serif",
"count": 73
},
{
"family": "Noto Sans TC",
"count": 68
},
{
"family": "Quicksand",
"count": 5
}
],
"colors": {
"primary": "#0000EE",
"accent": "#23608C",
"background": "#F2F2F2",
"textPrimary": "#23608C",
"link": "#23608C"
},
"typography": {
"fontFamilies": {
"primary": "Noto Sans TC",
"heading": "Noto Sans TC"
},
"fontStacks": {
"body": [
"Noto Sans TC",
"sans-serif"
],
"heading": [
"Noto Sans TC",
"sans-serif"
],
"paragraph": [
"Quicksand",
"sans-serif"
]
},
"fontSizes": {
"h1": "64.41px",
"h2": "46px",
"body": "29.64px"
}
},
"spacing": {
"baseUnit": 8,
"borderRadius": "0px"
},
"components": {},
"images": {
"logo": "https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f24aa108528b3886942cba_enchun.svg",
"favicon": "https://cdn.prod.website-files.com/61f24aa108528b1962942c95/6200ec44d8b8b96d8b782995_enchun%20ico-.png",
"ogImage": "https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f6972f64ea04254d02b655_Enchun%20digital%20marketing.png"
},
"__framework_hints": [
"Webflow"
],
"__llm_logo_reasoning": {
"selectedIndex": 1,
"reasoning": "Strong indicators: header logo linking to homepage, header location, visible, class contains logo, reasonable size. Score: 95 (clear winner by 35 points)",
"confidence": 0.9
},
"__llm_button_reasoning": {
"primary": {
"index": -1,
"text": "N/A",
"reasoning": "LLM failed"
},
"secondary": {
"index": -1,
"text": "N/A",
"reasoning": "LLM failed"
},
"confidence": 0
},
"confidence": {
"buttons": 0,
"colors": 0,
"overall": 0
}
}

160
design-tokens.css Normal file
View File

@@ -0,0 +1,160 @@
/**
* Design Tokens for enchun.tw
* Generated from branding.json
*/
:root {
/* ========================================
Color Tokens
======================================== */
/* Primary Colors */
--color-primary: #0000EE;
--color-accent: #23608C;
/* Background Colors */
--color-background: #F2F2F2;
--color-background-light: #FFFFFF;
--color-background-dark: #E5E5E5;
/* Text Colors */
--color-text-primary: #23608C;
--color-text-secondary: #666666;
--color-text-muted: #999999;
/* Link Colors */
--color-link: #23608C;
--color-link-hover: #1A4A6A;
--color-link-visited: #0000EE;
/* ========================================
Typography Tokens
======================================== */
/* Font Families */
--font-family-primary: 'Noto Sans TC', sans-serif;
--font-family-heading: 'Noto Sans TC', sans-serif;
--font-family-paragraph: 'Quicksand', sans-serif;
--font-family-body: 'Noto Sans TC', sans-serif;
/* Font Stacks (fallbacks included) */
--font-stack-body: 'Noto Sans TC', sans-serif;
--font-stack-heading: 'Noto Sans TC', sans-serif;
--font-stack-paragraph: 'Quicksand', sans-serif;
/* Font Sizes */
--font-size-h1: 64.41px;
--font-size-h2: 46px;
--font-size-h3: 32px;
--font-size-h4: 24px;
--font-size-body: 29.64px;
--font-size-small: 14px;
--font-size-xs: 12px;
/* Font Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Line Heights */
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* ========================================
Spacing Tokens
======================================== */
/* Base Unit: 8px */
--spacing-unit: 8px;
/* Spacing Scale */
--spacing-0: 0;
--spacing-1: 8px; /* 1 unit */
--spacing-2: 16px; /* 2 units */
--spacing-3: 24px; /* 3 units */
--spacing-4: 32px; /* 4 units */
--spacing-5: 40px; /* 5 units */
--spacing-6: 48px; /* 6 units */
--spacing-8: 64px; /* 8 units */
--spacing-10: 80px; /* 10 units */
--spacing-12: 96px; /* 12 units */
--spacing-16: 128px; /* 16 units */
/* ========================================
Border Tokens
======================================== */
--border-radius-none: 0px;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 16px;
--border-radius-full: 9999px;
/* Default border radius from branding */
--border-radius: 0px;
--border-width-thin: 1px;
--border-width-medium: 2px;
--border-width-thick: 4px;
/* ========================================
Shadow Tokens
======================================== */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* ========================================
Transition Tokens
======================================== */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 400ms ease;
/* ========================================
Z-Index Tokens
======================================== */
--z-index-dropdown: 100;
--z-index-sticky: 200;
--z-index-fixed: 300;
--z-index-modal-backdrop: 400;
--z-index-modal: 500;
--z-index-popover: 600;
--z-index-tooltip: 700;
/* ========================================
Asset URLs
======================================== */
--url-logo: url('https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f24aa108528b3886942cba_enchun.svg');
--url-favicon: url('https://cdn.prod.website-files.com/61f24aa108528b1962942c95/6200ec44d8b8b96d8b782995_enchun%20ico-.png');
--url-og-image: url('https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f6972f64ea04254d02b655_Enchun%20digital%20marketing.png');
}
/* ========================================
Color Scheme: Light (default)
======================================== */
[data-theme="light"],
:root {
color-scheme: light;
}
/* ========================================
Dark Mode Override (optional)
======================================== */
[data-theme="dark"] {
--color-background: #1a1a1a;
--color-background-light: #2d2d2d;
--color-background-dark: #0d0d0d;
--color-text-primary: #e0e0e0;
--color-text-secondary: #b0b0b0;
--color-text-muted: #808080;
}

89
docker-build-push.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Enchun Docker Build & Push ===${NC}"
echo ""
# Configuration
IMAGE_PREFIX="${DOCKER_USERNAME:-pukpuklouis}"
BACKEND_IMAGE="${IMAGE_PREFIX}/website-enchun-cms"
FRONTEND_IMAGE="${IMAGE_PREFIX}/website-enchun-web"
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo -e "${RED}Error: Docker is not installed or not running${NC}"
exit 1
fi
# Check if logged in to Docker Hub
if ! docker info | grep -q "Username"; then
echo -e "${YELLOW}Warning: Not logged in to Docker Hub${NC}"
echo -e "Please run: ${GREEN}docker login${NC}"
exit 1
fi
# Function to build and push image
build_and_push() {
local service=$1
local image=$2
local dockerfile=$3
local context=$4
echo -e "${YELLOW}Building ${service}...${NC}"
echo -e " Dockerfile: ${dockerfile}"
echo -e " Context: ${context}"
docker build -t "${image}" -f "${dockerfile}" "${context}"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to build ${service}${NC}"
exit 1
fi
echo -e "${YELLOW}Pushing ${image}...${NC}"
docker push "${image}"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to push ${image}${NC}"
exit 1
fi
echo -e "${GREEN}${service} built and pushed successfully${NC}"
}
# Main menu
echo "Select option:"
echo "1) Build and push frontend only"
echo "2) Build and push backend only"
echo "3) Build and push both"
echo "4) Exit"
echo ""
read -p "Enter choice: " choice
case $choice in
1)
build_and_push "frontend" "${FRONTEND_IMAGE}" "Dockerfile.frontend" "."
;;
2)
build_and_push "backend" "${BACKEND_IMAGE}" "Dockerfile.backend" "."
;;
3)
build_and_push "frontend" "${FRONTEND_IMAGE}" "Dockerfile.frontend" "."
build_and_push "backend" "${BACKEND_IMAGE}" "Dockerfile.backend" "."
;;
4)
echo "Exiting..."
exit 0
;;
*)
echo -e "${RED}Invalid option${NC}"
exit 1
;;
esac
echo -e "${GREEN}=== Done ===${NC}"

View File

@@ -0,0 +1,49 @@
# Docker Compose for Coolify deployment
# Use this for "Docker Compose" deployment type in Coolify
services:
enchun-cms:
image: pukpuklouis/website-enchun-cms:latest
container_name: enchun-cms
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000 # Explicitly set to match EXPOSE
- DATABASE_URI=${DATABASE_URI}
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
- NEXT_PUBLIC_SERVER_URL=${NEXT_PUBLIC_SERVER_URL:-https://enchun-admin.anlstudio.cc}
# Expose port 3000 (matches Next.js default)
expose:
- "3000"
# Health check
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:3000/', (r) => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Traefik labels for routing
labels:
- "traefik.enable=true"
- "traefik.http.routers.enchun-cms.rule=Host(`enchun-admin.anlstudio.cc`)"
- "traefik.http.routers.enchun-cms.entrypoints=http"
- "traefik.http.services.enchun-cms.loadbalancer.server.port=3000"
- "traefik.http.middlewares.enchun-gzip.compress=true"
- "traefik.http.routers.enchun-cms.middlewares=enchun-gzip"
networks:
- coolify
networks:
coolify:
external: true

66
docker-compose.yml Normal file
View File

@@ -0,0 +1,66 @@
version: '3.8'
services:
# Frontend (Astro) - Static site served with nginx
frontend:
build:
context: ./apps/frontend
dockerfile: Dockerfile.frontend
image: enchun-frontend:latest
container_name: enchun-frontend
ports:
- "4321:4321"
networks:
- enchun-network
environment:
- NODE_ENV=production
# Backend (Payload CMS)
backend:
build:
context: ./apps/backend
dockerfile: Dockerfile.backend
image: enchun-backend:latest
container_name: enchun-backend
ports:
- "3000:3000"
environment:
# Database
- DATABASE_URI=${DATABASE_URI}
# Payload
- PAYLOAD_CMS_URL=http://localhost:3000
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
- NEXT_PUBLIC_SERVER_URL=${NEXT_PUBLIC_SERVER_URL}
# Preview
- PREVIEW_SECRET=${PREVIEW_SECRET}
# Resend
- RESEND_API_KEY=${RESEND_API_KEY}
- RESEND_FROM_EMAIL=${RESEND_FROM_EMAIL}
# R2
- R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
- R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
- R2_BUCKET=${R2_BUCKET}
- R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
# Velcer
- VERCEL_PROJECT_PRODUCTION_URL=${VERCEL_PROJECT_PRODUCTION_URL}
# Node options
- NODE_OPTIONS=--no-deprecation
networks:
- enchun-network
depends_on:
- frontend
# Optional: MongoDB (if you need a containerized database)
# mongodb:
# image: mongo:7
# container_name: enchun-mongodb
# ports:
# - "27017:27017"
# volumes:
# - mongodb_data:/data/db
# networks:
# - enchun-network
networks:
enchun-network:
driver: bridge

27
nginx.conf Normal file
View File

@@ -0,0 +1,27 @@
server {
listen 4321;
server_name _;
root /usr/share/nginx/html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/rss+xml text/x-component text/x-cross-domain-policy;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
}
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
}

View File

@@ -0,0 +1,300 @@
/**
* Shared Tailwind CSS Configuration
* Designed for Enchun Digital Marketing website migration
*
* Source: Webflow CSS extraction
* Date: 2026-01-31
*/
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: 'class', // Can use 'media' preference-based later
content: [],
theme: {
extend: {
// ============================================
// 🎨 COLORS - From Webflow Extraction
// ============================================
colors: {
// Primary Colors (主要色)
primary: {
DEFAULT: '#3898EC',
dark: '#0082F3',
light: '#67AEE1',
hover: '#2895F7',
DEFAULTContrast: '#FFFFFF',
},
secondary: {
DEFAULT: '#F39C12',
light: '#F6C456',
dark: '#D84038',
hover: '#E08E0B',
},
accent: {
DEFAULT: '#D84038',
light: '#F6C456',
dark: '#EA384C',
},
// Neutral Colors (中性色 - Mapped to Tailwind defaults)
white: '#FFFFFF',
black: '#000000',
slate: {
50: '#FAFAFA', // --color-gray-50
100: '#F5F5F5', // --color-gray-100
200: '#F3F3F3', // --color-gray-200
300: '#EEEEEE', // --color-gray-300
400: '#DDDDDD', // --color-gray-400
500: '#C8C8C8', // --color-gray-500
600: '#999999', // --color-gray-600 (text-muted)
700: '#828282', // --color-gray-700
800: '#758696', // --color-gray-800
900: '#5D6C7B', // --color-gray-900
950: '#4F4F4F', // --color-gray-950
},
// Text Colors (文字色)
text: {
primary: '#333333', // --color-text-primary
secondary: '#222222', // --color-text-secondary
muted: '#999999', // --color-text-muted
light: '#758696', // --color-text-light
inverse: '#FFFFFF', // --color-text-inverse
},
// Link Colors (連結色)
link: {
DEFAULT: '#3083BF',
hover: '#23608C',
},
// Border & Divider (邊框與分隔線)
border: '#E2E8F0',
divider: '#DDDDDD',
// Category Colors (文章分類)
category: {
google: '#67AEE1', // Google小學堂
meta: '#8974DE', // Meta小學堂
news: '#3083BF', // 行銷時事最前線
enchun: '#3898EC', // 恩群數位最新公告
},
// Badge Colors (標籤)
badge: {
hot: '#EA384C', // Hot 標籤 (紅)
new: '#67AEE1', // New 標籤 (淡藍)
},
// Special Colors
background: '#F2F2F2', // Grey 6 - Main background from Webflow
surface: '#FAFAFA',
surface2: '#F3F3F3',
},
// ============================================
// 🔤 TYPOGRAPHY - From Webflow
// ============================================
fontFamily: {
sans: [
'Noto Sans TC',
'Quicksand',
'Arial',
'sans-serif',
],
heading: [
'Noto Sans TC',
'Quicksand',
'Arial',
'sans-serif',
],
accent: [
'Quicksand',
'Noto Sans TC',
'sans-serif',
],
},
fontSize: {
// Webflow-based scale
'xs': ['0.75rem', { lineHeight: '1.5' }], // 12px
'sm': ['0.875rem', { lineHeight: '1.5' }], // 14px
'base': ['1rem', { lineHeight: '1.5' }], // 16px
'lg': ['1.125rem', { lineHeight: '1.5' }], // 18px
'xl': ['1.25rem', { lineHeight: '1.5' }], // 20px
'2xl': ['1.5rem', { lineHeight: '1.5' }], // 24px
'3xl': ['1.875rem', { lineHeight: '1.5' }], // 30px
'4xl': ['2.25rem', { lineHeight: '1.5' }], // 36px
'5xl': ['3rem', { lineHeight: '1.5' }], // 48px
// Extra sizes
'6xl': ['3.75rem', { lineHeight: '1' }], // 60px
'7xl': ['4.5rem', { lineHeight: '1' }], // 72px
},
fontWeight: {
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
},
lineHeight: {
tight: '1.25',
snug: '1.375',
normal: '1.5',
relaxed: '1.625',
loose: '2',
},
// ============================================
// 📏 SPACING - Tailwind Default + Custom
// ============================================
spacing: {
'18': '4.5rem', // 72px - container padding
'20': '5rem', // 80px - section padding
'24': '6rem', // 96px - large section
'32': '8rem', // 128px - extra large section
},
// ============================================
// 📱 BREAKPOINTS - Webflow Original
// ============================================
screens: {
// Webflow breakpoints (max-width based)
'xs': '479px', // < 479px (small mobile)
'sm': '640px', // 640px+ (mobile landscape)
'md': '768px', // 768px+ (tablet)
'lg': '992px', // 992px+ (desktop - matches 991px closely)
'xl': '1024px', // 1024px+ (large desktop)
'2xl': '1280px', // 1280px+ (xlarge desktop)
'3xl': '1536px', // 1536px+ (xxlarge desktop)
},
// ============================================
// 🔲 BORDER RADIUS
// ============================================
borderRadius: {
DEFAULT: '0.375rem', // 6px
'none': '0',
'sm': '0.125rem', // 2px
DEFAULT: '0.375rem', // 6px
'md': '0.5rem', // 8px
'lg': '0.75rem', // 12px
'xl': '1rem', // 16px
'2xl': '1.5rem', // 24px
'3xl': '2rem', // 32px
'full': '9999px',
},
// ============================================
// 💫 SHADOWS
// ============================================
boxShadow: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
},
// ============================================
// ⏱ TRANSITIONS
// ============================================
transitionDuration: {
'150': '150ms',
'200': '200ms',
'250': '250ms',
'300': '300ms',
'350': '350ms',
'400': '400ms',
'500': '500ms',
'600': '600ms',
'700': '700ms',
'1000': '1000ms',
},
transitionTimingFunction: {
DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
'in': 'cubic-bezier(0.4, 0, 1, 1)',
'out': 'cubic-bezier(0, 0, 0.2, 1)',
'in-out': 'cubic-bezier(0.4, 0, 0.6, 1)',
},
// ============================================
// 🎯 Z-INDEX LAYERS
// ============================================
zIndex: {
dropdown: 1000,
sticky: 1020,
modal: 1040,
popover: 1060,
tooltip: 1080,
},
// ============================================
// 📐 LAYOUT
// ============================================
maxWidth: {
'container': '1200px', // Max content width
'container-sm': '640px',
'container-md': '768px',
'container-lg': '1024px',
'container-xl': '1280px',
},
// Container padding
padding: {
'container': '1.5rem', // 24px mobile
'container-lg': '2rem', // 32px desktop
},
// Grid (Webflow uses 12-column grid)
gridTemplateColumns: {
12: 'repeat(12, minmax(0, 1fr))',
},
gap: {
'DEFAULT': '1.25rem', // 20px - Webflow grid gap
'0': '0',
'px': '1px',
},
// ============================================
// 🎨 COMPONENT SPECIFIC
// ============================================
// Button styles
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(1rem)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
animation: {
'fade-in': 'fadeIn 150ms ease-in-out',
'slide-up': 'slideUp 250ms ease-in-out',
},
},
},
plugins: [
require('@tailwindcss/typography'),
],
};
export default config;

931
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff