diff --git a/DESIGN.md b/DESIGN.md index 858cd5a..3ab10fe 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -355,10 +355,114 @@ When in doubt, apply these in order: --- +## 15. Tailwind v4 integration + +The marketing site (`site/`) uses **Tailwind v4** wired through `@tailwindcss/vite`. There is no `tailwind.config.{js,ts}` — Tailwind v4 is **CSS-first**: tokens declared in `@theme {}` inside [`site/src/styles/tokens.css`](site/src/styles/tokens.css) automatically become utilities (`bg-brand-primary`, `text-ink-700`, `rounded-lg`, …). + +### 15.1 Wiring + +- `astro.config.mjs` → `vite: { plugins: [tailwindcss()] }`. +- `site/src/styles/tokens.css` is the single source of truth: + 1. **Tailwind imports are split, NOT `@import "tailwindcss"`** : + ```css + @import 'tailwindcss/theme.css' layer(theme); + @import 'tailwindcss/utilities.css' layer(utilities); + ``` + Preflight is **intentionally omitted**. The full `@import "tailwindcss"` pulls a global CSS reset that resets `h1`/`p`/`ul` margins and list markers — that mangles Starlight's docs typography (heading sizes collapse, lists lose bullets, links default-color). Starlight ships its own scoped reset; ours adds nothing. If a marketing component needs a real reset, scope it manually with `.reset { all: revert; }` etc. + 2. `@variant dark (&:where([data-theme='dark'], [data-theme='dark'] *))` rebinds Tailwind's built-in `dark:` variant to our `[data-theme]` attribute (instead of `prefers-color-scheme`), so it stays in sync with `ThemeToggle.astro` + Starlight's theme provider. + 3. `@theme { … }` declares **every** token Tailwind should expose as utilities. + 4. `:root[data-theme='dark']` overrides the same `--color-*` vars for dark mode — utilities and raw `var(--color-…)` references both flip automatically. + 5. Legacy aliases (`--brand-primary`, `--space-4`, …) are kept as `var(--color-brand-primary)` etc. so older components written before Tailwind landed keep working without a rewrite. + +### 15.2 Token → utility mapping + +Tailwind v4 generates utilities from the prefix of each `--*-name` declared in `@theme`. The naming convention is **enforced by Tailwind**, so it's worth memorizing. + +| `@theme` prefix | Utilities generated | Example | +|----------------|---------------------|---------| +| `--color-*` | `bg-*`, `text-*`, `border-*`, `ring-*`, `from-*`, `to-*`, `via-*`, `outline-*`, `decoration-*`, `accent-*`, `caret-*`, `divide-*`, `placeholder-*` | `bg-brand-primary`, `text-ink-700` | +| `--font-*` | `font-*` | `font-display`, `font-mono` | +| `--text-*` | `text-*` (typography scale) | `text-md`, `text-3xl` | +| `--spacing` (single) | `p-N`, `m-N`, `gap-N`, `w-N`, `h-N`, etc. — `N × spacing` | `p-4` = `4 × 4px` = `16px` | +| `--radius-*` | `rounded-*` | `rounded-md`, `rounded-full` | +| `--shadow-*` | `shadow-*` | `shadow-md`, `shadow-lg` | +| `--container-*` | `max-w-*`, `mx-auto` containers | `max-w-prose`, `max-w-main` | +| `--breakpoint-*` | responsive variants | `sm:`, `md:`, `lg:`, `xl:` | + +### 15.3 When to use utility vs CSS var + +- **Tailwind utility** — for layout, spacing, typography, color in JSX-style markup. Reads cleanly inline. Default choice for new components. +- **Raw `var(--color-…)`** — for SVG fills/strokes, CSS-in-JS dynamic values, gradients with `color-mix()`, anywhere a utility doesn't exist or you need the raw value. The two systems share the same `--color-*` vars, so they stay in sync. +- **Scoped ` diff --git a/site/src/components/FeatureGrid.astro b/site/src/components/FeatureGrid.astro index e2d01be..b1fb760 100644 --- a/site/src/components/FeatureGrid.astro +++ b/site/src/components/FeatureGrid.astro @@ -27,99 +27,33 @@ const features = [ ]; --- -
-
-
-

Why Mailify

-

Built for developers who want ownership without redoing email infra from scratch.

+
+
+
+

Why Mailify

+

+ Built for developers who want ownership without redoing email infra from scratch. +

-
+
{features.map((feature) => ( -
- -

{feature.title}

-

{feature.body}

+
+ +

+ {feature.title} +

+

{feature.body}

))}
- - diff --git a/site/src/components/Hero.astro b/site/src/components/Hero.astro index 09b492f..8a3f570 100644 --- a/site/src/components/Hero.astro +++ b/site/src/components/Hero.astro @@ -1,31 +1,69 @@ -
-
-
-

