chore(infra): update build config and deployment scripts
Update Docker configurations, nginx setup, and shared design tokens. Refresh lockfile.
This commit is contained in:
58
Dockerfile.backend
Normal file
58
Dockerfile.backend
Normal 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
30
Dockerfile.frontend
Normal 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
85
branding.json
Normal 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
160
design-tokens.css
Normal 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
89
docker-build-push.sh
Executable 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}"
|
||||
49
docker-compose.coolify.yml
Normal file
49
docker-compose.coolify.yml
Normal 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
66
docker-compose.yml
Normal 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
27
nginx.conf
Normal 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";
|
||||
}
|
||||
300
packages/shared/tailwind-config.mjs
Normal file
300
packages/shared/tailwind-config.mjs
Normal 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
931
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user