commit 065d92636a7afefc4d1a442cfbdc475aec57dbcd Author: Kunthawat Greethong Date: Thu Apr 30 20:58:06 2026 +0700 feat: add official EmDash marketing template - Marketing landing page with hero, features, testimonials, FAQ, pricing - EmDash CMS with pages collection and marketing blocks - Full seed data with all content sections - Dockerfile with entrypoint for database persistence - Responsive design with CSS variables Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..144658d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +data.db +uploads/ +.env +*.log \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fc58037 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +This is an EmDash site -- a CMS built on Astro with a full admin UI. + +## Commands + +```bash +npx emdash dev # Start dev server (runs migrations, seeds, generates types) +npx emdash types # Regenerate TypeScript types from schema +npx emdash seed seed/seed.json --validate # Validate seed file +``` + +The admin UI is at `http://localhost:4321/_emdash/admin`. + +## Key Files + +| File | Purpose | +| ------------------------ | ---------------------------------------------------------------------------------- | +| `astro.config.mjs` | Astro config with `emdash()` integration, database, and storage | +| `src/live.config.ts` | EmDash loader registration (boilerplate -- don't modify) | +| `seed/seed.json` | Schema definition + demo content (collections, fields, taxonomies, menus, widgets) | +| `emdash-env.d.ts` | Generated types for collections (auto-regenerated on dev server start) | +| `src/layouts/Base.astro` | Base layout with EmDash wiring (menus, search, page contributions) | +| `src/pages/` | Astro pages -- all server-rendered | + +## Rules + +- All content pages must be server-rendered (`output: "server"`). No `getStaticPaths()` for CMS content. +- `entry.id` is the slug (for URLs). `entry.data.id` is the database ULID (for API calls like `getEntryTerms`). +- Always call `Astro.cache.set(cacheHint)` on pages that query content. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..845a06a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM node:22-alpine AS deps + +RUN apk add --no-cache python3 make g++ sqlite + +WORKDIR /app + +COPY package.json pnpm-lock.yaml* ./ +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate +RUN pnpm install + +FROM deps AS builder + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +RUN pnpm build && pnpm exec emdash init && mkdir -p uploads + +FROM deps AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 + +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/uploads ./uploads + +EXPOSE 4321 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1423f2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Marketing Template + +A conversion-focused landing page with pricing and contact form. Built on Astro + EmDash CMS. + +## Features + +- Hero with CTAs +- Feature grid +- Pricing cards +- FAQ and contact form +- EmDash CMS for content management + +## Commands + +```bash +npm run dev # Start dev server +npm run build # Build for production +npm run bootstrap # Initialize database and seed content +``` + +## Pages + +- `/` - Homepage with hero, features, testimonials, FAQ +- `/pricing` - Pricing page with plans comparison +- `/contact` - Contact form + +## Admin + +Access the CMS at `/_emdash/admin` after running bootstrap. \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..9d4e279 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,57 @@ +import node from "@astrojs/node"; +import react from "@astrojs/react"; +import icon from "astro-iconset"; +import { defineConfig, fontProviders } from "astro/config"; +import emdash, { local } from "emdash/astro"; +import { sqlite } from "emdash/db"; + +export default defineConfig({ + output: "server", + adapter: node({ mode: "standalone" }), + image: { + layout: "constrained", + responsiveStyles: true, + }, + integrations: [ + react(), + icon({ + include: { + ph: [ + "chart-bar", + "check-circle", + "clock", + "cloud", + "code", + "currency-dollar", + "envelope", + "globe", + "heart", + "lifebuoy", + "lightning", + "lock", + "shield-check", + "sparkle", + "star", + "users-three", + ], + }, + }), + emdash({ + database: sqlite({ url: "file:./data.db" }), + storage: local({ + directory: "./uploads", + baseUrl: "/_emdash/api/media/file", + }), + }), + ], + fonts: [ + { + provider: fontProviders.google(), + name: "Inter", + cssVariable: "--font-sans", + weights: [400, 500, 600, 700, 800], + fallbacks: ["sans-serif"], + }, + ], + devToolbar: { enabled: false }, +}); \ No newline at end of file diff --git a/emdash-env.d.ts b/emdash-env.d.ts new file mode 100644 index 0000000..6f9505e --- /dev/null +++ b/emdash-env.d.ts @@ -0,0 +1,24 @@ +// Generated by EmDash on dev server start +// Do not edit manually + +/// + +import type { ContentBylineCredit, PortableTextBlock } from "emdash"; + +export interface Page { + id: string; + slug: string | null; + status: string; + title: string; + content?: PortableTextBlock[]; + createdAt: Date; + updatedAt: Date; + publishedAt: Date | null; + bylines?: ContentBylineCredit[]; +} + +declare module "emdash" { + interface EmDashCollections { + pages: Page; + } +} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..61bb210 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Only seed on first launch (when data.db doesn't exist or is empty) +if [ ! -f data.db ] || ! sqlite3 data.db "SELECT COUNT(*) FROM content" 2>/dev/null | grep -q "^[1-9]"; then + echo "Database missing or empty, running emdash init & seed..." + pnpm exec emdash init + pnpm exec emdash seed +else + echo "Database exists with content, starting normally..." +fi +exec node ./dist/server/entry.mjs \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f316ac6 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@emdash-cms/template-marketing", + "version": "0.0.3", + "private": true, + "type": "module", + "emdash": { + "seed": "seed/seed.json" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "start": "node ./dist/server/entry.mjs", + "bootstrap": "emdash init && emdash seed", + "seed": "emdash seed", + "typecheck": "astro check" + }, + "dependencies": { + "@astrojs/node": "^10.0.0", + "@astrojs/react": "^5.0.0", + "@iconify-json/ph": "^1.2.2", + "astro": "^6.0.1", + "astro-iconset": "^0.0.4", + "better-sqlite3": "^12.8.0", + "emdash": "^0.8.0", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@astrojs/check": "^0.9.7" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "esbuild" + ] + } +} \ No newline at end of file diff --git a/seed/seed.json b/seed/seed.json new file mode 100644 index 0000000..9667539 --- /dev/null +++ b/seed/seed.json @@ -0,0 +1,283 @@ +{ + "$schema": "https://emdashcms.com/seed.schema.json", + "version": "1", + "meta": { + "name": "Marketing Starter", + "description": "A conversion-focused marketing site with landing pages", + "author": "EmDash" + }, + "settings": { + "title": "Acme", + "tagline": "Build products people actually want" + }, + "collections": [ + { + "slug": "pages", + "label": "Pages", + "labelSingular": "Page", + "supports": ["drafts", "revisions", "seo"], + "fields": [ + { + "slug": "title", + "label": "Title", + "type": "string", + "required": true + }, + { + "slug": "content", + "label": "Content", + "type": "portableText" + } + ] + } + ], + "menus": [ + { + "name": "primary", + "label": "Primary Navigation", + "items": [ + { + "type": "custom", + "label": "Features", + "url": "/#features" + }, + { + "type": "custom", + "label": "Pricing", + "url": "/pricing" + }, + { + "type": "custom", + "label": "Contact", + "url": "/contact" + } + ] + } + ], + "content": { + "pages": [ + { + "id": "home", + "slug": "home", + "status": "published", + "data": { + "title": "Home", + "content": [ + { + "_type": "marketing.hero", + "_key": "hero", + "headline": "Build products people actually want", + "subheadline": "The all-in-one platform for modern teams. Ship faster, collaborate better, and focus on what matters.", + "primaryCta": { + "label": "Start Free Trial", + "url": "/signup" + }, + "secondaryCta": { + "label": "Watch Demo", + "url": "/demo" + } + }, + { + "_type": "marketing.features", + "_key": "features", + "headline": "Everything you need to ship", + "subheadline": "Powerful features that help your team move faster without sacrificing quality.", + "features": [ + { + "icon": "zap", + "title": "Lightning Fast", + "description": "Built for speed from the ground up. Your team will notice the difference from day one." + }, + { + "icon": "shield", + "title": "Enterprise Security", + "description": "SOC 2 compliant with end-to-end encryption. Your data stays yours." + }, + { + "icon": "users", + "title": "Team Collaboration", + "description": "Real-time collaboration features that make working together feel effortless." + }, + { + "icon": "chart", + "title": "Powerful Analytics", + "description": "Understand how your team works with detailed insights and reporting." + }, + { + "icon": "code", + "title": "Developer Friendly", + "description": "A robust API and CLI tools that integrate with your existing workflow." + }, + { + "icon": "globe", + "title": "Global Scale", + "description": "Deployed to edge locations worldwide. Fast for everyone, everywhere." + } + ] + }, + { + "_type": "marketing.testimonials", + "_key": "testimonials", + "headline": "Trusted by teams everywhere", + "testimonials": [ + { + "quote": "We cut our deployment time by 80%. The team actually enjoys shipping now.", + "author": "Sarah Chen", + "role": "CTO", + "company": "Streamline" + }, + { + "quote": "The best developer experience I've used. Everything just works the way you'd expect.", + "author": "Marcus Johnson", + "role": "Lead Engineer", + "company": "Volt Labs" + }, + { + "quote": "Finally, a tool that doesn't get in the way. Our team is more productive than ever.", + "author": "Elena Rodriguez", + "role": "VP Engineering", + "company": "Nexus" + } + ] + }, + { + "_type": "marketing.faq", + "_key": "faq", + "headline": "Frequently asked questions", + "items": [ + { + "question": "How long does setup take?", + "answer": "Most teams are up and running in under 15 minutes. Our onboarding wizard guides you through connecting your existing tools and inviting your team." + }, + { + "question": "Can I migrate from my current tool?", + "answer": "Yes. We have built-in importers for all major platforms, and our support team can help with custom migrations for larger teams." + }, + { + "question": "What kind of support do you offer?", + "answer": "All plans include email support with a 24-hour response time. Pro and Enterprise plans include priority support with dedicated account managers." + }, + { + "question": "Is there a free trial?", + "answer": "Yes, all plans come with a 14-day free trial. No credit card required. You can upgrade, downgrade, or cancel at any time." + } + ] + } + ] + } + }, + { + "id": "pricing", + "slug": "pricing", + "status": "published", + "data": { + "title": "Pricing", + "content": [ + { + "_type": "marketing.hero", + "_key": "pricing-hero", + "headline": "Simple, transparent pricing", + "subheadline": "No hidden fees. No surprises. Start free and scale as you grow.", + "centered": true + }, + { + "_type": "marketing.pricing", + "_key": "pricing-plans", + "plans": [ + { + "name": "Starter", + "price": "$0", + "period": "/month", + "description": "Perfect for trying things out", + "features": [ + "Up to 3 team members", + "5 projects", + "Basic analytics", + "Community support", + "1GB storage" + ], + "cta": { + "label": "Get Started", + "url": "/signup" + } + }, + { + "name": "Pro", + "price": "$29", + "period": "/user/month", + "description": "For growing teams", + "features": [ + "Unlimited team members", + "Unlimited projects", + "Advanced analytics", + "Priority support", + "100GB storage", + "Custom integrations", + "API access" + ], + "cta": { + "label": "Start Free Trial", + "url": "/signup?plan=pro" + }, + "highlighted": true + }, + { + "name": "Enterprise", + "price": "Custom", + "description": "For large organizations", + "features": [ + "Everything in Pro", + "Dedicated support", + "Custom contracts", + "SLA guarantee", + "Unlimited storage" + ], + "cta": { + "label": "Contact Sales", + "url": "/contact" + } + } + ] + }, + { + "_type": "marketing.faq", + "_key": "pricing-faq", + "headline": "Pricing FAQ", + "items": [ + { + "question": "Can I change plans later?", + "answer": "Yes, you can upgrade or downgrade at any time. Changes take effect immediately and we'll prorate your billing." + }, + { + "question": "What payment methods do you accept?", + "answer": "We accept all major credit cards, and Enterprise customers can pay via invoice with NET 30 terms." + }, + { + "question": "Is there a discount for annual billing?", + "answer": "Yes, annual plans receive a 20% discount compared to monthly billing." + } + ] + } + ] + } + }, + { + "id": "contact", + "slug": "contact", + "status": "published", + "data": { + "title": "Contact", + "content": [ + { + "_type": "marketing.hero", + "_key": "contact-hero", + "headline": "Get in touch", + "subheadline": "Have questions? Want a demo? We'd love to hear from you.", + "centered": true + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/components/MarketingBlocks.astro b/src/components/MarketingBlocks.astro new file mode 100644 index 0000000..5a1dc5e --- /dev/null +++ b/src/components/MarketingBlocks.astro @@ -0,0 +1,416 @@ +--- +interface Props { + value: any[]; +} + +const { value } = Astro.props; +--- + +
+ {value.map((block: any) => { + const type = block._type; + if (type === "marketing.hero") { + return ( +
+
+
+

{block.headline}

+ {block.subheadline &&

{block.subheadline}

} +
+ {block.primaryCta && ( + {block.primaryCta.label} + )} + {block.secondaryCta && ( + {block.secondaryCta.label} + )} +
+
+
+
+ ); + } + if (type === "marketing.features") { + return ( +
+
+
+

{block.headline}

+ {block.subheadline &&

{block.subheadline}

} +
+
+ {block.features?.map((feature: any) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.description}

+
+ ))} +
+
+
+ ); + } + if (type === "marketing.testimonials") { + return ( +
+
+

{block.headline}

+
+ {block.testimonials?.map((t: any) => ( +
+

"{t.quote}"

+
+ {t.author} + {t.role && {t.role}{t.company && ` at ${t.company}`}} +
+
+ ))} +
+
+
+ ); + } + if (type === "marketing.pricing") { + return ( +
+
+
+ {block.plans?.map((plan: any) => ( +
+ {plan.highlighted && Most popular} +

{plan.name}

+
+ {plan.price} + {plan.period && {plan.period}} +
+ {plan.description &&

{plan.description}

} +
    + {plan.features?.map((f: string) =>
  • {f}
  • )} +
+ {plan.cta.label} +
+ ))} +
+
+
+ ); + } + if (type === "marketing.faq") { + return ( +
+
+

{block.headline}

+
+ {block.items?.map((item: any) => ( +
+

{item.question}

+

{item.answer}

+
+ ))} +
+
+
+ ); + } + return null; + })} +
+ + \ No newline at end of file diff --git a/src/components/blocks/FAQ.astro b/src/components/blocks/FAQ.astro new file mode 100644 index 0000000..f1c5bda --- /dev/null +++ b/src/components/blocks/FAQ.astro @@ -0,0 +1,21 @@ +--- +interface Props { + node: { + _key?: string; + headline?: string; + items: Array<{ question: string; answer: string }>; + }; +} + +const { node } = Astro.props; +const { _key, headline, items } = node; +--- + +{headline &&

{headline}

} + +{items?.map((item) => ( +
+

{item.question}

+

{item.answer}

+
+))} \ No newline at end of file diff --git a/src/components/blocks/Features.astro b/src/components/blocks/Features.astro new file mode 100644 index 0000000..1978f82 --- /dev/null +++ b/src/components/blocks/Features.astro @@ -0,0 +1,39 @@ +--- +interface Props { + node: { + _key?: string; + headline?: string; + subheadline?: string; + features: Array<{ + icon: string; + title: string; + description: string; + }>; + }; +} + +const { node } = Astro.props; +const { _key, headline, subheadline, features } = node; +--- + +{(headline || subheadline) && ( +
+ {headline &&

{headline}

} + {subheadline &&

{subheadline}

} +
+)} + +{features?.map((feature) => ( +
+
{feature.icon}
+

{feature.title}

+

{feature.description}

+
+))} + + \ No newline at end of file diff --git a/src/components/blocks/Hero.astro b/src/components/blocks/Hero.astro new file mode 100644 index 0000000..582162a --- /dev/null +++ b/src/components/blocks/Hero.astro @@ -0,0 +1,4 @@ +--- +const { entry: page } = Astro.props; +--- +