Self-hosted. Theme-aware. Rust-fast.

-

The mail server that wears your brand.

-

+--- +--- + +

+
+
+

+ Self-hosted · Theme-aware · Rust-fast +

+ +

+ The mail server that wears your brand. +

+ +

Self-hosted transactional mail with your colors, your templates, and your SMTP provider. One Docker image. Zero vendor lock-in.

-
- Get started - - docker pull donighost/mailify +
+ + Get started → + + docker pull donighost/mailify +
-
    -
  • Branded emails from config-driven theme tokens
  • -
  • Per-job SMTP override for multi-tenant setups
  • -
  • Compiled React Email templates with a durable queue
  • +
      +
    • + + Branded emails from config-driven theme tokens +
    • +
    • + + Per-job SMTP override for multi-tenant setups +
    • +
    • + + Compiled React Email templates with a durable queue +
-
- - diff --git a/site/src/styles/global.css b/site/src/styles/global.css index ffaf56f..6667256 100644 --- a/site/src/styles/global.css +++ b/site/src/styles/global.css @@ -1,3 +1,12 @@ +/* + * Global styles. Two scopes: + * 1. Truly global resets (fonts, box-sizing, prefers-reduced-motion). + * 2. Marketing-only rules under .marketing — applied via the MarketingLayout + * wrapper. Starlight docs pages MUST NOT inherit + * heading/link colors from here, otherwise the sidebar links go invisible + * and the docs typography breaks. + */ + @import '@fontsource/inter/400.css'; @import '@fontsource/inter/500.css'; @import '@fontsource/geist-sans/500.css'; @@ -6,7 +15,10 @@ @import '@fontsource/geist-mono/400.css'; @import '@fontsource/geist-mono/500.css'; -* { +/* ─── Truly global ─── */ +*, +*::before, +*::after { box-sizing: border-box; } @@ -27,33 +39,56 @@ body { -moz-osx-font-smoothing: grayscale; } -h1, h2, h3, h4, h5, h6 { +img { + max-width: 100%; + display: block; +} + +:focus-visible { + outline: 2px solid var(--brand-primary); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +::selection { + background: color-mix(in srgb, var(--brand-primary) 26%, transparent); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* ─── Marketing-only (scoped under .marketing on ) ─── */ +.marketing h1, +.marketing h2, +.marketing h3, +.marketing h4, +.marketing h5, +.marketing h6 { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.02em; color: var(--text); } -a { +.marketing a { color: var(--link); text-decoration: none; transition: color 150ms ease-out; } -a:hover { +.marketing a:hover { color: var(--brand-primary-hover); } -img { - max-width: 100%; - display: block; -} - -code, pre { - font-family: var(--font-mono); -} - -code:not(pre code) { +.marketing code:not(pre code) { padding: 0.1rem 0.35rem; border-radius: var(--radius-sm); background: var(--brand-primary-muted); @@ -61,7 +96,7 @@ code:not(pre code) { font-size: 0.925em; } -.skip-link { +.marketing .skip-link { position: absolute; left: var(--space-4); top: -3rem; @@ -72,10 +107,12 @@ code:not(pre code) { color: var(--paper); } -.skip-link:focus { +.marketing .skip-link:focus { top: var(--space-4); } +/* Buttons used across marketing pages */ +.marketing .btn, .btn { display: inline-flex; align-items: center; @@ -91,52 +128,38 @@ code:not(pre code) { text-decoration: none; } +.marketing .btn-primary, .btn-primary { background: var(--brand-primary); color: white; } +.marketing .btn-primary:hover, .btn-primary:hover { background: var(--brand-primary-hover); color: white; } +.marketing .btn-secondary, .btn-secondary { background: var(--bg-raised); color: var(--text); border-color: var(--border); } +.marketing .btn-secondary:hover, .btn-secondary:hover { background: var(--ink-100); color: var(--text); } +.marketing .btn-ghost, .btn-ghost { background: transparent; color: var(--link); } +.marketing .btn-ghost:hover, .btn-ghost:hover { background: var(--brand-primary-muted); } - -:focus-visible { - outline: 2px solid var(--brand-primary); - outline-offset: 2px; - border-radius: var(--radius-sm); -} - -::selection { - background: color-mix(in srgb, var(--brand-primary) 26%, transparent); -} - -/* Prefers reduced motion */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } -} diff --git a/site/src/styles/tokens.css b/site/src/styles/tokens.css index 82d041c..bee5d65 100644 --- a/site/src/styles/tokens.css +++ b/site/src/styles/tokens.css @@ -1,115 +1,186 @@ -/* Mailify design tokens — mirrors DESIGN.md §2–§4 */ +/* + * Mailify design tokens — single source of truth for the site theme. + * Mirrors DESIGN.md §2–§4. Exposed to Tailwind v4 via @theme so utilities + * like `bg-brand-primary`, `text-ink-700`, `rounded-lg` are auto-generated. + * + * Components can use either Tailwind utilities or raw CSS vars (`var(--color-...)`). + */ -:root { - /* Brand scale */ - --brand-primary: #2d5bff; - --brand-primary-hover: #1e43d9; - --brand-primary-muted: #e7edff; - --brand-accent: #ff8a3d; - --brand-accent-muted: #fff0e5; - - /* Neutral scale (light) */ - --ink-900: #0b1020; - --ink-700: #1f2937; - --ink-500: #475569; - --ink-300: #94a3b8; - --ink-100: #e2e8f0; - --paper: #f8fafc; - --paper-raised: #ffffff; - --border: #e2e8f0; - - /* Semantic */ - --text: var(--ink-900); - --text-muted: var(--ink-500); - --link: var(--brand-primary); - --bg: var(--paper); - --bg-raised: var(--paper-raised); - --success: #10b981; - --warning: #f59e0b; - --danger: #ef4444; - --info: var(--brand-primary); - - /* Typography */ - --fs-xs: 0.75rem; - --fs-sm: 0.875rem; - --fs-base: 1rem; - --fs-md: 1.125rem; - --fs-lg: 1.5rem; - --fs-xl: 1.875rem; - --fs-2xl: 2.5rem; - --fs-3xl: 3.5rem; - --fs-4xl: 4.5rem; +/* + * Tailwind v4 imports — split, NOT `@import "tailwindcss"`: + * - `theme` → enables @theme {} so tokens become utilities + * - `utilities` → ships utility classes + * - preflight is intentionally OMITTED. It would reset Starlight's + * typography (h1/p margins, list markers, link colors). Starlight + * already ships its own scoped reset. Components needing reset can + * scope it manually. + */ +@layer base, starlight, theme, components, utilities; +@import '@astrojs/starlight-tailwind'; +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/utilities.css' layer(utilities); + +/* Bind `dark:` variant to the [data-theme="dark"] attribute used by Starlight + ThemeToggle. */ +@variant dark (&:where([data-theme='dark'], [data-theme='dark'] *)); + +@theme { + /* ─── Brand colors ─── */ + --color-brand-primary: #2d5bff; + --color-brand-primary-hover: #1e43d9; + --color-brand-primary-muted: #e7edff; + --color-brand-accent: #ff8a3d; + --color-brand-accent-muted: #fff0e5; + + /* ─── Neutral scale ─── */ + --color-ink-900: #0b1020; + --color-ink-700: #1f2937; + --color-ink-500: #475569; + --color-ink-300: #94a3b8; + --color-ink-100: #e2e8f0; + --color-paper: #f8fafc; + --color-paper-raised: #ffffff; + --color-border: #e2e8f0; + + /* ─── Semantic ─── */ + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-info: #2d5bff; + /* ─── Typography ─── */ --font-display: 'Geist Sans', 'Inter', -apple-system, system-ui, sans-serif; --font-body: 'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif; --font-mono: 'Geist Mono', 'JetBrains Mono', Menlo, Consolas, monospace; + --font-sans: var(--font-body); - /* Spacing (4px base) */ - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-6: 24px; - --space-8: 32px; - --space-12: 48px; - --space-16: 64px; - --space-24: 96px; - --space-32: 128px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-md: 1.125rem; + --text-lg: 1.5rem; + --text-xl: 1.875rem; + --text-2xl: 2.5rem; + --text-3xl: 3.5rem; + --text-4xl: 4.5rem; - /* Radii */ + /* ─── Spacing (4px base) ─── */ + --spacing: 4px; + + /* ─── Radii ─── */ --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-full: 9999px; - /* Elevation */ + /* ─── Elevation ─── */ --shadow-sm: 0 1px 2px rgba(11, 16, 32, 0.06); --shadow-md: 0 4px 12px rgba(11, 16, 32, 0.08); --shadow-lg: 0 12px 32px rgba(11, 16, 32, 0.12); - /* Containers */ - --container-prose: 720px; - --container-main: 1120px; - --container-wide: 1280px; + /* ─── Containers ─── */ + --container-prose: 45rem; + --container-main: 70rem; + --container-wide: 80rem; + + /* ─── Breakpoints (mirror DESIGN.md §4.5) ─── */ + --breakpoint-sm: 640px; + --breakpoint-md: 768px; + --breakpoint-lg: 1024px; + --breakpoint-xl: 1280px; } +/* ─── Light-mode semantic aliases ─── */ +:root { + --color-text: var(--color-ink-900); + --color-text-muted: var(--color-ink-500); + --color-link: var(--color-brand-primary); + --color-bg: var(--color-paper); + --color-bg-raised: var(--color-paper-raised); + + /* Legacy short aliases (kept for components written before Tailwind landed) */ + --brand-primary: var(--color-brand-primary); + --brand-primary-hover: var(--color-brand-primary-hover); + --brand-primary-muted: var(--color-brand-primary-muted); + --brand-accent: var(--color-brand-accent); + --brand-accent-muted: var(--color-brand-accent-muted); + --ink-900: var(--color-ink-900); + --ink-700: var(--color-ink-700); + --ink-500: var(--color-ink-500); + --ink-300: var(--color-ink-300); + --ink-100: var(--color-ink-100); + --paper: var(--color-paper); + --paper-raised: var(--color-paper-raised); + --border: var(--color-border); + --text: var(--color-text); + --text-muted: var(--color-text-muted); + --link: var(--color-link); + --bg: var(--color-bg); + --bg-raised: var(--color-bg-raised); + --success: var(--color-success); + --warning: var(--color-warning); + --danger: var(--color-danger); + --info: var(--color-info); + + --fs-xs: var(--text-xs); + --fs-sm: var(--text-sm); + --fs-base: var(--text-base); + --fs-md: var(--text-md); + --fs-lg: var(--text-lg); + --fs-xl: var(--text-xl); + --fs-2xl: var(--text-2xl); + --fs-3xl: var(--text-3xl); + --fs-4xl: var(--text-4xl); + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; + --space-12: 48px; + --space-16: 64px; + --space-24: 96px; + --space-32: 128px; +} + +/* ─── Dark-mode overrides — apply to both Tailwind utilities and raw vars ─── */ :root[data-theme='dark'] { - --brand-primary: #5b82ff; - --brand-primary-hover: #7a9cff; - --brand-primary-muted: #1a2449; - --brand-accent: #ffa466; - --brand-accent-muted: #3d2414; - - --ink-900: #f8fafc; - --ink-700: #e2e8f0; - --ink-500: #94a3b8; - --ink-300: #475569; - --ink-100: #1f2937; - --paper: #0b1020; - --paper-raised: #111832; - --border: #1f2937; + --color-brand-primary: #5b82ff; + --color-brand-primary-hover: #7a9cff; + --color-brand-primary-muted: #1a2449; + --color-brand-accent: #ffa466; + --color-brand-accent-muted: #3d2414; + + --color-ink-900: #f8fafc; + --color-ink-700: #e2e8f0; + --color-ink-500: #94a3b8; + --color-ink-300: #475569; + --color-ink-100: #1f2937; + --color-paper: #0b1020; + --color-paper-raised: #111832; + --color-border: #1f2937; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); } -/* Map to Starlight theme hooks */ +/* ─── Starlight theme bridge ─── */ :root { - --sl-color-accent: var(--brand-primary); - --sl-color-accent-high: var(--brand-primary-hover); - --sl-color-accent-low: var(--brand-primary-muted); - --sl-color-white: var(--paper-raised); - --sl-color-gray-1: var(--ink-100); - --sl-color-gray-2: var(--border); - --sl-color-gray-6: var(--ink-500); - --sl-color-gray-7: var(--ink-700); - --sl-color-black: var(--ink-900); - --sl-color-text: var(--text); - --sl-color-text-accent: var(--brand-primary); - --sl-color-bg: var(--bg); - --sl-color-bg-nav: var(--paper-raised); - --sl-color-bg-sidebar: var(--paper-raised); + --sl-color-accent: var(--color-brand-primary); + --sl-color-accent-high: var(--color-brand-primary-hover); + --sl-color-accent-low: var(--color-brand-primary-muted); + --sl-color-white: var(--color-paper-raised); + --sl-color-gray-1: var(--color-ink-100); + --sl-color-gray-2: var(--color-border); + --sl-color-gray-6: var(--color-ink-500); + --sl-color-gray-7: var(--color-ink-700); + --sl-color-black: var(--color-ink-900); + --sl-color-text: var(--color-text); + --sl-color-text-accent: var(--color-brand-primary); + --sl-color-bg: var(--color-bg); + --sl-color-bg-nav: var(--color-paper-raised); + --sl-color-bg-sidebar: var(--color-paper-raised); --sl-font: var(--font-body); --sl-font-mono: var(--font-mono); }