{entry.data.title}

\ No newline at end of file diff --git a/src/components/blocks/Pricing.astro b/src/components/blocks/Pricing.astro new file mode 100644 index 0000000..42d3f23 --- /dev/null +++ b/src/components/blocks/Pricing.astro @@ -0,0 +1,35 @@ +--- +interface Props { + node: { + _key?: string; + headline?: string; + plans: Array<{ + name: string; + price: string; + period?: string; + description?: string; + features: string[]; + cta: { label: string; url: string }; + highlighted?: boolean; + }>; + }; +} + +const { node } = Astro.props; +const { headline, plans } = node; +--- + +{headline &&

{headline}

} + +{plans?.map((plan) => ( +
+ {plan.highlighted && Most popular} +

{plan.name}

+

{plan.price}{plan.period && {plan.period}}

+ {plan.description &&

{plan.description}

} +
    + {plan.features?.map((f) =>
  • {f}
  • )} +
+ {plan.cta.label} +
+))} \ No newline at end of file diff --git a/src/components/blocks/Testimonials.astro b/src/components/blocks/Testimonials.astro new file mode 100644 index 0000000..e7b4ed7 --- /dev/null +++ b/src/components/blocks/Testimonials.astro @@ -0,0 +1,29 @@ +--- +interface Props { + node: { + _key?: string; + headline?: string; + testimonials: Array<{ + quote: string; + author: string; + role?: string; + company?: string; + }>; + }; +} + +const { node } = Astro.props; +const { headline, testimonials } = node; +--- + +{headline &&

{headline}

} + +{testimonials?.map((t) => ( +
+

"{t.quote}"

+
+ {t.author} + {t.role && {t.role}{t.company && ` at ${t.company}`}} +
+
+))} \ No newline at end of file diff --git a/src/components/blocks/index.ts b/src/components/blocks/index.ts new file mode 100644 index 0000000..a500247 --- /dev/null +++ b/src/components/blocks/index.ts @@ -0,0 +1,5 @@ +export { default as Hero } from "./Hero.astro"; +export { default as Features } from "./Features.astro"; +export { default as Testimonials } from "./Testimonials.astro"; +export { default as Pricing } from "./Pricing.astro"; +export { default as FAQ } from "./FAQ.astro"; \ No newline at end of file diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro new file mode 100644 index 0000000..2773f14 --- /dev/null +++ b/src/layouts/Base.astro @@ -0,0 +1,156 @@ +--- +import { getMenu, getSiteSettings } from "emdash"; +import { EmDashHead } from "emdash/ui"; +import { createPublicPageContext } from "emdash/page"; +import { Font } from "astro:assets"; +import "../styles/theme.css"; + +interface Props { + title?: string; + description?: string; + image?: string; +} + +const { title, description, image } = Astro.props; +const settings = await getSiteSettings(); +const siteTitle = settings?.title || "Acme"; +const fullTitle = title ? `${title} — ${siteTitle}` : siteTitle; +const siteDescription = settings?.tagline || "Build products people actually want"; +const siteLogo = (settings?.logo as any)?.url ? settings.logo as { mediaId: string; alt?: string; url: string } : null; +const siteFavicon = (settings?.favicon as any)?.url ?? null; + +const menu = await getMenu("primary"); + +const pageCtx = createPublicPageContext({ + Astro, + kind: "custom", + pageType: "website", + title: fullTitle, + pageTitle: title ?? siteTitle, + description: description || siteDescription, + canonical: Astro.url.href, + image, + seo: { ogImage: image }, + siteName: siteTitle, +}); +--- + + + + + + + {fullTitle} + {siteFavicon && } + {description && } + + + + + + +
+ +
+ +
+
+

© {new Date().getFullYear()} {siteTitle}. All rights reserved.

+
+
+ + + + \ No newline at end of file diff --git a/src/live.config.ts b/src/live.config.ts new file mode 100644 index 0000000..ab59bb3 --- /dev/null +++ b/src/live.config.ts @@ -0,0 +1,11 @@ +/** + * EmDash Live Content Collections + */ +import { defineLiveCollection } from "astro:content"; +import { emdashLoader } from "emdash/runtime"; + +export const collections = { + _emdash: defineLiveCollection({ + loader: emdashLoader() + }), +}; \ No newline at end of file diff --git a/src/pages/contact.astro b/src/pages/contact.astro new file mode 100644 index 0000000..f62b5ac --- /dev/null +++ b/src/pages/contact.astro @@ -0,0 +1,230 @@ +--- +import { Icon } from "astro-iconset/components"; +import { getEmDashEntry } from "emdash"; +import Base from "../layouts/Base.astro"; +import MarketingBlocks from "../components/MarketingBlocks.astro"; + +const { entry: page, cacheHint } = await getEmDashEntry("pages", "contact"); +Astro.cache.set(cacheHint); +const pageContent = page?.data.content; +--- + + + {pageContent && } + +
+
+
+
+

Talk to our team

+

Fill out the form and we'll be in touch within 24 hours.

+ +
+
+
📧
+
+

Email

+ hello@acme.example +
+
+ +
+
💬
+
+

Support

+ support@acme.example +
+
+ +
+
💼
+
+

Sales

+ sales@acme.example +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..8fa822e --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,22 @@ +--- +import { getEmDashEntry } from "emdash"; +import Base from "../layouts/Base.astro"; +import MarketingBlocks from "../components/MarketingBlocks.astro"; + +const { entry: page, cacheHint } = await getEmDashEntry("pages", "home"); +Astro.cache.set(cacheHint); +const pageTitle = page?.data.title; +const pageContent = page?.data.content; +--- + + + {pageContent ? ( + + ) : ( +
+

Welcome to Acme

+

Edit the home page content in the admin to get started.

+ Open Admin +
+ )} + \ No newline at end of file diff --git a/src/pages/pricing.astro b/src/pages/pricing.astro new file mode 100644 index 0000000..824ac7f --- /dev/null +++ b/src/pages/pricing.astro @@ -0,0 +1,22 @@ +--- +import { getEmDashEntry } from "emdash"; +import Base from "../layouts/Base.astro"; +import MarketingBlocks from "../components/MarketingBlocks.astro"; + +const { entry: page, cacheHint } = await getEmDashEntry("pages", "pricing"); +Astro.cache.set(cacheHint); +const pageTitle = page?.data.title; +const pageContent = page?.data.content; +--- + + + {pageContent ? ( + + ) : ( +
+

Pricing

+

Edit the pricing page content in the admin to get started.

+ Open Admin +
+ )} + \ No newline at end of file diff --git a/src/styles/theme.css b/src/styles/theme.css new file mode 100644 index 0000000..2a108da --- /dev/null +++ b/src/styles/theme.css @@ -0,0 +1,70 @@ +/* theme.css -- Design tokens for the marketing template */ + +:root { + /* --- Colors --- */ + --color-bg: #ffffff; + --color-text: #0f172a; + --color-muted: #64748b; + --color-border: #e2e8f0; + --color-surface: #f8fafc; + --color-primary: #6366f1; + --color-primary-dark: #4f46e5; + --color-primary-light: #818cf8; + --color-accent: #f472b6; + --color-accent-light: #f9a8d4; + --color-success: #22c55e; + --color-warning: #f59e0b; + + /* --- Typography --- */ + --font-mono: ui-monospace, "SF Mono", monospace; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 2rem; + --font-size-4xl: 2.5rem; + --font-size-5xl: 3.5rem; + --font-size-6xl: 4.5rem; + + /* --- Spacing --- */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + --spacing-3xl: 4rem; + --spacing-4xl: 6rem; + --spacing-5xl: 8rem; + + /* --- Layout --- */ + --max-width: 720px; + --wide-width: 1200px; + --radius-sm: 6px; + --radius: 10px; + --radius-lg: 16px; + --radius-full: 9999px; + + /* --- Transitions --- */ + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; + + /* --- Shadows --- */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); +} + +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #0f172a; + --color-text: #f8fafc; + --color-muted: #94a3b8; + --color-border: #1e293b; + --color-surface: #1e293b; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..642e474 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} \ No newline at end of file