feat(landing): add Atlas landing page with Astro static site
Implement the full landing page for atlas.atomstorm.ai per the PRD at Docs/Execution/Landing-Page-PRD-2026-03-14.md. Includes design spec, bilingual Astro site, CI pipeline, and all assets. Key deliverables: - DESIGN.md: 7-section Atom Web Design specification - Astro 5.x static site with 15 components and 11 page sections - Bilingual i18n (zh/en) with path-based routing - Build-time GitHub release manifest integration - Release channel state machine (Stable/Prerelease/Coming Soon) - CSS design tokens mapped from AtlasBrand.swift - Self-hosted fonts (Space Grotesk, Instrument Sans, IBM Plex Mono) - OG social sharing images for both locales - GitHub Actions workflow for GitHub Pages deployment - Zero client JS, 227KB page weight, 17/17 quality checks passed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70
.github/workflows/landing-page.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Landing Page
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'Apps/LandingSite/**'
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: Apps/LandingSite/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Fetch release manifest
|
||||
working-directory: Apps/LandingSite
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm run fetch-release
|
||||
|
||||
- name: Build static site
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm run build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
with:
|
||||
path: Apps/LandingSite/dist
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac2b3c603fc # v4
|
||||
4
Apps/LandingSite/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
src/data/release-manifest.json
|
||||
988
Apps/LandingSite/DESIGN.md
Normal file
@@ -0,0 +1,988 @@
|
||||
# Atlas Landing Page — Atom Web Design Specification
|
||||
|
||||
> **Domain**: `atlas.atomstorm.ai` | **Deploy**: GitHub Pages | **Source**: `Apps/LandingSite/`
|
||||
> **PRD**: `Docs/Execution/Landing-Page-PRD-2026-03-14.md`
|
||||
> **Date**: 2026-03-15
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Product Definition](#1-product-definition)
|
||||
2. [Tech Stack Decision](#2-tech-stack-decision)
|
||||
3. [Data Architecture](#3-data-architecture)
|
||||
4. [Component Architecture](#4-component-architecture)
|
||||
5. [Constraint System](#5-constraint-system)
|
||||
6. [Visual Design System](#6-visual-design-system)
|
||||
7. [Quality Gates](#7-quality-gates)
|
||||
|
||||
---
|
||||
|
||||
## 1. Product Definition
|
||||
|
||||
### 1.1 User Stories
|
||||
|
||||
| ID | Story | Acceptance Criteria |
|
||||
|----|-------|-------------------|
|
||||
| US-01 | As a first-time visitor, I want to understand what Atlas does in one screen so I can decide if it solves my problem. | Hero section communicates product promise in < 10 seconds; headline + subheadline + screenshot visible above the fold. |
|
||||
| US-02 | As a cautious Mac user, I want to see safety and trust signals so I can decide whether Atlas is safe enough to install. | Trust strip, open-source badge, recovery-first messaging, permissions explanation, and Gatekeeper guidance are all visible without deep scrolling. |
|
||||
| US-03 | As a developer, I want to see that Atlas understands developer disk pressure (Xcode, simulators, caches) so I know it is relevant to my workflow. | Developer cleanup section lists concrete developer artifacts (derived data, simulators, package caches) with Atlas-specific handling. |
|
||||
| US-04 | As a potential downloader, I want to get the latest build without navigating GitHub so I can install Atlas quickly. | Primary CTA links to the correct release asset; version number, channel badge, and release date are visible next to the CTA. |
|
||||
| US-05 | As a visitor encountering a prerelease, I want honest disclosure about signing status and Gatekeeper friction so I can install without confusion. | Prerelease badge, warning label, and "Open Anyway" recovery path are shown when the latest release is not Developer ID signed. |
|
||||
|
||||
### 1.2 MVP Scope
|
||||
|
||||
| Included | Deferred |
|
||||
|----------|----------|
|
||||
| Single bilingual single-page site (`/zh/`, `/en/`) | Blog / CMS |
|
||||
| Responsive hero with release-state CTA | Changelog microsite |
|
||||
| 11 page sections (Hero → Footer) | Testimonials from named users |
|
||||
| Screenshot gallery (4–6 images) | Interactive benchmark calculator |
|
||||
| Dynamic release state block (build-time) | Gated PDF lead magnet |
|
||||
| Trust/safety section with Gatekeeper guidance | Multi-page docs hub |
|
||||
| FAQ with expand/collapse | Account system |
|
||||
| GitHub Pages deployment with custom domain | Pricing or checkout flow |
|
||||
| Privacy-respecting analytics (Plausible) | In-browser disk scan demo |
|
||||
| Optional beta email capture (3rd-party form) | |
|
||||
|
||||
### 1.3 Release Channel State Machine
|
||||
|
||||
The page must treat release channel status as product truth. Three states drive CTA behavior:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ No Public Release │ ← no GitHub Release with assets
|
||||
│ CTA: "View on │
|
||||
│ GitHub" / "Join │
|
||||
│ Beta Updates" │
|
||||
└──────────┬──────────┘
|
||||
│ first release published
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Prerelease Only │ ← prerelease=true on latest release
|
||||
│ CTA: "Download │
|
||||
│ Prerelease" │
|
||||
│ + warning label │
|
||||
│ + Gatekeeper note │
|
||||
└──────────┬──────────┘
|
||||
│ Developer ID signing configured
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Stable Release │ ← prerelease=false on latest release
|
||||
│ CTA: "Download │
|
||||
│ for macOS" │
|
||||
│ + version badge │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**State × CTA Behavior Matrix**:
|
||||
|
||||
| State | Primary CTA Label | Badge | Warning | Secondary CTA |
|
||||
|-------|-------------------|-------|---------|---------------|
|
||||
| Stable | Download for macOS | `Stable` (teal) | None | View on GitHub |
|
||||
| Prerelease | Download Prerelease | `Prerelease` (amber) | Gatekeeper install note | View on GitHub |
|
||||
| No Release | View on GitHub | `Coming Soon` (slate) | None | Join Beta Updates |
|
||||
|
||||
---
|
||||
|
||||
## 2. Tech Stack Decision
|
||||
|
||||
### 2.1 Stack Table
|
||||
|
||||
| Layer | Choice | Why |
|
||||
|-------|--------|-----|
|
||||
| **Framework** | Astro 5.x (static adapter) | Outputs pure HTML/CSS with zero JS by default; matches PRD's "static-first" requirement |
|
||||
| **Styling** | Vanilla CSS + custom properties | 1:1 token mapping from `AtlasBrand.swift`; avoids Tailwind's generic aesthetic per PRD's "not generic SaaS" direction |
|
||||
| **i18n** | `@astrojs/i18n` path-based routing | Native Astro feature; produces `/en/` and `/zh/` paths with `hreflang` tags |
|
||||
| **Fonts** | Self-hosted (Space Grotesk, Instrument Sans, IBM Plex Mono) | Eliminates Google Fonts dependency; keeps < 20KB JS budget; GDPR-safe |
|
||||
| **Analytics** | Plausible (self-hosted or cloud) | Privacy-respecting, cookie-free, GDPR-compliant; custom events for CTA/FAQ tracking |
|
||||
| **Search Console** | Google Search Console | Indexing monitoring and query analysis; no client-side impact |
|
||||
| **Hosting** | GitHub Pages | Free, reliable, native GitHub Actions integration; custom domain with HTTPS |
|
||||
| **CI/CD** | GitHub Actions (`.github/workflows/landing-page.yml`) | Triggers on source changes + release events; deploys via `actions/deploy-pages` |
|
||||
| **Release Data** | Build-time GitHub API fetch → static manifest | No client-side API dependency for first paint; fallback to embedded JSON |
|
||||
| **UI Framework** | None (no React/Vue/Svelte) | Zero framework overhead; `.astro` components render to static HTML |
|
||||
| **Package Manager** | pnpm | Fast, deterministic, disk-efficient |
|
||||
|
||||
### 2.2 Key Technical Decisions
|
||||
|
||||
1. **No UI framework islands** — Every component is a `.astro` file that renders to static HTML. The only client JS is: language toggle persistence (`localStorage`, < 200 bytes), FAQ accordion (`<details>` elements with optional progressive enhancement), and Plausible analytics snippet (< 1KB).
|
||||
|
||||
2. **Self-hosted fonts** — Font files are committed to `Apps/LandingSite/public/fonts/`. Subset to Latin + CJK ranges. Use `font-display: swap` for all faces.
|
||||
|
||||
3. **Build-time release manifest** — A `scripts/fetch-release.ts` script runs at build time to query the GitHub Releases API and emit `src/data/release-manifest.json`. A static fallback (`src/data/release-fallback.json`) is used if the API call fails.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Architecture
|
||||
|
||||
### 3.1 `ReleaseManifest` Schema
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Generated at build time by scripts/fetch-release.ts
|
||||
* Consumed by Hero, CTA, and Footer components.
|
||||
* File: src/data/release-manifest.json
|
||||
*/
|
||||
interface ReleaseManifest {
|
||||
/** Release channel: determines CTA behavior and badge */
|
||||
channel: "stable" | "prerelease" | "none";
|
||||
|
||||
/** Semantic version string, e.g. "1.0.2" */
|
||||
version: string | null;
|
||||
|
||||
/** ISO 8601 date string of the release publication */
|
||||
publishedAt: string | null;
|
||||
|
||||
/** GitHub Release page URL */
|
||||
releaseUrl: string | null;
|
||||
|
||||
/** Direct download asset links */
|
||||
assets: {
|
||||
dmg: string | null;
|
||||
zip: string | null;
|
||||
pkg: string | null;
|
||||
sha256: string | null;
|
||||
};
|
||||
|
||||
/** Whether Gatekeeper friction is expected */
|
||||
gatekeeperWarning: boolean;
|
||||
|
||||
/** Human-readable install note for prerelease builds */
|
||||
installNote: string | null;
|
||||
|
||||
/** Git tag name, e.g. "V1.0.2" */
|
||||
tagName: string | null;
|
||||
|
||||
/** Timestamp of manifest generation (ISO 8601) */
|
||||
generatedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Priority chain** (per PRD):
|
||||
1. Build-time generated `release-manifest.json` via `scripts/fetch-release.ts`
|
||||
2. Static fallback `src/data/release-fallback.json` (committed, manually maintained)
|
||||
3. No client-side GitHub API fetch for first paint
|
||||
|
||||
**Manifest generation logic** (`scripts/fetch-release.ts`):
|
||||
```
|
||||
1. Fetch latest release from GitHub API (repos/{owner}/{repo}/releases/latest)
|
||||
2. If no release exists → channel: "none"
|
||||
3. If release.prerelease === true → channel: "prerelease", gatekeeperWarning: true
|
||||
4. If release.prerelease === false → channel: "stable", gatekeeperWarning: false
|
||||
5. Extract .dmg, .zip, .pkg, .sha256 from release.assets[]
|
||||
6. Write to src/data/release-manifest.json
|
||||
```
|
||||
|
||||
### 3.2 `LandingCopy` i18n Schema
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Translation file structure.
|
||||
* Files: src/i18n/en.json, src/i18n/zh.json
|
||||
* Keys are grouped by page section for maintainability.
|
||||
*/
|
||||
interface LandingCopy {
|
||||
meta: {
|
||||
title: string; // <title> and og:title
|
||||
description: string; // <meta name="description"> and og:description
|
||||
ogImage: string; // og:image path
|
||||
};
|
||||
|
||||
nav: {
|
||||
whyAtlas: string;
|
||||
howItWorks: string;
|
||||
developers: string;
|
||||
safety: string;
|
||||
faq: string;
|
||||
download: string; // CTA label (dynamic, overridden by channel)
|
||||
};
|
||||
|
||||
hero: {
|
||||
headline: string;
|
||||
subheadline: string;
|
||||
ctaPrimary: string; // Overridden by channel state
|
||||
ctaSecondary: string; // "View on GitHub"
|
||||
badgeStable: string;
|
||||
badgePrerelease: string;
|
||||
badgeComingSoon: string;
|
||||
prereleaseWarning: string;
|
||||
gatekeeperNote: string;
|
||||
versionLabel: string; // "Version {version} · {date}"
|
||||
};
|
||||
|
||||
trustStrip: {
|
||||
openSource: string;
|
||||
recoveryFirst: string;
|
||||
developerAware: string;
|
||||
macNative: string;
|
||||
directDownload: string;
|
||||
};
|
||||
|
||||
problem: {
|
||||
sectionTitle: string;
|
||||
scenarios: Array<{
|
||||
before: string; // Pain point
|
||||
after: string; // Atlas outcome
|
||||
}>;
|
||||
};
|
||||
|
||||
features: {
|
||||
sectionTitle: string;
|
||||
cards: Array<{
|
||||
title: string;
|
||||
value: string; // User-facing value proposition
|
||||
example: string; // Concrete example
|
||||
trustCue: string; // Trust signal
|
||||
}>;
|
||||
};
|
||||
|
||||
howItWorks: {
|
||||
sectionTitle: string;
|
||||
steps: Array<{
|
||||
label: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
developer: {
|
||||
sectionTitle: string;
|
||||
subtitle: string;
|
||||
items: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
safety: {
|
||||
sectionTitle: string;
|
||||
subtitle: string;
|
||||
points: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
gatekeeperGuide: {
|
||||
title: string;
|
||||
steps: string[];
|
||||
};
|
||||
};
|
||||
|
||||
screenshots: {
|
||||
sectionTitle: string;
|
||||
items: Array<{
|
||||
src: string;
|
||||
alt: string;
|
||||
caption: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
openSource: {
|
||||
sectionTitle: string;
|
||||
repoLabel: string;
|
||||
licenseLabel: string;
|
||||
attributionLabel: string;
|
||||
changelogLabel: string;
|
||||
};
|
||||
|
||||
faq: {
|
||||
sectionTitle: string;
|
||||
items: Array<{
|
||||
question: string;
|
||||
answer: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
footer: {
|
||||
download: string;
|
||||
github: string;
|
||||
documentation: string;
|
||||
privacy: string;
|
||||
security: string;
|
||||
copyright: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 State Management Map
|
||||
|
||||
All state is resolved at build time. The only client-side state is:
|
||||
|
||||
| State | Scope | Storage | Purpose |
|
||||
|-------|-------|---------|---------|
|
||||
| Language preference | Session | `localStorage` key `atlas-lang` | Remember manual language switch |
|
||||
| FAQ expanded items | Transient | DOM (`<details open>`) | No persistence needed |
|
||||
| Release data | Build-time | `release-manifest.json` | Embedded in HTML at build |
|
||||
| Analytics events | Fire-and-forget | Plausible JS SDK | No local state |
|
||||
|
||||
---
|
||||
|
||||
## 4. Component Architecture
|
||||
|
||||
### 4.1 File Structure
|
||||
|
||||
```
|
||||
Apps/LandingSite/
|
||||
├── astro.config.mjs # Astro config: static adapter, i18n, site URL
|
||||
├── package.json # Dependencies: astro, @astrojs/sitemap
|
||||
├── pnpm-lock.yaml
|
||||
├── tsconfig.json
|
||||
├── public/
|
||||
│ ├── fonts/ # Self-hosted font files (woff2)
|
||||
│ │ ├── SpaceGrotesk-Bold.woff2
|
||||
│ │ ├── SpaceGrotesk-Medium.woff2
|
||||
│ │ ├── InstrumentSans-Regular.woff2
|
||||
│ │ ├── InstrumentSans-Medium.woff2
|
||||
│ │ ├── IBMPlexMono-Regular.woff2
|
||||
│ │ └── IBMPlexMono-Medium.woff2
|
||||
│ ├── images/
|
||||
│ │ ├── atlas-icon.png # App icon (from Docs/Media/README/)
|
||||
│ │ ├── og-image-en.png # Open Graph image (English)
|
||||
│ │ ├── og-image-zh.png # Open Graph image (Chinese)
|
||||
│ │ └── screenshots/ # Product screenshots
|
||||
│ │ ├── atlas-overview.png
|
||||
│ │ ├── atlas-smart-clean.png
|
||||
│ │ ├── atlas-apps.png
|
||||
│ │ ├── atlas-history.png
|
||||
│ │ ├── atlas-settings.png
|
||||
│ │ └── atlas-prerelease-warning.png
|
||||
│ ├── favicon.ico
|
||||
│ └── robots.txt
|
||||
├── src/
|
||||
│ ├── data/
|
||||
│ │ ├── release-manifest.json # Build-time generated
|
||||
│ │ └── release-fallback.json # Static fallback (committed)
|
||||
│ ├── i18n/
|
||||
│ │ ├── en.json # English translations
|
||||
│ │ ├── zh.json # Chinese translations
|
||||
│ │ └── utils.ts # t() helper, locale detection
|
||||
│ ├── styles/
|
||||
│ │ ├── tokens.css # Design tokens as CSS custom properties
|
||||
│ │ ├── reset.css # Minimal CSS reset
|
||||
│ │ ├── global.css # Global styles (fonts, base elements)
|
||||
│ │ └── utilities.css # Utility classes (sr-only, container, etc.)
|
||||
│ ├── layouts/
|
||||
│ │ └── BaseLayout.astro # HTML shell: <head>, meta, fonts, analytics
|
||||
│ ├── components/
|
||||
│ │ ├── NavBar.astro # [interactive] Sticky top nav + language toggle + CTA
|
||||
│ │ ├── Hero.astro # [static] Headline, subheadline, CTA, badge, screenshot
|
||||
│ │ ├── TrustStrip.astro # [static] Five trust signal pills
|
||||
│ │ ├── ProblemOutcome.astro # [static] Three pain → solution cards
|
||||
│ │ ├── FeatureGrid.astro # [static] Six feature story cards
|
||||
│ │ ├── HowItWorks.astro # [static] Four-step workflow visualization
|
||||
│ │ ├── DeveloperSection.astro # [static] Developer cleanup showcase
|
||||
│ │ ├── SafetySection.astro # [static] Permissions, trust, Gatekeeper guide
|
||||
│ │ ├── ScreenshotGallery.astro# [interactive] Desktop gallery / mobile carousel
|
||||
│ │ ├── OpenSourceSection.astro# [static] Repo link, license, attribution
|
||||
│ │ ├── FaqSection.astro # [interactive] Expandable Q&A using <details>
|
||||
│ │ ├── FooterSection.astro # [static] Links, privacy, security contact
|
||||
│ │ ├── CtaButton.astro # [static] Reusable CTA (primary/secondary variants)
|
||||
│ │ ├── ChannelBadge.astro # [static] Release channel badge (stable/prerelease/coming)
|
||||
│ │ └── FeatureCard.astro # [static] Reusable card for feature grid
|
||||
│ └── pages/
|
||||
│ ├── index.astro # Root redirect → /zh/
|
||||
│ ├── zh/
|
||||
│ │ └── index.astro # Chinese landing page
|
||||
│ └── en/
|
||||
│ └── index.astro # English landing page
|
||||
└── scripts/
|
||||
└── fetch-release.ts # Build-time script: GitHub API → release-manifest.json
|
||||
```
|
||||
|
||||
### 4.2 Component Interaction Map
|
||||
|
||||
```
|
||||
BaseLayout.astro
|
||||
└── [lang]/index.astro
|
||||
├── NavBar.astro .................. [interactive: language toggle, mobile menu]
|
||||
│ ├── CtaButton.astro props: { label, href, variant: "primary" }
|
||||
│ └── ChannelBadge.astro props: { channel }
|
||||
├── Hero.astro .................... [static]
|
||||
│ ├── CtaButton.astro props: { label, href, variant: "primary" }
|
||||
│ ├── CtaButton.astro props: { label, href, variant: "secondary" }
|
||||
│ └── ChannelBadge.astro props: { channel, version, date }
|
||||
├── TrustStrip.astro .............. [static]
|
||||
├── ProblemOutcome.astro .......... [static]
|
||||
├── FeatureGrid.astro ............. [static]
|
||||
│ └── FeatureCard.astro (×6) props: { title, value, example, trustCue, icon }
|
||||
├── HowItWorks.astro .............. [static]
|
||||
├── DeveloperSection.astro ........ [static]
|
||||
├── SafetySection.astro ........... [static]
|
||||
├── ScreenshotGallery.astro ....... [interactive: carousel on mobile]
|
||||
├── OpenSourceSection.astro ....... [static]
|
||||
├── FaqSection.astro .............. [interactive: <details> expand/collapse]
|
||||
└── FooterSection.astro ........... [static]
|
||||
└── CtaButton.astro props: { label, href, variant: "primary" }
|
||||
```
|
||||
|
||||
**Boundary annotations**:
|
||||
- `[static]` — Pure HTML at build time, zero client JS
|
||||
- `[interactive]` — Minimal client JS via inline `<script>` in the component (no framework island)
|
||||
|
||||
### 4.3 Component Props Summary
|
||||
|
||||
| Component | Props | Data Source |
|
||||
|-----------|-------|-------------|
|
||||
| `CtaButton` | `label: string, href: string, variant: "primary" \| "secondary" \| "ghost"` | i18n + manifest |
|
||||
| `ChannelBadge` | `channel: "stable" \| "prerelease" \| "none", version?: string, date?: string` | manifest |
|
||||
| `FeatureCard` | `title: string, value: string, example: string, trustCue: string, icon: string` | i18n |
|
||||
| `NavBar` | `locale: "en" \| "zh", manifest: ReleaseManifest` | i18n + manifest |
|
||||
| `Hero` | `locale: "en" \| "zh", manifest: ReleaseManifest` | i18n + manifest |
|
||||
| `FaqSection` | `items: Array<{ question: string, answer: string }>` | i18n |
|
||||
| `ScreenshotGallery` | `items: Array<{ src: string, alt: string, caption: string }>` | i18n |
|
||||
|
||||
---
|
||||
|
||||
## 5. Constraint System
|
||||
|
||||
### 5.1 NEVER Rules
|
||||
|
||||
| # | Category | Rule |
|
||||
|---|----------|------|
|
||||
| N-01 | Brand | NEVER use the `Mole` brand name in any user-facing text, metadata, or alt text. |
|
||||
| N-02 | Brand | NEVER claim malware protection, antivirus behavior, or security scanning capability. |
|
||||
| N-03 | Brand | NEVER overstate physical recovery coverage — always qualify with "when supported". |
|
||||
| N-04 | Brand | NEVER imply all releases are Apple-signed if the current release is a prerelease. |
|
||||
| N-05 | Security | NEVER include hardcoded GitHub tokens, API keys, or secrets in client-facing code. |
|
||||
| N-06 | Security | NEVER use client-side GitHub API fetches for critical first-paint release information. |
|
||||
| N-07 | Security | NEVER rely on a manually committed `CNAME` file when using a custom GitHub Actions Pages workflow. |
|
||||
| N-08 | Performance | NEVER ship a client JS bundle exceeding 20KB (excluding analytics). |
|
||||
| N-09 | Performance | NEVER load fonts from external CDNs (Google Fonts, etc.). |
|
||||
| N-10 | Performance | NEVER use framework islands (React, Vue, Svelte) for any component. |
|
||||
| N-11 | Aesthetics | NEVER use generic SaaS gradients, purple-heavy palettes, or interchangeable startup layouts. |
|
||||
| N-12 | Aesthetics | NEVER use endless floating particles, decorative animation loops, or bouncy spring motion. |
|
||||
| N-13 | Copy | NEVER use hype words: "ultimate", "magic", "AI cleaner", "revolutionary", "blazing fast". |
|
||||
| N-14 | Copy | NEVER use fear-based maintenance language: "Your Mac is at risk", "Critical error", "You must allow this". |
|
||||
|
||||
### 5.2 ALWAYS Rules
|
||||
|
||||
| # | Category | Rule |
|
||||
|---|----------|------|
|
||||
| A-01 | Disclosure | ALWAYS show exact version number and release date next to the download CTA. |
|
||||
| A-02 | Disclosure | ALWAYS show a channel badge (`Stable`, `Prerelease`, or `Coming Soon`) next to the CTA. |
|
||||
| A-03 | Disclosure | ALWAYS show a Gatekeeper install note when the release is a prerelease. |
|
||||
| A-04 | Accessibility | ALWAYS maintain WCAG 2.1 AA contrast ratios (4.5:1 for normal text, 3:1 for large text). |
|
||||
| A-05 | Accessibility | ALWAYS provide alt text for every image; screenshot alt text must describe the UI state shown. |
|
||||
| A-06 | Accessibility | ALWAYS ensure all interactive elements are keyboard-navigable with visible focus indicators. |
|
||||
| A-07 | i18n | ALWAYS include `hreflang` tags on both `/en/` and `/zh/` pages. |
|
||||
| A-08 | i18n | ALWAYS serve localized `<title>`, `<meta description>`, and Open Graph metadata per locale. |
|
||||
| A-09 | Testing | ALWAYS validate HTML output with the W3C validator before each deploy. |
|
||||
| A-10 | Testing | ALWAYS run Lighthouse CI on both locales before merging to main. |
|
||||
| A-11 | SEO | ALWAYS use crawlable `<h1>`–`<h6>` headings; never render hero text only inside images. |
|
||||
| A-12 | Copy | ALWAYS qualify recovery claims with "when supported" or "while the retention window is open". |
|
||||
| A-13 | Copy | ALWAYS use concrete verbs for CTAs: `Scan`, `Review`, `Restore`, `Download`. |
|
||||
|
||||
### 5.3 Naming Conventions
|
||||
|
||||
| Entity | Convention | Example |
|
||||
|--------|-----------|---------|
|
||||
| CSS custom property | `--atlas-{category}-{name}` | `--atlas-color-brand` |
|
||||
| Component file | PascalCase `.astro` | `FeatureCard.astro` |
|
||||
| CSS class | BEM-lite: `block__element--modifier` | `hero__cta--primary` |
|
||||
| i18n key | dot-separated section path | `hero.headline` |
|
||||
| Image file | kebab-case, descriptive | `atlas-overview.png` |
|
||||
| Script file | kebab-case `.ts` | `fetch-release.ts` |
|
||||
| Data file | kebab-case `.json` | `release-manifest.json` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Visual Design System
|
||||
|
||||
### 6.1 Design Thinking — Four Questions
|
||||
|
||||
**Q1: Who is viewing this page?**
|
||||
Mac users with disk pressure — both mainstream and developers — evaluating Atlas as an alternative to opaque commercial cleanup apps. They are cautious, technically aware, and skeptical of "magic cleaner" marketing.
|
||||
|
||||
**Q2: What should they feel?**
|
||||
"This tool understands my Mac and will be honest with me." Calm authority, not hype. Precision, not spectacle. The page should feel like a modern macOS-native operations console translated into a polished marketing surface.
|
||||
|
||||
**Q3: What is the single most important action?**
|
||||
Download the latest release (or, if prerelease, understand the install friction and proceed anyway).
|
||||
|
||||
**Q4: What could go wrong?**
|
||||
- User mistakes a prerelease for a stable release → mitigated by mandatory channel badge and warning
|
||||
- User bounces because the page looks like generic SaaS → mitigated by dark "precision utility" theme with native Mac feel
|
||||
- User can't find the download → mitigated by persistent CTA in nav + hero + footer
|
||||
|
||||
### 6.2 CSS Custom Properties — Design Tokens
|
||||
|
||||
All values are derived from `AtlasBrand.swift` and the Xcode color assets. The landing page uses a **dark-only** theme per PRD direction.
|
||||
|
||||
```css
|
||||
/* ══════════════════════════════════════════════════════
|
||||
Atlas Landing Page — Design Tokens
|
||||
Source of truth: AtlasBrand.swift + AtlasColors.xcassets
|
||||
Theme: "Precision Utility" (dark-only)
|
||||
══════════════════════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── Colors: Background ──────────────────────────── */
|
||||
--atlas-color-bg-base: #0D0F11; /* Graphite / near-black */
|
||||
--atlas-color-bg-surface: #1A1D21; /* Warm slate cards */
|
||||
--atlas-color-bg-surface-hover: #22262B; /* Card hover state */
|
||||
--atlas-color-bg-raised: rgba(255, 255, 255, 0.06); /* Glassmorphic tint (matches AtlasColor.cardRaised dark) */
|
||||
--atlas-color-bg-code: #151820; /* Code block background */
|
||||
|
||||
/* ── Colors: Brand ───────────────────────────────── */
|
||||
/*
|
||||
* AtlasBrand.colorset:
|
||||
* Light: sRGB(0.0588, 0.4627, 0.4314) = #0F766E
|
||||
* Dark: sRGB(0.0784, 0.5647, 0.5216) = #149085
|
||||
*
|
||||
* Landing page uses the dark variant as primary.
|
||||
*/
|
||||
--atlas-color-brand: #149085; /* AtlasBrand dark — primary teal */
|
||||
--atlas-color-brand-light: #0F766E; /* AtlasBrand light — used for hover contrast */
|
||||
--atlas-color-brand-glow: rgba(20, 144, 133, 0.25); /* CTA shadow glow */
|
||||
|
||||
/* ── Colors: Accent ──────────────────────────────── */
|
||||
/*
|
||||
* AtlasAccent.colorset:
|
||||
* Light: sRGB(0.2039, 0.8275, 0.6000) = #34D399
|
||||
* Dark: sRGB(0.3216, 0.8863, 0.7098) = #52E2B5
|
||||
*/
|
||||
--atlas-color-accent: #34D399; /* AtlasAccent light — mint highlight */
|
||||
--atlas-color-accent-bright: #52E2B5; /* AtlasAccent dark — brighter mint */
|
||||
|
||||
/* ── Colors: Semantic ────────────────────────────── */
|
||||
--atlas-color-success: #22C55E; /* systemGreen equivalent */
|
||||
--atlas-color-warning: #F59E0B; /* Amber — prerelease/caution states */
|
||||
--atlas-color-danger: #EF4444; /* systemRed equivalent */
|
||||
--atlas-color-info: #3B82F6; /* systemBlue equivalent */
|
||||
|
||||
/* ── Colors: Text ────────────────────────────────── */
|
||||
--atlas-color-text-primary: #F1F5F9; /* High contrast on dark bg */
|
||||
--atlas-color-text-secondary: #94A3B8; /* Muted body text */
|
||||
--atlas-color-text-tertiary: rgba(148, 163, 184, 0.6); /* Footnotes, timestamps (matches AtlasColor.textTertiary) */
|
||||
|
||||
/* ── Colors: Border ──────────────────────────────── */
|
||||
--atlas-color-border: rgba(241, 245, 249, 0.08); /* Subtle (matches AtlasColor.border) */
|
||||
--atlas-color-border-emphasis: rgba(241, 245, 249, 0.14); /* Focus/prominent (matches AtlasColor.borderEmphasis) */
|
||||
|
||||
/* ── Typography ──────────────────────────────────── */
|
||||
--atlas-font-display: 'Space Grotesk', system-ui, sans-serif;
|
||||
--atlas-font-body: 'Instrument Sans', system-ui, sans-serif;
|
||||
--atlas-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
|
||||
/* Display sizes */
|
||||
--atlas-text-hero: clamp(2.5rem, 5vw, 4rem); /* Hero headline */
|
||||
--atlas-text-hero-weight: 700;
|
||||
--atlas-text-section: clamp(1.75rem, 3.5vw, 2.5rem); /* Section title */
|
||||
--atlas-text-section-weight: 700;
|
||||
--atlas-text-card-title: 1.25rem; /* 20px — card heading */
|
||||
--atlas-text-card-title-weight: 600;
|
||||
|
||||
/* Body sizes (mapped from AtlasTypography) */
|
||||
--atlas-text-body: 1rem; /* 16px — standard body */
|
||||
--atlas-text-body-weight: 400;
|
||||
--atlas-text-body-small: 0.875rem; /* 14px — secondary body */
|
||||
--atlas-text-label: 0.875rem; /* 14px — semibold label */
|
||||
--atlas-text-label-weight: 600;
|
||||
--atlas-text-caption: 0.75rem; /* 12px — chips, footnotes */
|
||||
--atlas-text-caption-weight: 600;
|
||||
--atlas-text-caption-small: 0.6875rem; /* 11px — legal, timestamps */
|
||||
|
||||
/* Line heights */
|
||||
--atlas-leading-tight: 1.2; /* Display text */
|
||||
--atlas-leading-normal: 1.6; /* Body text */
|
||||
--atlas-leading-relaxed: 1.8; /* Long-form reading */
|
||||
|
||||
/* Letter spacing */
|
||||
--atlas-tracking-tight: -0.02em; /* Display */
|
||||
--atlas-tracking-normal: 0; /* Body */
|
||||
--atlas-tracking-wide: 0.05em; /* Overlines, badges */
|
||||
|
||||
/* ── Spacing (4pt grid from AtlasSpacing) ────────── */
|
||||
--atlas-space-xxs: 4px; /* AtlasSpacing.xxs */
|
||||
--atlas-space-xs: 6px; /* AtlasSpacing.xs */
|
||||
--atlas-space-sm: 8px; /* AtlasSpacing.sm */
|
||||
--atlas-space-md: 12px; /* AtlasSpacing.md */
|
||||
--atlas-space-lg: 16px; /* AtlasSpacing.lg */
|
||||
--atlas-space-xl: 20px; /* AtlasSpacing.xl */
|
||||
--atlas-space-xxl: 24px; /* AtlasSpacing.xxl */
|
||||
--atlas-space-screen-h: 28px; /* AtlasSpacing.screenH */
|
||||
--atlas-space-section: 32px; /* AtlasSpacing.section */
|
||||
|
||||
/* Web-specific extended spacing */
|
||||
--atlas-space-section-gap: 80px; /* Between page sections */
|
||||
--atlas-space-section-gap-lg: 120px; /* Hero → Trust strip gap */
|
||||
|
||||
/* ── Radius (continuous corners from AtlasRadius) ── */
|
||||
--atlas-radius-sm: 8px; /* AtlasRadius.sm — chips, tags */
|
||||
--atlas-radius-md: 12px; /* AtlasRadius.md — inline cards */
|
||||
--atlas-radius-lg: 16px; /* AtlasRadius.lg — detail rows */
|
||||
--atlas-radius-xl: 20px; /* AtlasRadius.xl — standard cards */
|
||||
--atlas-radius-xxl: 24px; /* AtlasRadius.xxl — hero cards */
|
||||
--atlas-radius-full: 9999px; /* Pills, badges, CTA buttons */
|
||||
|
||||
/* ── Elevation (shadow system from AtlasElevation) ── */
|
||||
/* Flat — no shadow */
|
||||
--atlas-shadow-flat: none;
|
||||
|
||||
/* Raised — default card level */
|
||||
--atlas-shadow-raised: 0 10px 18px rgba(0, 0, 0, 0.05);
|
||||
--atlas-shadow-raised-border: rgba(241, 245, 249, 0.08);
|
||||
|
||||
/* Prominent — hero cards, primary action areas */
|
||||
--atlas-shadow-prominent: 0 16px 28px rgba(0, 0, 0, 0.09);
|
||||
--atlas-shadow-prominent-border: rgba(241, 245, 249, 0.12);
|
||||
|
||||
/* CTA glow */
|
||||
--atlas-shadow-cta: 0 6px 12px rgba(20, 144, 133, 0.25);
|
||||
--atlas-shadow-cta-hover: 0 8px 20px rgba(20, 144, 133, 0.35);
|
||||
|
||||
/* ── Motion (from AtlasMotion) ───────────────────── */
|
||||
--atlas-motion-fast: 150ms cubic-bezier(0.2, 0, 0, 1); /* Hover, press */
|
||||
--atlas-motion-standard: 220ms cubic-bezier(0.2, 0, 0, 1); /* Toggle, selection */
|
||||
--atlas-motion-slow: 350ms cubic-bezier(0.2, 0, 0, 1); /* Page transitions */
|
||||
--atlas-motion-spring: 450ms cubic-bezier(0.34, 1.56, 0.64, 1); /* Playful feedback */
|
||||
|
||||
/* Staggered section reveal */
|
||||
--atlas-stagger-delay: 80ms; /* Delay between items in stagger */
|
||||
|
||||
/* ── Layout (from AtlasLayout) ───────────────────── */
|
||||
--atlas-width-reading: 920px; /* AtlasLayout.maxReadingWidth */
|
||||
--atlas-width-workspace: 1200px; /* AtlasLayout.maxWorkspaceWidth */
|
||||
--atlas-width-content: 1080px; /* AtlasLayout.maxWorkflowWidth — main content ceiling */
|
||||
|
||||
/* Responsive breakpoints */
|
||||
--atlas-bp-sm: 640px; /* Mobile → Tablet */
|
||||
--atlas-bp-md: 860px; /* Matches AtlasLayout.browserSplitThreshold */
|
||||
--atlas-bp-lg: 1080px; /* Tablet → Desktop */
|
||||
--atlas-bp-xl: 1280px; /* Wide desktop */
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Five-Dimension Design Decisions
|
||||
|
||||
| Dimension | Decision | Rationale |
|
||||
|-----------|----------|-----------|
|
||||
| **Color** | Dark-only graphite base (#0D0F11) with teal brand (#149085) and mint accent (#34D399) | PRD specifies "graphite/near-black" background; teal carries trust; mint provides discovery cues without clashing |
|
||||
| **Typography** | Space Grotesk (display) + Instrument Sans (body) + IBM Plex Mono (utility) | Geometric display for tech authority; humanist sans for readability; monospace for version/code credibility |
|
||||
| **Spacing** | 80px section gap, 4pt internal grid, max 1080px content width | Generous whitespace per PRD "not crowded"; 4pt grid matches native app; reading width prevents long lines |
|
||||
| **Shape** | Continuous corners (8–24px radius scale), capsule CTAs, no sharp edges | Maps directly from `AtlasRadius`; "rounded but not bubbly" per PRD; capsule CTAs match `AtlasPrimaryButtonStyle` |
|
||||
| **Motion** | Staggered section reveal on scroll, 150ms hover transitions, no decorative loops | PRD: "snappy but never bouncy"; intersection observer triggers for progressive reveal; respects `prefers-reduced-motion` |
|
||||
|
||||
### 6.4 Section Band Pattern
|
||||
|
||||
The page alternates between "dark" and "surface" bands to create visual rhythm:
|
||||
|
||||
```
|
||||
Section Background Band
|
||||
─────────────────────────────────────────────────────
|
||||
NavBar transparent → bg-base —
|
||||
Hero bg-base Dark
|
||||
Trust Strip bg-surface Surface
|
||||
Problem → Outcome bg-base Dark
|
||||
Feature Grid bg-surface Surface
|
||||
How It Works bg-base Dark
|
||||
Developer Section bg-surface Surface
|
||||
Safety Section bg-base Dark
|
||||
Screenshot Gallery bg-surface Surface
|
||||
Open Source bg-base Dark
|
||||
FAQ bg-surface Surface
|
||||
Footer bg-base + border-top Dark
|
||||
```
|
||||
|
||||
### 6.5 Component Styling Convention
|
||||
|
||||
**BEM-lite + CSS custom properties**:
|
||||
|
||||
```css
|
||||
/* Block */
|
||||
.hero { ... }
|
||||
|
||||
/* Element */
|
||||
.hero__headline { ... }
|
||||
.hero__cta { ... }
|
||||
|
||||
/* Modifier */
|
||||
.hero__cta--primary { ... }
|
||||
.hero__cta--secondary { ... }
|
||||
```
|
||||
|
||||
**Card pattern** (maps from `AtlasCardModifier`):
|
||||
|
||||
```css
|
||||
.card {
|
||||
padding: var(--atlas-space-xl); /* 20px — AtlasSpacing.xl */
|
||||
background: var(--atlas-color-bg-surface);
|
||||
border: 1px solid var(--atlas-color-border); /* 0.08 opacity */
|
||||
border-radius: var(--atlas-radius-xl); /* 20px — AtlasRadius.xl */
|
||||
box-shadow: var(--atlas-shadow-raised); /* 0 10px 18px */
|
||||
transition: transform var(--atlas-motion-fast),
|
||||
box-shadow var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: scale(1.008); /* Matches AtlasHoverModifier */
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card--prominent {
|
||||
border-radius: var(--atlas-radius-xxl); /* 24px — AtlasRadius.xxl */
|
||||
border-width: 1.5px;
|
||||
border-color: var(--atlas-shadow-prominent-border);
|
||||
box-shadow: var(--atlas-shadow-prominent);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.08) 0%, transparent 50%),
|
||||
var(--atlas-color-bg-surface); /* Top-left inner glow */
|
||||
}
|
||||
```
|
||||
|
||||
**CTA button pattern** (maps from `AtlasPrimaryButtonStyle`):
|
||||
|
||||
```css
|
||||
.cta--primary {
|
||||
font-family: var(--atlas-font-body);
|
||||
font-size: var(--atlas-text-label);
|
||||
font-weight: var(--atlas-text-label-weight);
|
||||
color: #FFFFFF;
|
||||
padding: var(--atlas-space-md) var(--atlas-space-xxl); /* 12px 24px */
|
||||
background: var(--atlas-color-brand);
|
||||
border-radius: var(--atlas-radius-full); /* Capsule */
|
||||
box-shadow: var(--atlas-shadow-cta);
|
||||
transition: transform var(--atlas-motion-fast),
|
||||
box-shadow var(--atlas-motion-fast);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cta--primary:hover {
|
||||
box-shadow: var(--atlas-shadow-cta-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cta--primary:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 2px 4px rgba(20, 144, 133, 0.15);
|
||||
}
|
||||
|
||||
.cta--primary:disabled {
|
||||
background: rgba(20, 144, 133, 0.4);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
```
|
||||
|
||||
**Badge pattern** (channel badge):
|
||||
|
||||
```css
|
||||
.badge {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-caption);
|
||||
font-weight: var(--atlas-text-caption-weight);
|
||||
letter-spacing: var(--atlas-tracking-wide);
|
||||
text-transform: uppercase;
|
||||
padding: var(--atlas-space-xxs) var(--atlas-space-sm);
|
||||
border-radius: var(--atlas-radius-sm);
|
||||
}
|
||||
|
||||
.badge--stable {
|
||||
background: rgba(20, 144, 133, 0.15);
|
||||
color: var(--atlas-color-accent);
|
||||
border: 1px solid rgba(20, 144, 133, 0.3);
|
||||
}
|
||||
|
||||
.badge--prerelease {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: var(--atlas-color-warning);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.badge--coming {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 Responsive Strategy
|
||||
|
||||
| Breakpoint | Layout Behavior |
|
||||
|------------|----------------|
|
||||
| < 640px (mobile) | Single column; hero screenshot below CTA; feature cards stack; screenshot carousel; hamburger nav |
|
||||
| 640–860px (tablet) | Two-column feature grid; hero screenshot beside text; nav items visible |
|
||||
| 860–1080px (small desktop) | Three-column feature grid; full nav; gallery grid |
|
||||
| > 1080px (desktop) | Max content width 1080px centered; generous margins |
|
||||
|
||||
### 6.7 Font Loading Strategy
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url('/fonts/SpaceGrotesk-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-024F, U+4E00-9FFF; /* Latin + CJK */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
src: url('/fonts/InstrumentSans-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-024F, U+4E00-9FFF;
|
||||
}
|
||||
|
||||
/* ... additional faces for Medium weights and IBM Plex Mono */
|
||||
```
|
||||
|
||||
Preload critical fonts in `<head>`:
|
||||
|
||||
```html
|
||||
<link rel="preload" href="/fonts/SpaceGrotesk-Bold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/InstrumentSans-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Quality Gates
|
||||
|
||||
### 7.1 Core Web Vitals
|
||||
|
||||
| Metric | Target | Tool |
|
||||
|--------|--------|------|
|
||||
| Largest Contentful Paint (LCP) | < 2.0s | Lighthouse CI |
|
||||
| Interaction to Next Paint (INP) | < 100ms | Lighthouse CI |
|
||||
| Cumulative Layout Shift (CLS) | < 0.05 | Lighthouse CI |
|
||||
| Lighthouse Performance Score | >= 95 | Lighthouse CI |
|
||||
| Lighthouse Accessibility Score | >= 95 | Lighthouse CI |
|
||||
| Lighthouse Best Practices Score | >= 95 | Lighthouse CI |
|
||||
| Lighthouse SEO Score | >= 95 | Lighthouse CI |
|
||||
| Total Client JS | < 20KB (gzip) | Build output check |
|
||||
| Total Page Weight | < 500KB (excl. screenshots) | Build output check |
|
||||
|
||||
### 7.2 Accessibility (WCAG 2.1 AA)
|
||||
|
||||
| Check | Requirement |
|
||||
|-------|-------------|
|
||||
| Color contrast | 4.5:1 minimum for normal text; 3:1 for large text (>= 18px bold / >= 24px) |
|
||||
| Keyboard navigation | All interactive elements focusable; visible focus ring; logical tab order |
|
||||
| Screen reader | Semantic HTML (`<nav>`, `<main>`, `<section>`, `<article>`); ARIA labels where needed |
|
||||
| Alt text | Every `<img>` has descriptive alt text; decorative images use `alt=""` |
|
||||
| Reduced motion | `@media (prefers-reduced-motion: reduce)` disables all animations |
|
||||
| Language | `<html lang="zh-Hans">` / `<html lang="en">` set per locale |
|
||||
|
||||
### 7.3 CI Pipeline
|
||||
|
||||
**File**: `.github/workflows/landing-page.yml`
|
||||
|
||||
```yaml
|
||||
name: Landing Page
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'Apps/LandingSite/**'
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: Apps/LandingSite/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Fetch release manifest
|
||||
working-directory: Apps/LandingSite
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm run fetch-release
|
||||
|
||||
- name: Build static site
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm run build
|
||||
|
||||
- name: Validate HTML
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm run validate
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
working-directory: Apps/LandingSite
|
||||
run: pnpm run lighthouse
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: Apps/LandingSite/dist
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
```
|
||||
|
||||
**Trigger conditions**:
|
||||
1. Push to `main` that modifies `Apps/LandingSite/**`
|
||||
2. GitHub Release publication (triggers manifest regeneration)
|
||||
3. Manual dispatch for ad-hoc deploys
|
||||
|
||||
### 7.4 Acceptance Criteria → PRD Traceability
|
||||
|
||||
| FR | Requirement | How Addressed |
|
||||
|----|-------------|---------------|
|
||||
| FR-01 | Release metadata (version, channel, date, asset links) | `ReleaseManifest` schema (§3.1); Hero + Footer render from manifest; build-time fetch |
|
||||
| FR-02 | Channel-aware UI (prerelease badge, warning, Gatekeeper help) | `ChannelBadge` component; release state machine (§1.3); SafetySection Gatekeeper guide |
|
||||
| FR-03 | Bilingual support (EN + ZH, stable URLs) | Path-based i18n (`/en/`, `/zh/`); `LandingCopy` schema (§3.2); `hreflang` tags (A-07) |
|
||||
| FR-04 | Download path clarity (where, which file, prerelease, Gatekeeper) | Hero CTA links to correct asset; ChannelBadge shows state; SafetySection explains Gatekeeper |
|
||||
| FR-05 | Trust links (GitHub, releases, changelog, security, license) | OpenSourceSection + FooterSection; links derived from constants |
|
||||
| FR-06 | Optional beta email capture | Deferred to 3rd-party form endpoint slot in FooterSection; no custom backend |
|
||||
| FR-07 | Responsive behavior (desktop, tablet, mobile) | Responsive strategy (§6.6); breakpoints at 640/860/1080px; no CTA hidden in accordion |
|
||||
|
||||
### 7.5 User Story → Verification
|
||||
|
||||
| Story | Test |
|
||||
|-------|------|
|
||||
| US-01: Understand Atlas in one screen | Lighthouse "First Meaningful Paint" check; manual review that headline + subheadline + screenshot are above the fold on 1280×720 viewport |
|
||||
| US-02: Safety and trust signals | Automated check: TrustStrip rendered; SafetySection contains "recovery", "permissions", "Gatekeeper" keywords; OpenSourceSection links to GitHub |
|
||||
| US-03: Developer cleanup awareness | DeveloperSection renders with >= 4 concrete developer artifact types (Xcode derived data, simulators, package caches, build artifacts) |
|
||||
| US-04: Download without GitHub navigation | Hero CTA `href` matches `release-manifest.json` asset URL; version and date rendered next to CTA |
|
||||
| US-05: Honest prerelease disclosure | When `channel === "prerelease"`: ChannelBadge shows amber "Prerelease" badge; warning text visible; Gatekeeper "Open Anyway" steps visible |
|
||||
|
||||
### 7.6 Pre-Deploy Checklist
|
||||
|
||||
- [ ] `pnpm run build` succeeds with zero warnings
|
||||
- [ ] `release-manifest.json` is valid and matches latest GitHub Release
|
||||
- [ ] Both `/en/` and `/zh/` render without missing translation keys
|
||||
- [ ] Lighthouse scores >= 95 on all four categories for both locales
|
||||
- [ ] All images have alt text; screenshots have descriptive captions
|
||||
- [ ] `hreflang` tags present and correct on both locale pages
|
||||
- [ ] Open Graph meta tags render correct title, description, and image per locale
|
||||
- [ ] No `Mole` brand references in rendered HTML
|
||||
- [ ] Client JS budget < 20KB confirmed via build output
|
||||
- [ ] `robots.txt` and `sitemap.xml` present and valid
|
||||
- [ ] Custom domain DNS verified and HTTPS enforced
|
||||
- [ ] Plausible analytics tracking confirmed on both locales
|
||||
- [ ] Mobile viewport (375px) preserves CTA visibility and screenshot clarity
|
||||
19
Apps/LandingSite/astro.config.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://atlas.atomstorm.ai',
|
||||
output: 'static',
|
||||
integrations: [sitemap()],
|
||||
i18n: {
|
||||
defaultLocale: 'zh',
|
||||
locales: ['zh', 'en'],
|
||||
routing: {
|
||||
prefixDefaultLocale: true,
|
||||
redirectToDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
assets: '_assets',
|
||||
},
|
||||
});
|
||||
21
Apps/LandingSite/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "atlas-landing",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"fetch-release": "tsx scripts/fetch-release.ts",
|
||||
"prebuild": "tsx scripts/fetch-release.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.7.0",
|
||||
"@astrojs/sitemap": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
3530
Apps/LandingSite/pnpm-lock.yaml
generated
Normal file
BIN
Apps/LandingSite/public/fonts/IBMPlexMono-Medium.woff2
Normal file
BIN
Apps/LandingSite/public/fonts/IBMPlexMono-Regular.woff2
Normal file
BIN
Apps/LandingSite/public/fonts/InstrumentSans-Medium.woff2
Normal file
BIN
Apps/LandingSite/public/fonts/InstrumentSans-Regular.woff2
Normal file
BIN
Apps/LandingSite/public/fonts/SpaceGrotesk-Bold.woff2
Normal file
BIN
Apps/LandingSite/public/fonts/SpaceGrotesk-Medium.woff2
Normal file
BIN
Apps/LandingSite/public/images/atlas-icon.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
Apps/LandingSite/public/images/og-atlas-en.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
Apps/LandingSite/public/images/og-atlas-zh.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
Apps/LandingSite/public/images/screenshots/atlas-apps.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Apps/LandingSite/public/images/screenshots/atlas-history.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Apps/LandingSite/public/images/screenshots/atlas-overview.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
Apps/LandingSite/public/images/screenshots/atlas-settings.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
Apps/LandingSite/public/images/screenshots/atlas-smart-clean.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
4
Apps/LandingSite/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://atlas.atomstorm.ai/sitemap-index.xml
|
||||
142
Apps/LandingSite/scripts/fetch-release.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Build-time script: Fetches latest release from GitHub API
|
||||
* and writes release-manifest.json for the landing page.
|
||||
*
|
||||
* Usage: tsx scripts/fetch-release.ts
|
||||
* Env: GITHUB_TOKEN (optional, increases rate limit)
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const OWNER = 'nicekid1';
|
||||
const REPO = 'CleanMyPc';
|
||||
const OUTPUT = join(__dirname, '..', 'src', 'data', 'release-manifest.json');
|
||||
|
||||
interface ReleaseManifest {
|
||||
channel: 'stable' | 'prerelease' | 'none';
|
||||
version: string | null;
|
||||
publishedAt: string | null;
|
||||
releaseUrl: string | null;
|
||||
assets: {
|
||||
dmg: string | null;
|
||||
zip: string | null;
|
||||
pkg: string | null;
|
||||
sha256: string | null;
|
||||
};
|
||||
gatekeeperWarning: boolean;
|
||||
installNote: string | null;
|
||||
tagName: string | null;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
function findAsset(assets: any[], suffix: string): string | null {
|
||||
const asset = assets.find((a: any) =>
|
||||
a.name.toLowerCase().endsWith(suffix.toLowerCase())
|
||||
);
|
||||
return asset?.browser_download_url ?? null;
|
||||
}
|
||||
|
||||
async function fetchRelease(): Promise<ReleaseManifest> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'atlas-landing-build',
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
// Try latest release first, then fall back to all releases
|
||||
let release: any = null;
|
||||
|
||||
try {
|
||||
const latestRes = await fetch(
|
||||
`https://api.github.com/repos/${OWNER}/${REPO}/releases/latest`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (latestRes.ok) {
|
||||
release = await latestRes.json();
|
||||
}
|
||||
} catch {
|
||||
// Ignore — try all releases next
|
||||
}
|
||||
|
||||
if (!release) {
|
||||
try {
|
||||
const allRes = await fetch(
|
||||
`https://api.github.com/repos/${OWNER}/${REPO}/releases?per_page=5`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (allRes.ok) {
|
||||
const releases = await allRes.json();
|
||||
if (Array.isArray(releases) && releases.length > 0) {
|
||||
release = releases[0];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Will return "none" channel
|
||||
}
|
||||
}
|
||||
|
||||
if (!release) {
|
||||
return {
|
||||
channel: 'none',
|
||||
version: null,
|
||||
publishedAt: null,
|
||||
releaseUrl: `https://github.com/${OWNER}/${REPO}`,
|
||||
assets: { dmg: null, zip: null, pkg: null, sha256: null },
|
||||
gatekeeperWarning: false,
|
||||
installNote: null,
|
||||
tagName: null,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
const isPrerelease = release.prerelease === true;
|
||||
const version = (release.tag_name ?? '').replace(/^V/i, '') || null;
|
||||
const assets = release.assets ?? [];
|
||||
|
||||
return {
|
||||
channel: isPrerelease ? 'prerelease' : 'stable',
|
||||
version,
|
||||
publishedAt: release.published_at ?? null,
|
||||
releaseUrl: release.html_url ?? `https://github.com/${OWNER}/${REPO}/releases`,
|
||||
assets: {
|
||||
dmg: findAsset(assets, '.dmg'),
|
||||
zip: findAsset(assets, '.zip'),
|
||||
pkg: findAsset(assets, '.pkg'),
|
||||
sha256: findAsset(assets, '.sha256'),
|
||||
},
|
||||
gatekeeperWarning: isPrerelease,
|
||||
installNote: isPrerelease
|
||||
? 'This build is development-signed. macOS Gatekeeper may require "Open Anyway" or a right-click "Open" flow.'
|
||||
: null,
|
||||
tagName: release.tag_name ?? null,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Fetching release data for ${OWNER}/${REPO}...`);
|
||||
|
||||
try {
|
||||
const manifest = await fetchRelease();
|
||||
writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2) + '\n');
|
||||
console.log(`Wrote ${OUTPUT}`);
|
||||
console.log(` channel: ${manifest.channel}`);
|
||||
console.log(` version: ${manifest.version ?? '(none)'}`);
|
||||
console.log(` gatekeeperWarning: ${manifest.gatekeeperWarning}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch release, using fallback:', err);
|
||||
// The build will use release-fallback.json via the data loader
|
||||
process.exit(0); // Don't fail the build
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
78
Apps/LandingSite/src/components/ChannelBadge.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
interface Props {
|
||||
channel: 'stable' | 'prerelease' | 'none';
|
||||
label: string;
|
||||
version?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
const { channel, label, version, date } = Astro.props;
|
||||
---
|
||||
|
||||
<span class={`badge badge--${channel}`}>
|
||||
<span class="badge__dot" aria-hidden="true"></span>
|
||||
<span class="badge__label">{label}</span>
|
||||
{version && <span class="badge__version">v{version}</span>}
|
||||
{date && <span class="badge__date">{date}</span>}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-xs);
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-caption);
|
||||
font-weight: var(--atlas-text-caption-weight);
|
||||
letter-spacing: var(--atlas-tracking-wide);
|
||||
text-transform: uppercase;
|
||||
padding: var(--atlas-space-xxs) var(--atlas-space-sm);
|
||||
border-radius: var(--atlas-radius-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.badge__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge__version,
|
||||
.badge__date {
|
||||
opacity: 0.7;
|
||||
font-size: var(--atlas-text-caption-small);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.badge--stable {
|
||||
background: rgba(20, 144, 133, 0.15);
|
||||
color: var(--atlas-color-accent);
|
||||
border: 1px solid rgba(20, 144, 133, 0.3);
|
||||
}
|
||||
|
||||
.badge--stable .badge__dot {
|
||||
background-color: var(--atlas-color-accent);
|
||||
}
|
||||
|
||||
.badge--prerelease {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: var(--atlas-color-warning);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.badge--prerelease .badge__dot {
|
||||
background-color: var(--atlas-color-warning);
|
||||
}
|
||||
|
||||
.badge--none {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.badge--none .badge__dot {
|
||||
background-color: var(--atlas-color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
98
Apps/LandingSite/src/components/CtaButton.astro
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
interface Props {
|
||||
label: string;
|
||||
href: string;
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
external?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
label,
|
||||
href,
|
||||
variant = 'primary',
|
||||
external = true,
|
||||
class: className = '',
|
||||
} = Astro.props;
|
||||
|
||||
const classes = `cta cta--${variant} ${className}`.trim();
|
||||
const linkAttrs = external ? { target: '_blank', rel: 'noopener noreferrer' } : {};
|
||||
---
|
||||
|
||||
<a href={href} class={classes} {...linkAttrs}>
|
||||
{label}
|
||||
{variant === 'secondary' && (
|
||||
<svg class="cta__icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-sm);
|
||||
font-family: var(--atlas-font-body);
|
||||
font-size: var(--atlas-text-label);
|
||||
font-weight: var(--atlas-text-label-weight);
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
border-radius: var(--atlas-radius-full);
|
||||
transition: transform var(--atlas-motion-fast),
|
||||
box-shadow var(--atlas-motion-fast),
|
||||
background-color var(--atlas-motion-fast);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cta--primary {
|
||||
color: #ffffff;
|
||||
padding: var(--atlas-space-md) var(--atlas-space-xxl);
|
||||
background-color: var(--atlas-color-brand);
|
||||
box-shadow: var(--atlas-shadow-cta);
|
||||
}
|
||||
|
||||
.cta--primary:hover {
|
||||
box-shadow: var(--atlas-shadow-cta-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cta--primary:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 2px 4px rgba(20, 144, 133, 0.15);
|
||||
}
|
||||
|
||||
.cta--secondary {
|
||||
color: var(--atlas-color-brand);
|
||||
padding: var(--atlas-space-md) var(--atlas-space-xxl);
|
||||
background-color: rgba(20, 144, 133, 0.04);
|
||||
border: 1.5px solid rgba(20, 144, 133, 0.3);
|
||||
}
|
||||
|
||||
.cta--secondary:hover {
|
||||
background-color: rgba(20, 144, 133, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cta--secondary:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.cta--ghost {
|
||||
color: var(--atlas-color-brand);
|
||||
padding: var(--atlas-space-sm) var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.cta--ghost:hover {
|
||||
background-color: rgba(20, 144, 133, 0.06);
|
||||
}
|
||||
|
||||
.cta--ghost:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.cta__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
82
Apps/LandingSite/src/components/DeveloperSection.astro
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="dev section band--surface fade-in" id="developers">
|
||||
<div class="dev__inner container">
|
||||
<div class="dev__header">
|
||||
<h2 class="dev__title">{copy.developer.sectionTitle}</h2>
|
||||
<p class="dev__subtitle">{copy.developer.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="dev__grid">
|
||||
{copy.developer.items.map((item) => (
|
||||
<div class="dev__card">
|
||||
<h3 class="dev__card-title">{item.title}</h3>
|
||||
<p class="dev__card-desc">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.dev__header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.dev__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
margin-bottom: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.dev__subtitle {
|
||||
font-size: clamp(1rem, 2vw, 1.125rem);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.dev__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: var(--atlas-space-xl);
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.dev__grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.dev__card {
|
||||
padding: var(--atlas-space-xl);
|
||||
background: var(--atlas-color-bg-base);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-lg);
|
||||
}
|
||||
|
||||
.dev__card-title {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-body-small);
|
||||
font-weight: 500;
|
||||
color: var(--atlas-color-accent);
|
||||
margin-bottom: var(--atlas-space-sm);
|
||||
}
|
||||
|
||||
.dev__card-desc {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
</style>
|
||||
97
Apps/LandingSite/src/components/FaqSection.astro
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="faq section band--surface fade-in" id="faq">
|
||||
<div class="faq__inner container">
|
||||
<h2 class="faq__title">{copy.faq.sectionTitle}</h2>
|
||||
|
||||
<div class="faq__list">
|
||||
{copy.faq.items.map((item) => (
|
||||
<details class="faq__item">
|
||||
<summary class="faq__question">
|
||||
<span>{item.question}</span>
|
||||
<svg class="faq__chevron" width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path d="M5 8L10 13L15 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="faq__answer">
|
||||
<p>{item.answer}</p>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.faq__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.faq__list {
|
||||
max-width: var(--atlas-width-reading);
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.faq__item {
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-lg);
|
||||
background: var(--atlas-color-bg-base);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.faq__item[open] {
|
||||
border-color: var(--atlas-color-border-emphasis);
|
||||
}
|
||||
|
||||
.faq__question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-lg);
|
||||
padding: var(--atlas-space-xl);
|
||||
font-size: var(--atlas-text-body);
|
||||
font-weight: 500;
|
||||
color: var(--atlas-color-text-primary);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.faq__question:hover {
|
||||
color: var(--atlas-color-accent);
|
||||
}
|
||||
|
||||
.faq__chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
transition: transform var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.faq__item[open] .faq__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.faq__answer {
|
||||
padding: 0 var(--atlas-space-xl) var(--atlas-space-xl);
|
||||
}
|
||||
|
||||
.faq__answer p {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-relaxed);
|
||||
}
|
||||
</style>
|
||||
74
Apps/LandingSite/src/components/FeatureCard.astro
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string;
|
||||
example: string;
|
||||
trustCue: string;
|
||||
}
|
||||
|
||||
const { title, value, example, trustCue } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="feature-card fade-in">
|
||||
<h3 class="feature-card__title">{title}</h3>
|
||||
<p class="feature-card__value">{value}</p>
|
||||
<p class="feature-card__example">{example}</p>
|
||||
<p class="feature-card__trust">
|
||||
<svg class="feature-card__trust-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path d="M11.5 4L5.5 10L2.5 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{trustCue}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.feature-card {
|
||||
padding: var(--atlas-space-xl);
|
||||
background: var(--atlas-color-bg-surface);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-xl);
|
||||
box-shadow: var(--atlas-shadow-raised);
|
||||
transition: transform var(--atlas-motion-fast),
|
||||
box-shadow var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: scale(1.008);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.feature-card__title {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: var(--atlas-text-card-title);
|
||||
font-weight: var(--atlas-text-card-title-weight);
|
||||
color: var(--atlas-color-text-primary);
|
||||
margin-bottom: var(--atlas-space-sm);
|
||||
}
|
||||
|
||||
.feature-card__value {
|
||||
font-size: var(--atlas-text-body);
|
||||
color: var(--atlas-color-text-primary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
margin-bottom: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.feature-card__example {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
margin-bottom: var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.feature-card__trust {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-xs);
|
||||
font-size: var(--atlas-text-caption);
|
||||
font-weight: var(--atlas-text-caption-weight);
|
||||
color: var(--atlas-color-accent);
|
||||
}
|
||||
|
||||
.feature-card__trust-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
49
Apps/LandingSite/src/components/FeatureGrid.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import FeatureCard from './FeatureCard.astro';
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="features section band--surface" id="features">
|
||||
<div class="features__inner container">
|
||||
<h2 class="features__title fade-in">{copy.features.sectionTitle}</h2>
|
||||
|
||||
<div class="features__grid">
|
||||
{copy.features.cards.map((card) => (
|
||||
<FeatureCard
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
example={card.example}
|
||||
trustCue={card.trustCue}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.features__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.features__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--atlas-space-xxl);
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.features__grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
120
Apps/LandingSite/src/components/FooterSection.astro
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
import CtaButton from './CtaButton.astro';
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
import { getRelease, getDownloadUrl } from '../data/release';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
const manifest = getRelease();
|
||||
const downloadUrl = getDownloadUrl(manifest);
|
||||
|
||||
const REPO_URL = 'https://github.com/nicekid1/CleanMyPc';
|
||||
---
|
||||
|
||||
<footer class="footer band--dark">
|
||||
<div class="footer__inner container">
|
||||
<div class="footer__top">
|
||||
<div class="footer__brand">
|
||||
<img src="/images/atlas-icon.png" alt="" width="40" height="40" class="footer__logo" />
|
||||
<div>
|
||||
<p class="footer__name">Atlas for Mac</p>
|
||||
<p class="footer__tagline">{locale === 'zh' ? '可解释、恢复优先的 Mac 维护工作区' : 'Explainable, recovery-first Mac maintenance'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CtaButton
|
||||
label={copy.footer.download}
|
||||
href={downloadUrl}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="footer__links" aria-label="Footer navigation">
|
||||
<a href={downloadUrl} class="footer__link">{copy.footer.download}</a>
|
||||
<a href={REPO_URL} class="footer__link" target="_blank" rel="noopener noreferrer">{copy.footer.github}</a>
|
||||
<a href={`${REPO_URL}#readme`} class="footer__link" target="_blank" rel="noopener noreferrer">{copy.footer.documentation}</a>
|
||||
<a href={`${REPO_URL}/blob/main/Docs/PRIVACY.md`} class="footer__link" target="_blank" rel="noopener noreferrer">{copy.footer.privacy}</a>
|
||||
<a href={`${REPO_URL}/security`} class="footer__link" target="_blank" rel="noopener noreferrer">{copy.footer.security}</a>
|
||||
</nav>
|
||||
|
||||
<div class="footer__bottom">
|
||||
<p class="footer__copyright">{copy.footer.copyright}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
border-top: 1px solid var(--atlas-color-border);
|
||||
padding-block: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.footer__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-section);
|
||||
}
|
||||
|
||||
.footer__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--atlas-space-xl);
|
||||
}
|
||||
|
||||
.footer__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.footer__logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--atlas-radius-md);
|
||||
}
|
||||
|
||||
.footer__name {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.footer__tagline {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
}
|
||||
|
||||
.footer__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--atlas-space-xxl);
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.footer__link:hover {
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.footer__bottom {
|
||||
padding-top: var(--atlas-space-xl);
|
||||
border-top: 1px solid var(--atlas-color-border);
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
font-size: var(--atlas-text-caption-small);
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
189
Apps/LandingSite/src/components/Hero.astro
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
import CtaButton from './CtaButton.astro';
|
||||
import ChannelBadge from './ChannelBadge.astro';
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
import { getRelease, getDownloadUrl, formatDate } from '../data/release';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
const manifest = getRelease();
|
||||
const downloadUrl = getDownloadUrl(manifest);
|
||||
|
||||
const badgeLabels = {
|
||||
stable: copy.hero.badgeStable,
|
||||
prerelease: copy.hero.badgePrerelease,
|
||||
none: copy.hero.badgeComingSoon,
|
||||
};
|
||||
|
||||
const REPO_URL = 'https://github.com/nicekid1/CleanMyPc';
|
||||
|
||||
// Per state machine: stable/prerelease → download CTA + GitHub secondary
|
||||
// none → GitHub as primary, no secondary download
|
||||
const primaryLabel = manifest.channel === 'none'
|
||||
? copy.hero.ctaSecondary // "View Source" / "查看源码"
|
||||
: copy.hero.ctaPrimary; // "Download Atlas" / "下载 Atlas"
|
||||
|
||||
const primaryHref = manifest.channel === 'none'
|
||||
? REPO_URL
|
||||
: downloadUrl;
|
||||
|
||||
const showSecondary = manifest.channel !== 'none';
|
||||
|
||||
const publishedDate = formatDate(manifest.publishedAt, locale);
|
||||
---
|
||||
|
||||
<section class="hero band--dark" id="hero">
|
||||
<div class="hero__inner container">
|
||||
<div class="hero__content">
|
||||
<ChannelBadge
|
||||
channel={manifest.channel}
|
||||
label={badgeLabels[manifest.channel]}
|
||||
version={manifest.version ?? undefined}
|
||||
date={publishedDate || undefined}
|
||||
/>
|
||||
|
||||
<h1 class="hero__headline">{copy.hero.headline}</h1>
|
||||
<p class="hero__subheadline">{copy.hero.subheadline}</p>
|
||||
|
||||
<div class="hero__actions">
|
||||
<CtaButton
|
||||
label={primaryLabel}
|
||||
href={primaryHref}
|
||||
variant="primary"
|
||||
/>
|
||||
{showSecondary && (
|
||||
<CtaButton
|
||||
label={copy.hero.ctaSecondary}
|
||||
href={REPO_URL}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{manifest.channel === 'prerelease' && (
|
||||
<p class="hero__warning">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M8 1L15 14H1L8 1Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M8 6V9M8 11.5V12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{copy.hero.prereleaseWarning}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{manifest.version && (
|
||||
<p class="hero__version">
|
||||
{copy.hero.versionLabel
|
||||
.replace('{version}', manifest.version)
|
||||
.replace('{date}', publishedDate)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="hero__visual">
|
||||
<div class="hero__frame">
|
||||
<img
|
||||
src="/images/screenshots/atlas-overview.png"
|
||||
alt={locale === 'zh' ? 'Atlas for Mac 概览界面' : 'Atlas for Mac overview interface'}
|
||||
width="800"
|
||||
height="560"
|
||||
loading="eager"
|
||||
class="hero__screenshot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding-top: calc(80px + var(--atlas-space-section-gap));
|
||||
padding-bottom: var(--atlas-space-section-gap);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero__inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--atlas-space-section-gap);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.hero__inner {
|
||||
grid-template-columns: 1fr 1.1fr;
|
||||
gap: var(--atlas-space-section);
|
||||
}
|
||||
}
|
||||
|
||||
.hero__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.hero__headline {
|
||||
font-size: var(--atlas-text-hero);
|
||||
font-weight: var(--atlas-text-hero-weight);
|
||||
line-height: var(--atlas-leading-tight);
|
||||
letter-spacing: var(--atlas-tracking-tight);
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.hero__subheadline {
|
||||
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-relaxed);
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--atlas-space-md);
|
||||
margin-top: var(--atlas-space-sm);
|
||||
}
|
||||
|
||||
.hero__warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-sm);
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-warning);
|
||||
padding: var(--atlas-space-sm) var(--atlas-space-md);
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||
border-radius: var(--atlas-radius-md);
|
||||
max-width: max-content;
|
||||
}
|
||||
|
||||
.hero__version {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-caption-small);
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
}
|
||||
|
||||
.hero__visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero__frame {
|
||||
position: relative;
|
||||
border-radius: var(--atlas-radius-xxl);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--atlas-shadow-prominent);
|
||||
border: 1px solid var(--atlas-color-border-emphasis);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hero__screenshot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
95
Apps/LandingSite/src/components/HowItWorks.astro
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="how section band--dark fade-in" id="how">
|
||||
<div class="how__inner container">
|
||||
<h2 class="how__title">{copy.howItWorks.sectionTitle}</h2>
|
||||
|
||||
<div class="how__steps">
|
||||
{copy.howItWorks.steps.map((step, i) => (
|
||||
<div class="how__step">
|
||||
<div class="how__step-number" aria-hidden="true">{String(i + 1).padStart(2, '0')}</div>
|
||||
<h3 class="how__step-label">{step.label}</h3>
|
||||
<p class="how__step-desc">{step.description}</p>
|
||||
{i < copy.howItWorks.steps.length - 1 && (
|
||||
<div class="how__connector" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.how__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.how__steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--atlas-space-xxl);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.how__steps {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.how__step {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.how__step-number {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: 700;
|
||||
color: var(--atlas-color-brand);
|
||||
opacity: 0.3;
|
||||
margin-bottom: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.how__step-label {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: var(--atlas-text-card-title);
|
||||
font-weight: var(--atlas-text-card-title-weight);
|
||||
color: var(--atlas-color-text-primary);
|
||||
margin-bottom: var(--atlas-space-sm);
|
||||
}
|
||||
|
||||
.how__step-desc {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.how__connector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.how__connector {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: -12px;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: var(--atlas-color-border-emphasis);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
210
Apps/LandingSite/src/components/NavBar.astro
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
import CtaButton from './CtaButton.astro';
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
import { getRelease, getDownloadUrl } from '../data/release';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
const manifest = getRelease();
|
||||
const downloadUrl = getDownloadUrl(manifest);
|
||||
const altLocale = locale === 'zh' ? 'en' : 'zh';
|
||||
const altPath = locale === 'zh' ? '/en/' : '/zh/';
|
||||
const altLabel = locale === 'zh' ? 'EN' : '中文';
|
||||
|
||||
const navItems = [
|
||||
{ label: copy.nav.whyAtlas, href: '#why' },
|
||||
{ label: copy.nav.howItWorks, href: '#how' },
|
||||
{ label: copy.nav.developers, href: '#developers' },
|
||||
{ label: copy.nav.safety, href: '#safety' },
|
||||
{ label: copy.nav.faq, href: '#faq' },
|
||||
];
|
||||
---
|
||||
|
||||
<header class="navbar" id="navbar">
|
||||
<nav class="navbar__inner container" aria-label="Main navigation">
|
||||
<a href={`/${locale}/`} class="navbar__brand" aria-label="Atlas for Mac">
|
||||
<img src="/images/atlas-icon.png" alt="" width="32" height="32" class="navbar__logo" />
|
||||
<span class="navbar__wordmark">Atlas</span>
|
||||
</a>
|
||||
|
||||
<ul class="navbar__links" id="nav-links">
|
||||
{navItems.map((item) => (
|
||||
<li><a href={item.href} class="navbar__link">{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div class="navbar__actions">
|
||||
<a href={altPath} class="navbar__lang" aria-label={`Switch to ${altLabel}`}>
|
||||
{altLabel}
|
||||
</a>
|
||||
<CtaButton
|
||||
label={copy.nav.download}
|
||||
href={downloadUrl}
|
||||
variant="primary"
|
||||
class="navbar__cta"
|
||||
/>
|
||||
<button class="navbar__menu-btn" id="menu-btn" aria-label="Menu" aria-expanded="false">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById('menu-btn');
|
||||
const links = document.getElementById('nav-links');
|
||||
if (btn && links) {
|
||||
btn.addEventListener('click', () => {
|
||||
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!expanded));
|
||||
links.classList.toggle('is-open');
|
||||
});
|
||||
}
|
||||
|
||||
// Sticky background on scroll
|
||||
let ticking = false;
|
||||
const navbar = document.getElementById('navbar');
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
if (navbar) {
|
||||
navbar.classList.toggle('is-scrolled', window.scrollY > 40);
|
||||
}
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding-block: var(--atlas-space-md);
|
||||
transition: background-color var(--atlas-motion-standard),
|
||||
backdrop-filter var(--atlas-motion-standard);
|
||||
}
|
||||
|
||||
.navbar.is-scrolled {
|
||||
background-color: rgba(13, 15, 17, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--atlas-color-border);
|
||||
}
|
||||
|
||||
.navbar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-xxl);
|
||||
}
|
||||
|
||||
.navbar__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-sm);
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--atlas-radius-sm);
|
||||
}
|
||||
|
||||
.navbar__wordmark {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.navbar__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-xxl);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--atlas-motion-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar__link:hover {
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.navbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--atlas-space-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar__lang {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-caption);
|
||||
font-weight: 500;
|
||||
color: var(--atlas-color-text-secondary);
|
||||
text-decoration: none;
|
||||
padding: var(--atlas-space-xxs) var(--atlas-space-sm);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-sm);
|
||||
transition: color var(--atlas-motion-fast),
|
||||
border-color var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.navbar__lang:hover {
|
||||
color: var(--atlas-color-text-primary);
|
||||
border-color: var(--atlas-color-border-emphasis);
|
||||
}
|
||||
|
||||
.navbar__menu-btn {
|
||||
display: none;
|
||||
color: var(--atlas-color-text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.navbar__links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
padding: var(--atlas-space-xl);
|
||||
background-color: rgba(13, 15, 17, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--atlas-color-border);
|
||||
gap: var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.navbar__links.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar__menu-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(.navbar__cta) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
Apps/LandingSite/src/components/OpenSourceSection.astro
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
|
||||
const REPO_URL = 'https://github.com/nicekid1/CleanMyPc';
|
||||
---
|
||||
|
||||
<section class="oss section band--dark fade-in" id="opensource">
|
||||
<div class="oss__inner container">
|
||||
<h2 class="oss__title">{copy.openSource.sectionTitle}</h2>
|
||||
|
||||
<div class="oss__grid">
|
||||
<a href={REPO_URL} class="oss__card" target="_blank" rel="noopener noreferrer">
|
||||
<span class="oss__card-label">{copy.openSource.repoLabel}</span>
|
||||
<span class="oss__card-value">GitHub</span>
|
||||
<svg class="oss__card-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4 12L12 4M12 4H6M12 4V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div class="oss__card">
|
||||
<span class="oss__card-label">{copy.openSource.licenseLabel}</span>
|
||||
<span class="oss__card-value">MIT</span>
|
||||
</div>
|
||||
|
||||
<a href={`${REPO_URL}/blob/main/Docs/ATTRIBUTION.md`} class="oss__card" target="_blank" rel="noopener noreferrer">
|
||||
<span class="oss__card-label">{copy.openSource.attributionLabel}</span>
|
||||
<span class="oss__card-value">ATTRIBUTION.md</span>
|
||||
<svg class="oss__card-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4 12L12 4M12 4H6M12 4V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a href={`${REPO_URL}/releases`} class="oss__card" target="_blank" rel="noopener noreferrer">
|
||||
<span class="oss__card-label">{copy.openSource.changelogLabel}</span>
|
||||
<span class="oss__card-value">Releases</span>
|
||||
<svg class="oss__card-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4 12L12 4M12 4H6M12 4V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.oss__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.oss__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--atlas-space-lg);
|
||||
max-width: 800px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.oss__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-xxs);
|
||||
padding: var(--atlas-space-xl);
|
||||
background: var(--atlas-color-bg-surface);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-lg);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: border-color var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
a.oss__card:hover {
|
||||
border-color: var(--atlas-color-border-emphasis);
|
||||
}
|
||||
|
||||
.oss__card-label {
|
||||
font-size: var(--atlas-text-caption);
|
||||
font-weight: var(--atlas-text-caption-weight);
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--atlas-tracking-wide);
|
||||
}
|
||||
|
||||
.oss__card-value {
|
||||
font-family: var(--atlas-font-mono);
|
||||
font-size: var(--atlas-text-body);
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
|
||||
.oss__card-arrow {
|
||||
position: absolute;
|
||||
top: var(--atlas-space-lg);
|
||||
right: var(--atlas-space-lg);
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
75
Apps/LandingSite/src/components/ProblemOutcome.astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="problem section band--dark fade-in" id="why">
|
||||
<div class="problem__inner container">
|
||||
<h2 class="problem__title">{copy.problem.sectionTitle}</h2>
|
||||
|
||||
<div class="problem__grid">
|
||||
{copy.problem.scenarios.map((scenario) => (
|
||||
<div class="problem__card">
|
||||
<p class="problem__before">{scenario.before}</p>
|
||||
<div class="problem__arrow" aria-hidden="true">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 5V19M12 19L5 12M12 19L19 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="problem__after">{scenario.after}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.problem__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.problem__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--atlas-space-xxl);
|
||||
}
|
||||
|
||||
.problem__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--atlas-space-lg);
|
||||
padding: var(--atlas-space-xxl);
|
||||
background: var(--atlas-color-bg-surface);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-xl);
|
||||
}
|
||||
|
||||
.problem__before {
|
||||
font-size: var(--atlas-text-body);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.problem__arrow {
|
||||
color: var(--atlas-color-brand);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.problem__after {
|
||||
font-size: var(--atlas-text-body);
|
||||
font-weight: 500;
|
||||
color: var(--atlas-color-accent);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
</style>
|
||||
141
Apps/LandingSite/src/components/SafetySection.astro
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
import { getRelease } from '../data/release';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
const manifest = getRelease();
|
||||
---
|
||||
|
||||
<section class="safety section band--dark fade-in" id="safety">
|
||||
<div class="safety__inner container">
|
||||
<div class="safety__header">
|
||||
<h2 class="safety__title">{copy.safety.sectionTitle}</h2>
|
||||
<p class="safety__subtitle">{copy.safety.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="safety__grid">
|
||||
{copy.safety.points.map((point) => (
|
||||
<div class="safety__card">
|
||||
<h3 class="safety__card-title">{point.title}</h3>
|
||||
<p class="safety__card-desc">{point.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{manifest.gatekeeperWarning && (
|
||||
<div class="safety__gatekeeper">
|
||||
<h3 class="safety__gk-title">{copy.safety.gatekeeperGuide.title}</h3>
|
||||
<ol class="safety__gk-steps">
|
||||
{copy.safety.gatekeeperGuide.steps.map((step) => (
|
||||
<li class="safety__gk-step">{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
<div class="safety__gk-visual">
|
||||
<img
|
||||
src="/images/screenshots/atlas-prerelease-warning.png"
|
||||
alt={locale === 'zh' ? 'macOS Gatekeeper 预发布警告示意' : 'macOS Gatekeeper prerelease warning'}
|
||||
width="400"
|
||||
height="280"
|
||||
loading="lazy"
|
||||
class="safety__gk-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.safety__header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.safety__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
margin-bottom: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.safety__subtitle {
|
||||
font-size: clamp(1rem, 2vw, 1.125rem);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.safety__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--atlas-space-xxl);
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.safety__card {
|
||||
padding: var(--atlas-space-xl);
|
||||
background: var(--atlas-color-bg-surface);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-xl);
|
||||
}
|
||||
|
||||
.safety__card-title {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: var(--atlas-text-card-title);
|
||||
font-weight: var(--atlas-text-card-title-weight);
|
||||
color: var(--atlas-color-text-primary);
|
||||
margin-bottom: var(--atlas-space-sm);
|
||||
}
|
||||
|
||||
.safety__card-desc {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.safety__gatekeeper {
|
||||
padding: var(--atlas-space-xxl);
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
border: 1px solid rgba(245, 158, 11, 0.15);
|
||||
border-radius: var(--atlas-radius-xl);
|
||||
}
|
||||
|
||||
.safety__gk-title {
|
||||
font-family: var(--atlas-font-display);
|
||||
font-size: var(--atlas-text-card-title);
|
||||
font-weight: var(--atlas-text-card-title-weight);
|
||||
color: var(--atlas-color-warning);
|
||||
margin-bottom: var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.safety__gk-steps {
|
||||
list-style: decimal;
|
||||
list-style-position: inside;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-sm);
|
||||
margin-bottom: var(--atlas-space-xl);
|
||||
}
|
||||
|
||||
.safety__gk-step {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
.safety__gk-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.safety__gk-img {
|
||||
border-radius: var(--atlas-radius-md);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
98
Apps/LandingSite/src/components/ScreenshotGallery.astro
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<section class="gallery section band--surface fade-in" id="screenshots">
|
||||
<div class="gallery__inner container">
|
||||
<h2 class="gallery__title">{copy.screenshots.sectionTitle}</h2>
|
||||
|
||||
<div class="gallery__grid" id="screenshot-gallery">
|
||||
{copy.screenshots.items.map((item, i) => (
|
||||
<figure class="gallery__item" data-index={i}>
|
||||
<div class="gallery__frame">
|
||||
<img
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
width="800"
|
||||
height="560"
|
||||
loading="lazy"
|
||||
class="gallery__img"
|
||||
/>
|
||||
</div>
|
||||
<figcaption class="gallery__caption">{item.caption}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.gallery__title {
|
||||
font-size: var(--atlas-text-section);
|
||||
font-weight: var(--atlas-text-section-weight);
|
||||
text-align: center;
|
||||
margin-bottom: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.gallery__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: var(--atlas-space-xxl);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.gallery__grid {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
gap: var(--atlas-space-lg);
|
||||
padding-bottom: var(--atlas-space-lg);
|
||||
}
|
||||
|
||||
.gallery__item {
|
||||
flex: 0 0 85vw;
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.gallery__frame {
|
||||
border-radius: var(--atlas-radius-xl);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
box-shadow: var(--atlas-shadow-raised);
|
||||
transition: transform var(--atlas-motion-fast),
|
||||
box-shadow var(--atlas-motion-fast);
|
||||
}
|
||||
|
||||
.gallery__frame:hover {
|
||||
transform: scale(1.005);
|
||||
box-shadow: var(--atlas-shadow-prominent);
|
||||
}
|
||||
|
||||
.gallery__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.gallery__caption {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
color: var(--atlas-color-text-secondary);
|
||||
text-align: center;
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
</style>
|
||||
51
Apps/LandingSite/src/components/TrustStrip.astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
import { t, type Locale } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const copy = t(locale);
|
||||
|
||||
const items = [
|
||||
copy.trustStrip.openSource,
|
||||
copy.trustStrip.recoveryFirst,
|
||||
copy.trustStrip.developerAware,
|
||||
copy.trustStrip.macNative,
|
||||
copy.trustStrip.directDownload,
|
||||
];
|
||||
---
|
||||
|
||||
<section class="trust-strip band--surface" aria-label="Trust signals">
|
||||
<div class="trust-strip__inner container">
|
||||
{items.map((item) => (
|
||||
<span class="trust-strip__pill">{item}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.trust-strip {
|
||||
padding-block: var(--atlas-space-xxl);
|
||||
border-top: 1px solid var(--atlas-color-border);
|
||||
border-bottom: 1px solid var(--atlas-color-border);
|
||||
}
|
||||
|
||||
.trust-strip__inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--atlas-space-md);
|
||||
}
|
||||
|
||||
.trust-strip__pill {
|
||||
font-size: var(--atlas-text-body-small);
|
||||
font-weight: 500;
|
||||
color: var(--atlas-color-text-secondary);
|
||||
padding: var(--atlas-space-xs) var(--atlas-space-lg);
|
||||
border: 1px solid var(--atlas-color-border);
|
||||
border-radius: var(--atlas-radius-full);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
16
Apps/LandingSite/src/data/release-fallback.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"channel": "prerelease",
|
||||
"version": "1.0.2",
|
||||
"publishedAt": null,
|
||||
"releaseUrl": "https://github.com/nicekid1/CleanMyPc/releases",
|
||||
"assets": {
|
||||
"dmg": null,
|
||||
"zip": null,
|
||||
"pkg": null,
|
||||
"sha256": null
|
||||
},
|
||||
"gatekeeperWarning": true,
|
||||
"installNote": null,
|
||||
"tagName": null,
|
||||
"generatedAt": "static-fallback"
|
||||
}
|
||||
70
Apps/LandingSite/src/data/release.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Release manifest loader.
|
||||
* Tries build-time generated manifest first, then falls back to static fallback.
|
||||
*/
|
||||
|
||||
export interface ReleaseManifest {
|
||||
channel: 'stable' | 'prerelease' | 'none';
|
||||
version: string | null;
|
||||
publishedAt: string | null;
|
||||
releaseUrl: string | null;
|
||||
assets: {
|
||||
dmg: string | null;
|
||||
zip: string | null;
|
||||
pkg: string | null;
|
||||
sha256: string | null;
|
||||
};
|
||||
gatekeeperWarning: boolean;
|
||||
installNote: string | null;
|
||||
tagName: string | null;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
let cached: ReleaseManifest | null = null;
|
||||
|
||||
export function getRelease(): ReleaseManifest {
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
// Try build-time generated manifest first
|
||||
const manifest = import.meta.glob('./release-manifest.json', { eager: true });
|
||||
const key = Object.keys(manifest)[0];
|
||||
if (key) {
|
||||
cached = (manifest[key] as any).default as ReleaseManifest;
|
||||
return cached;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fallback
|
||||
}
|
||||
|
||||
// Use static fallback
|
||||
const fallback = import.meta.glob('./release-fallback.json', { eager: true });
|
||||
const key = Object.keys(fallback)[0];
|
||||
cached = (fallback[key] as any).default as ReleaseManifest;
|
||||
return cached;
|
||||
}
|
||||
|
||||
export function getDownloadUrl(manifest: ReleaseManifest): string {
|
||||
// Prefer DMG > ZIP > PKG > release page
|
||||
return (
|
||||
manifest.assets.dmg ??
|
||||
manifest.assets.zip ??
|
||||
manifest.assets.pkg ??
|
||||
manifest.releaseUrl ??
|
||||
'https://github.com/nicekid1/CleanMyPc/releases'
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDate(iso: string | null, locale: string): string {
|
||||
if (!iso) return '';
|
||||
try {
|
||||
const date = new Date(iso);
|
||||
return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
236
Apps/LandingSite/src/i18n/en.json
Normal file
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Atlas for Mac — Explainable, Recovery-First Mac Maintenance",
|
||||
"description": "Atlas is an open-source macOS maintenance tool. Preview before you scan, restore after you clean. Built for developers and power users.",
|
||||
"ogImage": "/images/og-atlas-en.png"
|
||||
},
|
||||
"nav": {
|
||||
"whyAtlas": "Why Atlas",
|
||||
"howItWorks": "How It Works",
|
||||
"developers": "Developers",
|
||||
"safety": "Safety",
|
||||
"faq": "FAQ",
|
||||
"download": "Download"
|
||||
},
|
||||
"hero": {
|
||||
"headline": "Mac maintenance you can explain and reverse",
|
||||
"subheadline": "Atlas is an open-source macOS maintenance workspace. Every scan result is explained, every clean operation is recoverable. Nothing is silently deleted.",
|
||||
"ctaPrimary": "Download Atlas",
|
||||
"ctaSecondary": "View Source",
|
||||
"badgeStable": "Stable",
|
||||
"badgePrerelease": "Prerelease",
|
||||
"badgeComingSoon": "Coming Soon",
|
||||
"prereleaseWarning": "Prerelease builds are for testing only and may contain unresolved issues. Do not use in production.",
|
||||
"gatekeeperNote": "On first launch, right-click and select \"Open\" to pass macOS Gatekeeper verification.",
|
||||
"versionLabel": "Version {version} · {date}"
|
||||
},
|
||||
"trustStrip": {
|
||||
"openSource": "Fully Open Source",
|
||||
"recoveryFirst": "Recovery First",
|
||||
"developerAware": "Developer Aware",
|
||||
"macNative": "Native macOS",
|
||||
"directDownload": "Direct Download"
|
||||
},
|
||||
"problem": {
|
||||
"sectionTitle": "Why Atlas",
|
||||
"scenarios": [
|
||||
{
|
||||
"before": "Cleanup tools delete files without telling you what was removed",
|
||||
"after": "Atlas lists every scan result for your review before executing"
|
||||
},
|
||||
{
|
||||
"before": "Accidentally deleted a config file with no way to recover",
|
||||
"after": "Atlas supports per-item restore within the retention window, when supported"
|
||||
},
|
||||
{
|
||||
"before": "Tools demand Full Disk Access without explaining why",
|
||||
"after": "Atlas requests permissions on demand and explains each one"
|
||||
}
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"sectionTitle": "Core Features",
|
||||
"cards": [
|
||||
{
|
||||
"title": "Overview",
|
||||
"value": "System health at a glance",
|
||||
"example": "Disk usage, cache breakdown, reclaimable space — all on one screen",
|
||||
"trustCue": "Read-only scan, no modifications made"
|
||||
},
|
||||
{
|
||||
"title": "Smart Clean",
|
||||
"value": "Scan, review the plan, then execute",
|
||||
"example": "System cache 3.2 GB — inspect details before deciding to clean",
|
||||
"trustCue": "Auto-backup before every clean, supports rollback"
|
||||
},
|
||||
{
|
||||
"title": "Apps",
|
||||
"value": "Analyze full app footprint and plan uninstalls",
|
||||
"example": "See every associated file an app leaves on disk",
|
||||
"trustCue": "Full file list shown before uninstall, confirm each item"
|
||||
},
|
||||
{
|
||||
"title": "History",
|
||||
"value": "Complete audit trail and recovery timeline",
|
||||
"example": "Review detailed records of every clean operation in the past 30 days",
|
||||
"trustCue": "Every action is traceable, no silent operations"
|
||||
},
|
||||
{
|
||||
"title": "Recovery",
|
||||
"value": "Per-item restore within the retention window, when supported",
|
||||
"example": "Accidentally removed config files can be restored within the retention period",
|
||||
"trustCue": "File-level backup, not snapshot-based"
|
||||
},
|
||||
{
|
||||
"title": "Permissions",
|
||||
"value": "Least privilege, explained before asking",
|
||||
"example": "Downloads folder access is requested only when scanning that directory",
|
||||
"trustCue": "Every permission comes with a usage explanation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"howItWorks": {
|
||||
"sectionTitle": "How It Works",
|
||||
"steps": [
|
||||
{
|
||||
"label": "Scan",
|
||||
"description": "Atlas performs a read-only scan of your disk, identifying caches, logs, leftover files, and other reclaimable items."
|
||||
},
|
||||
{
|
||||
"label": "Review",
|
||||
"description": "Scan results are listed item by item with explanations and size estimates. You decide what to keep and what to clean."
|
||||
},
|
||||
{
|
||||
"label": "Execute",
|
||||
"description": "After confirmation, Atlas performs the cleanup. Removed files are automatically backed up within the retention period, when supported."
|
||||
},
|
||||
{
|
||||
"label": "Restore",
|
||||
"description": "Within the retention window, you can restore any cleaned file from the operation history on a per-item basis."
|
||||
}
|
||||
]
|
||||
},
|
||||
"developer": {
|
||||
"sectionTitle": "Developer Friendly",
|
||||
"subtitle": "Atlas recognizes development toolchains and avoids removing critical files.",
|
||||
"items": [
|
||||
{
|
||||
"title": "Toolchain Awareness",
|
||||
"description": "Automatically identifies caches and artifacts from Homebrew, Xcode, Node.js, and other development environments, marking them as safe or cautious."
|
||||
},
|
||||
{
|
||||
"title": "Project Directory Protection",
|
||||
"description": "Detects .git, node_modules, build directories, and other project folders. These are excluded from scans by default to avoid disrupting your workflow."
|
||||
},
|
||||
{
|
||||
"title": "CLI Integration",
|
||||
"description": "Provides command-line tools for scripted scanning and cleaning, ready to integrate into automated workflows."
|
||||
},
|
||||
{
|
||||
"title": "Transparent Operation Logs",
|
||||
"description": "Every operation produces a complete audit log with file paths, sizes, and timestamps for easy troubleshooting."
|
||||
}
|
||||
]
|
||||
},
|
||||
"safety": {
|
||||
"sectionTitle": "Safety",
|
||||
"subtitle": "Atlas is built on one principle: inform before acting, restore after acting.",
|
||||
"points": [
|
||||
{
|
||||
"title": "Preview Before Action",
|
||||
"description": "A complete file list is shown before every clean operation. There is no black-box \"one-click clean\" behavior."
|
||||
},
|
||||
{
|
||||
"title": "File-Level Backup",
|
||||
"description": "Cleaned files are automatically backed up during the retention period. Per-item restore is available from the operation history, when supported."
|
||||
},
|
||||
{
|
||||
"title": "Least Privilege",
|
||||
"description": "Atlas only requests specific permissions when needed and explains each one. Core features work without Full Disk Access."
|
||||
}
|
||||
],
|
||||
"gatekeeperGuide": {
|
||||
"title": "Opening Atlas for the First Time",
|
||||
"steps": [
|
||||
"Download the Atlas .dmg file from the official website",
|
||||
"Drag Atlas into your Applications folder",
|
||||
"Right-click the Atlas icon and select \"Open\"",
|
||||
"Click \"Open\" in the confirmation dialog"
|
||||
]
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"sectionTitle": "Interface Preview",
|
||||
"items": [
|
||||
{
|
||||
"src": "/images/screenshots/atlas-overview.png",
|
||||
"alt": "Atlas Overview interface",
|
||||
"caption": "Overview — Disk health, cache breakdown, reclaimable space"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-smart-clean.png",
|
||||
"alt": "Atlas Smart Clean interface",
|
||||
"caption": "Smart Clean — Scan results listed item by item, execute after review"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-apps.png",
|
||||
"alt": "Atlas Apps interface",
|
||||
"caption": "Apps — View full app footprint and associated files"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-history.png",
|
||||
"alt": "Atlas History interface",
|
||||
"caption": "History — Complete audit trail and restore entry point"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-settings.png",
|
||||
"alt": "Atlas Settings interface",
|
||||
"caption": "Settings — Permission management and retention configuration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"openSource": {
|
||||
"sectionTitle": "Open Source",
|
||||
"repoLabel": "GitHub Repository",
|
||||
"licenseLabel": "License",
|
||||
"attributionLabel": "Third-Party Attribution",
|
||||
"changelogLabel": "Changelog"
|
||||
},
|
||||
"faq": {
|
||||
"sectionTitle": "FAQ",
|
||||
"items": [
|
||||
{
|
||||
"question": "Is Atlas signed and notarized?",
|
||||
"answer": "Atlas is signed with an Apple Developer ID and verified through Apple's notarization service. On first launch, macOS Gatekeeper automatically checks its integrity. Prerelease builds may not be notarized and require manual confirmation to open."
|
||||
},
|
||||
{
|
||||
"question": "What happens with prerelease installs?",
|
||||
"answer": "Prerelease builds may include incomplete features or known issues and are intended for testing only. Since they may not be notarized by Apple, you will need to right-click and select \"Open\" on first launch. We recommend not using them as your primary tool on important machines."
|
||||
},
|
||||
{
|
||||
"question": "Does Atlas upload my files?",
|
||||
"answer": "No. Atlas runs entirely on your local machine. It does not collect or upload any user files or personal data. Scan results and operation records are stored locally. The source code is fully open for your review."
|
||||
},
|
||||
{
|
||||
"question": "What does recovery actually mean?",
|
||||
"answer": "Before cleaning, Atlas backs up files to a local retention directory. Within the retention period (30 days by default), you can restore cleaned files individually through the operation history. After the retention period expires, backups are automatically purged. This is file-level backup, not a system snapshot — some scenarios may not be covered."
|
||||
},
|
||||
{
|
||||
"question": "Does Atlas require Full Disk Access?",
|
||||
"answer": "Core features work without Full Disk Access. Atlas follows the principle of least privilege and only requests access to specific protected directories when needed, explaining the purpose before each request."
|
||||
},
|
||||
{
|
||||
"question": "Is this a Mac App Store app?",
|
||||
"answer": "Not currently. Atlas is distributed as a direct download from the official website to retain full system maintenance capabilities. The Mac App Store sandbox restrictions would limit core cleaning and recovery features. A simplified App Store version may be considered in the future."
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"download": "Download Atlas",
|
||||
"github": "GitHub",
|
||||
"documentation": "Documentation",
|
||||
"privacy": "Privacy Policy",
|
||||
"security": "Security",
|
||||
"copyright": "Atlas for Mac. Open-source project released under the MIT License."
|
||||
}
|
||||
}
|
||||
145
Apps/LandingSite/src/i18n/utils.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import zh from "./zh.json";
|
||||
import en from "./en.json";
|
||||
|
||||
// ─── Locale types ────────────────────────────────────────────────
|
||||
|
||||
export type Locale = "en" | "zh";
|
||||
|
||||
export const defaultLocale: Locale = "zh";
|
||||
|
||||
export const locales: Locale[] = ["zh", "en"];
|
||||
|
||||
// ─── LandingCopy schema ─────────────────────────────────────────
|
||||
|
||||
export interface LandingCopy {
|
||||
meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
ogImage: string;
|
||||
};
|
||||
nav: {
|
||||
whyAtlas: string;
|
||||
howItWorks: string;
|
||||
developers: string;
|
||||
safety: string;
|
||||
faq: string;
|
||||
download: string;
|
||||
};
|
||||
hero: {
|
||||
headline: string;
|
||||
subheadline: string;
|
||||
ctaPrimary: string;
|
||||
ctaSecondary: string;
|
||||
badgeStable: string;
|
||||
badgePrerelease: string;
|
||||
badgeComingSoon: string;
|
||||
prereleaseWarning: string;
|
||||
gatekeeperNote: string;
|
||||
versionLabel: string;
|
||||
};
|
||||
trustStrip: {
|
||||
openSource: string;
|
||||
recoveryFirst: string;
|
||||
developerAware: string;
|
||||
macNative: string;
|
||||
directDownload: string;
|
||||
};
|
||||
problem: {
|
||||
sectionTitle: string;
|
||||
scenarios: Array<{ before: string; after: string }>;
|
||||
};
|
||||
features: {
|
||||
sectionTitle: string;
|
||||
cards: Array<{
|
||||
title: string;
|
||||
value: string;
|
||||
example: string;
|
||||
trustCue: string;
|
||||
}>;
|
||||
};
|
||||
howItWorks: {
|
||||
sectionTitle: string;
|
||||
steps: Array<{ label: string; description: string }>;
|
||||
};
|
||||
developer: {
|
||||
sectionTitle: string;
|
||||
subtitle: string;
|
||||
items: Array<{ title: string; description: string }>;
|
||||
};
|
||||
safety: {
|
||||
sectionTitle: string;
|
||||
subtitle: string;
|
||||
points: Array<{ title: string; description: string }>;
|
||||
gatekeeperGuide: {
|
||||
title: string;
|
||||
steps: string[];
|
||||
};
|
||||
};
|
||||
screenshots: {
|
||||
sectionTitle: string;
|
||||
items: Array<{ src: string; alt: string; caption: string }>;
|
||||
};
|
||||
openSource: {
|
||||
sectionTitle: string;
|
||||
repoLabel: string;
|
||||
licenseLabel: string;
|
||||
attributionLabel: string;
|
||||
changelogLabel: string;
|
||||
};
|
||||
faq: {
|
||||
sectionTitle: string;
|
||||
items: Array<{ question: string; answer: string }>;
|
||||
};
|
||||
footer: {
|
||||
download: string;
|
||||
github: string;
|
||||
documentation: string;
|
||||
privacy: string;
|
||||
security: string;
|
||||
copyright: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Translation map ────────────────────────────────────────────
|
||||
|
||||
const translations: Record<Locale, LandingCopy> = {
|
||||
zh: zh as LandingCopy,
|
||||
en: en as LandingCopy,
|
||||
};
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the full translation object for the given locale.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const copy = t('zh');
|
||||
* console.log(copy.hero.headline);
|
||||
* ```
|
||||
*/
|
||||
export function t(locale: Locale): LandingCopy {
|
||||
return translations[locale] ?? translations[defaultLocale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the locale from a URL pathname.
|
||||
* Expects paths like `/en/...` or `/zh/...`.
|
||||
* Falls back to `defaultLocale` when the prefix is not a known locale.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getLocaleFromUrl(new URL('https://atlas.atomstorm.ai/en/'));
|
||||
* // => 'en'
|
||||
*
|
||||
* getLocaleFromUrl(new URL('https://atlas.atomstorm.ai/'));
|
||||
* // => 'zh'
|
||||
* ```
|
||||
*/
|
||||
export function getLocaleFromUrl(url: URL): Locale {
|
||||
const [, segment] = url.pathname.split("/");
|
||||
if (segment && locales.includes(segment as Locale)) {
|
||||
return segment as Locale;
|
||||
}
|
||||
return defaultLocale;
|
||||
}
|
||||
236
Apps/LandingSite/src/i18n/zh.json
Normal file
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Atlas for Mac — 可解释、可恢复的 Mac 维护工作台",
|
||||
"description": "Atlas 是一款开源 macOS 维护工具,扫描前可预览,操作后可恢复。面向开发者和进阶用户设计。",
|
||||
"ogImage": "/images/og-atlas-zh.png"
|
||||
},
|
||||
"nav": {
|
||||
"whyAtlas": "为什么选 Atlas",
|
||||
"howItWorks": "工作流程",
|
||||
"developers": "开发者友好",
|
||||
"safety": "安全机制",
|
||||
"faq": "常见问题",
|
||||
"download": "下载"
|
||||
},
|
||||
"hero": {
|
||||
"headline": "Mac 维护,每一步都可解释、可恢复",
|
||||
"subheadline": "Atlas 是开源的 macOS 维护工作台。扫描结果逐项说明,清理操作支持回滚,不静默删除任何文件。",
|
||||
"ctaPrimary": "下载 Atlas",
|
||||
"ctaSecondary": "查看源码",
|
||||
"badgeStable": "稳定版",
|
||||
"badgePrerelease": "预发布版",
|
||||
"badgeComingSoon": "即将推出",
|
||||
"prereleaseWarning": "预发布版本仅供测试,可能存在未知问题。请勿在生产环境使用。",
|
||||
"gatekeeperNote": "首次打开需右键选择「打开」以通过 macOS Gatekeeper 验证。",
|
||||
"versionLabel": "版本 {version} · {date}"
|
||||
},
|
||||
"trustStrip": {
|
||||
"openSource": "完整开源",
|
||||
"recoveryFirst": "恢复优先",
|
||||
"developerAware": "开发者友好",
|
||||
"macNative": "原生 macOS",
|
||||
"directDownload": "直接下载"
|
||||
},
|
||||
"problem": {
|
||||
"sectionTitle": "为什么选 Atlas",
|
||||
"scenarios": [
|
||||
{
|
||||
"before": "清理工具删了文件,不告诉你删的是什么",
|
||||
"after": "Atlas 扫描后逐项列出结果,你确认后才执行"
|
||||
},
|
||||
{
|
||||
"before": "误删了配置文件,无法恢复",
|
||||
"after": "Atlas 在保留期内支持逐项恢复(如适用)"
|
||||
},
|
||||
{
|
||||
"before": "工具要求完全磁盘访问权限,却不解释原因",
|
||||
"after": "Atlas 按需申请权限,每次都说明用途"
|
||||
}
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"sectionTitle": "核心功能",
|
||||
"cards": [
|
||||
{
|
||||
"title": "系统概览",
|
||||
"value": "一屏掌握 Mac 健康状态",
|
||||
"example": "磁盘用量、缓存占比、可回收空间,一目了然",
|
||||
"trustCue": "只读扫描,不做任何修改"
|
||||
},
|
||||
{
|
||||
"title": "智能清理",
|
||||
"value": "扫描、审查计划、确认后执行",
|
||||
"example": "系统缓存 3.2 GB — 查看明细后决定是否清理",
|
||||
"trustCue": "每次清理前自动备份,支持回滚"
|
||||
},
|
||||
{
|
||||
"title": "应用管理",
|
||||
"value": "分析应用完整足迹并规划卸载",
|
||||
"example": "查看某应用在磁盘上遗留的所有关联文件",
|
||||
"trustCue": "卸载前列出全部待删文件,逐项确认"
|
||||
},
|
||||
{
|
||||
"title": "操作历史",
|
||||
"value": "完整审计轨迹与恢复时间线",
|
||||
"example": "查看过去 30 天内每次清理操作的详细记录",
|
||||
"trustCue": "所有操作均可追溯,不存在静默行为"
|
||||
},
|
||||
{
|
||||
"title": "文件恢复",
|
||||
"value": "保留期内可逐项还原(如适用)",
|
||||
"example": "误删的配置文件可在保留窗口内恢复",
|
||||
"trustCue": "基于文件级备份,非快照机制"
|
||||
},
|
||||
{
|
||||
"title": "权限管理",
|
||||
"value": "最小权限原则,用前先说明",
|
||||
"example": "仅在扫描下载目录时才请求对应权限",
|
||||
"trustCue": "每项权限均附带用途说明"
|
||||
}
|
||||
]
|
||||
},
|
||||
"howItWorks": {
|
||||
"sectionTitle": "工作流程",
|
||||
"steps": [
|
||||
{
|
||||
"label": "扫描",
|
||||
"description": "Atlas 只读扫描磁盘,识别缓存、日志、残留文件等可清理项目。"
|
||||
},
|
||||
{
|
||||
"label": "审查",
|
||||
"description": "扫描结果逐项展示,附带说明和空间占用,由你决定保留或清理。"
|
||||
},
|
||||
{
|
||||
"label": "执行",
|
||||
"description": "确认后执行清理。被移除的文件在保留期内自动备份(如适用)。"
|
||||
},
|
||||
{
|
||||
"label": "恢复",
|
||||
"description": "在保留窗口内,可通过操作历史逐项还原已清理的文件。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"developer": {
|
||||
"sectionTitle": "开发者友好",
|
||||
"subtitle": "Atlas 识别开发工具链,避免误删关键文件。",
|
||||
"items": [
|
||||
{
|
||||
"title": "工具链感知",
|
||||
"description": "自动识别 Homebrew、Xcode、Node.js 等开发环境的缓存和产物,标记为安全项或谨慎项。"
|
||||
},
|
||||
{
|
||||
"title": "项目目录保护",
|
||||
"description": "检测 .git、node_modules、build 等项目目录,扫描时默认排除,避免干扰开发流程。"
|
||||
},
|
||||
{
|
||||
"title": "命令行集成",
|
||||
"description": "提供 CLI 工具支持脚本化扫描和清理,方便集成到自动化工作流。"
|
||||
},
|
||||
{
|
||||
"title": "透明的操作日志",
|
||||
"description": "所有操作记录完整的审计日志,包含路径、大小和时间戳,便于排查问题。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"safety": {
|
||||
"sectionTitle": "安全机制",
|
||||
"subtitle": "Atlas 的设计原则:操作前告知,操作后可恢复。",
|
||||
"points": [
|
||||
{
|
||||
"title": "操作前预览",
|
||||
"description": "每次清理前完整展示待处理文件列表,不存在「一键清理」的黑箱操作。"
|
||||
},
|
||||
{
|
||||
"title": "文件级备份",
|
||||
"description": "被清理的文件在保留期内自动备份,支持从操作历史中逐项恢复(如适用)。"
|
||||
},
|
||||
{
|
||||
"title": "最小权限",
|
||||
"description": "Atlas 仅在需要时请求特定权限,每次均说明用途。不要求完全磁盘访问权限即可运行基础功能。"
|
||||
}
|
||||
],
|
||||
"gatekeeperGuide": {
|
||||
"title": "首次打开 Atlas",
|
||||
"steps": [
|
||||
"从官网下载 Atlas .dmg 文件",
|
||||
"将 Atlas 拖入「应用程序」文件夹",
|
||||
"右键点击 Atlas 图标,选择「打开」",
|
||||
"在弹出的对话框中点击「打开」以确认"
|
||||
]
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"sectionTitle": "界面预览",
|
||||
"items": [
|
||||
{
|
||||
"src": "/images/screenshots/atlas-overview.png",
|
||||
"alt": "Atlas 系统概览界面",
|
||||
"caption": "系统概览 — 磁盘健康、缓存占比、可回收空间"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-smart-clean.png",
|
||||
"alt": "Atlas 智能清理界面",
|
||||
"caption": "智能清理 — 扫描结果逐项展示,确认后执行"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-apps.png",
|
||||
"alt": "Atlas 应用管理界面",
|
||||
"caption": "应用管理 — 查看应用完整足迹和关联文件"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-history.png",
|
||||
"alt": "Atlas 操作历史界面",
|
||||
"caption": "操作历史 — 完整审计轨迹与恢复入口"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshots/atlas-settings.png",
|
||||
"alt": "Atlas 设置界面",
|
||||
"caption": "设置 — 权限管理与保留期配置"
|
||||
}
|
||||
]
|
||||
},
|
||||
"openSource": {
|
||||
"sectionTitle": "开源透明",
|
||||
"repoLabel": "GitHub 仓库",
|
||||
"licenseLabel": "开源协议",
|
||||
"attributionLabel": "第三方致谢",
|
||||
"changelogLabel": "更新日志"
|
||||
},
|
||||
"faq": {
|
||||
"sectionTitle": "常见问题",
|
||||
"items": [
|
||||
{
|
||||
"question": "Atlas 经过签名和公证吗?",
|
||||
"answer": "Atlas 使用 Apple Developer ID 签名,并通过 Apple 公证服务验证。首次打开时,macOS Gatekeeper 会自动校验完整性。预发布版本可能未经公证,需要手动确认打开。"
|
||||
},
|
||||
{
|
||||
"question": "预发布版本安装后会怎样?",
|
||||
"answer": "预发布版本可能包含未完成的功能或已知问题,仅供测试用途。由于未经 Apple 公证,首次打开需要右键选择「打开」。建议不要在重要设备上作为主力工具使用。"
|
||||
},
|
||||
{
|
||||
"question": "Atlas 会上传我的文件吗?",
|
||||
"answer": "不会。Atlas 完全在本地运行,不收集、不上传任何用户文件或个人数据。扫描结果和操作记录均存储在本机。源代码完整开源,可自行审查。"
|
||||
},
|
||||
{
|
||||
"question": "「可恢复」具体指什么?",
|
||||
"answer": "Atlas 在执行清理前,会将待删除文件备份到本地保留目录。在保留期(默认 30 天)内,你可以通过操作历史逐项恢复已清理的文件。保留期过后,备份文件将被自动清除。此机制基于文件级备份,并非系统快照,部分场景可能不适用。"
|
||||
},
|
||||
{
|
||||
"question": "Atlas 需要「完全磁盘访问」权限吗?",
|
||||
"answer": "基础功能无需完全磁盘访问权限即可运行。Atlas 遵循最小权限原则,仅在需要扫描特定受保护目录时才会请求对应权限,并在请求前说明用途。"
|
||||
},
|
||||
{
|
||||
"question": "Atlas 是 Mac App Store 应用吗?",
|
||||
"answer": "目前不是。Atlas 通过官网直接下载分发,以保留完整的系统维护能力。Mac App Store 的沙箱限制会影响核心清理和恢复功能。未来可能考虑上架精简版本。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"download": "下载 Atlas",
|
||||
"github": "GitHub",
|
||||
"documentation": "使用文档",
|
||||
"privacy": "隐私政策",
|
||||
"security": "安全说明",
|
||||
"copyright": "Atlas for Mac. 开源项目,基于 MIT 协议发布。"
|
||||
}
|
||||
}
|
||||
93
Apps/LandingSite/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import '../styles/utilities.css';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
locale: 'en' | 'zh';
|
||||
ogImage?: string;
|
||||
}
|
||||
|
||||
const { title, description, locale, ogImage = '/images/og-image-zh.png' } = Astro.props;
|
||||
const htmlLang = locale === 'zh' ? 'zh-Hans' : 'en';
|
||||
const altLocale = locale === 'zh' ? 'en' : 'zh';
|
||||
const altPath = locale === 'zh' ? '/en/' : '/zh/';
|
||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||
const altUrl = new URL(altPath, Astro.site);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={htmlLang}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Canonical & hreflang -->
|
||||
<link rel="canonical" href={canonicalUrl.href} />
|
||||
<link rel="alternate" hreflang={htmlLang} href={canonicalUrl.href} />
|
||||
<link rel="alternate" hreflang={altLocale === 'zh' ? 'zh-Hans' : 'en'} href={altUrl.href} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(ogImage, Astro.site).href} />
|
||||
<meta property="og:url" content={canonicalUrl.href} />
|
||||
<meta property="og:locale" content={locale === 'zh' ? 'zh_CN' : 'en_US'} />
|
||||
<meta property="og:site_name" content="Atlas for Mac" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={new URL(ogImage, Astro.site).href} />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/images/atlas-icon.png" />
|
||||
|
||||
<!-- Font preload -->
|
||||
<link rel="preload" href="/fonts/SpaceGrotesk-Bold.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/InstrumentSans-Regular.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<!-- Structured Data: SoftwareApplication -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Atlas for Mac",
|
||||
"operatingSystem": "macOS",
|
||||
"applicationCategory": "UtilitiesApplication",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"description": description,
|
||||
"url": canonicalUrl.href
|
||||
})} />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
<!-- Scroll-reveal observer -->
|
||||
<script>
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
document.querySelectorAll('.fade-in').forEach((el) => observer.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
43
Apps/LandingSite/src/pages/en/index.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import NavBar from '../../components/NavBar.astro';
|
||||
import Hero from '../../components/Hero.astro';
|
||||
import TrustStrip from '../../components/TrustStrip.astro';
|
||||
import ProblemOutcome from '../../components/ProblemOutcome.astro';
|
||||
import FeatureGrid from '../../components/FeatureGrid.astro';
|
||||
import HowItWorks from '../../components/HowItWorks.astro';
|
||||
import DeveloperSection from '../../components/DeveloperSection.astro';
|
||||
import SafetySection from '../../components/SafetySection.astro';
|
||||
import ScreenshotGallery from '../../components/ScreenshotGallery.astro';
|
||||
import OpenSourceSection from '../../components/OpenSourceSection.astro';
|
||||
import FaqSection from '../../components/FaqSection.astro';
|
||||
import FooterSection from '../../components/FooterSection.astro';
|
||||
import { t } from '../../i18n/utils';
|
||||
|
||||
const locale = 'en' as const;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={copy.meta.title}
|
||||
description={copy.meta.description}
|
||||
locale={locale}
|
||||
ogImage={copy.meta.ogImage}
|
||||
>
|
||||
<NavBar locale={locale} />
|
||||
|
||||
<main>
|
||||
<Hero locale={locale} />
|
||||
<TrustStrip locale={locale} />
|
||||
<ProblemOutcome locale={locale} />
|
||||
<FeatureGrid locale={locale} />
|
||||
<HowItWorks locale={locale} />
|
||||
<DeveloperSection locale={locale} />
|
||||
<SafetySection locale={locale} />
|
||||
<ScreenshotGallery locale={locale} />
|
||||
<OpenSourceSection locale={locale} />
|
||||
<FaqSection locale={locale} />
|
||||
</main>
|
||||
|
||||
<FooterSection locale={locale} />
|
||||
</BaseLayout>
|
||||
4
Apps/LandingSite/src/pages/index.astro
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
// Root redirects to default locale /zh/
|
||||
return Astro.redirect('/zh/', 302);
|
||||
---
|
||||
43
Apps/LandingSite/src/pages/zh/index.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import NavBar from '../../components/NavBar.astro';
|
||||
import Hero from '../../components/Hero.astro';
|
||||
import TrustStrip from '../../components/TrustStrip.astro';
|
||||
import ProblemOutcome from '../../components/ProblemOutcome.astro';
|
||||
import FeatureGrid from '../../components/FeatureGrid.astro';
|
||||
import HowItWorks from '../../components/HowItWorks.astro';
|
||||
import DeveloperSection from '../../components/DeveloperSection.astro';
|
||||
import SafetySection from '../../components/SafetySection.astro';
|
||||
import ScreenshotGallery from '../../components/ScreenshotGallery.astro';
|
||||
import OpenSourceSection from '../../components/OpenSourceSection.astro';
|
||||
import FaqSection from '../../components/FaqSection.astro';
|
||||
import FooterSection from '../../components/FooterSection.astro';
|
||||
import { t } from '../../i18n/utils';
|
||||
|
||||
const locale = 'zh' as const;
|
||||
const copy = t(locale);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={copy.meta.title}
|
||||
description={copy.meta.description}
|
||||
locale={locale}
|
||||
ogImage={copy.meta.ogImage}
|
||||
>
|
||||
<NavBar locale={locale} />
|
||||
|
||||
<main>
|
||||
<Hero locale={locale} />
|
||||
<TrustStrip locale={locale} />
|
||||
<ProblemOutcome locale={locale} />
|
||||
<FeatureGrid locale={locale} />
|
||||
<HowItWorks locale={locale} />
|
||||
<DeveloperSection locale={locale} />
|
||||
<SafetySection locale={locale} />
|
||||
<ScreenshotGallery locale={locale} />
|
||||
<OpenSourceSection locale={locale} />
|
||||
<FaqSection locale={locale} />
|
||||
</main>
|
||||
|
||||
<FooterSection locale={locale} />
|
||||
</BaseLayout>
|
||||
84
Apps/LandingSite/src/styles/global.css
Normal file
@@ -0,0 +1,84 @@
|
||||
@import './reset.css';
|
||||
@import './tokens.css';
|
||||
|
||||
/* ── Font Faces ────────────────────────────────────── */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url('/fonts/SpaceGrotesk-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url('/fonts/SpaceGrotesk-Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
src: url('/fonts/InstrumentSans-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
src: url('/fonts/InstrumentSans-Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
src: url('/fonts/IBMPlexMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
src: url('/fonts/IBMPlexMono-Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ── Base ──────────────────────────────────────────── */
|
||||
|
||||
body {
|
||||
font-family: var(--atlas-font-body);
|
||||
font-size: var(--atlas-text-body);
|
||||
font-weight: var(--atlas-text-body-weight);
|
||||
color: var(--atlas-color-text-primary);
|
||||
background-color: var(--atlas-color-bg-base);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--atlas-font-display);
|
||||
line-height: var(--atlas-leading-tight);
|
||||
letter-spacing: var(--atlas-tracking-tight);
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--atlas-font-mono);
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--atlas-color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--atlas-radius-sm);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgba(20, 144, 133, 0.3);
|
||||
color: var(--atlas-color-text-primary);
|
||||
}
|
||||
65
Apps/LandingSite/src/styles/reset.css
Normal file
@@ -0,0 +1,65 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
line-height: var(--atlas-leading-normal);
|
||||
}
|
||||
|
||||
img, picture, video, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
104
Apps/LandingSite/src/styles/tokens.css
Normal file
@@ -0,0 +1,104 @@
|
||||
/* ══════════════════════════════════════════════════════
|
||||
Atlas Landing Page — Design Tokens
|
||||
Source of truth: AtlasBrand.swift + AtlasColors.xcassets
|
||||
Theme: "Precision Utility" (dark-only)
|
||||
══════════════════════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── Colors: Background ──────────────────────────── */
|
||||
--atlas-color-bg-base: #0D0F11;
|
||||
--atlas-color-bg-surface: #1A1D21;
|
||||
--atlas-color-bg-surface-hover: #22262B;
|
||||
--atlas-color-bg-raised: rgba(255, 255, 255, 0.06);
|
||||
--atlas-color-bg-code: #151820;
|
||||
|
||||
/* ── Colors: Brand ───────────────────────────────── */
|
||||
--atlas-color-brand: #149085;
|
||||
--atlas-color-brand-light: #0F766E;
|
||||
--atlas-color-brand-glow: rgba(20, 144, 133, 0.25);
|
||||
|
||||
/* ── Colors: Accent ──────────────────────────────── */
|
||||
--atlas-color-accent: #34D399;
|
||||
--atlas-color-accent-bright: #52E2B5;
|
||||
|
||||
/* ── Colors: Semantic ────────────────────────────── */
|
||||
--atlas-color-success: #22C55E;
|
||||
--atlas-color-warning: #F59E0B;
|
||||
--atlas-color-danger: #EF4444;
|
||||
--atlas-color-info: #3B82F6;
|
||||
|
||||
/* ── Colors: Text ────────────────────────────────── */
|
||||
--atlas-color-text-primary: #F1F5F9;
|
||||
--atlas-color-text-secondary: #94A3B8;
|
||||
--atlas-color-text-tertiary: rgba(148, 163, 184, 0.6);
|
||||
|
||||
/* ── Colors: Border ──────────────────────────────── */
|
||||
--atlas-color-border: rgba(241, 245, 249, 0.08);
|
||||
--atlas-color-border-emphasis: rgba(241, 245, 249, 0.14);
|
||||
|
||||
/* ── Typography ──────────────────────────────────── */
|
||||
--atlas-font-display: 'Space Grotesk', system-ui, sans-serif;
|
||||
--atlas-font-body: 'Instrument Sans', system-ui, sans-serif;
|
||||
--atlas-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
|
||||
--atlas-text-hero: clamp(2.5rem, 5vw, 4rem);
|
||||
--atlas-text-hero-weight: 700;
|
||||
--atlas-text-section: clamp(1.75rem, 3.5vw, 2.5rem);
|
||||
--atlas-text-section-weight: 700;
|
||||
--atlas-text-card-title: 1.25rem;
|
||||
--atlas-text-card-title-weight: 600;
|
||||
--atlas-text-body: 1rem;
|
||||
--atlas-text-body-weight: 400;
|
||||
--atlas-text-body-small: 0.875rem;
|
||||
--atlas-text-label: 0.875rem;
|
||||
--atlas-text-label-weight: 600;
|
||||
--atlas-text-caption: 0.75rem;
|
||||
--atlas-text-caption-weight: 600;
|
||||
--atlas-text-caption-small: 0.6875rem;
|
||||
|
||||
--atlas-leading-tight: 1.2;
|
||||
--atlas-leading-normal: 1.6;
|
||||
--atlas-leading-relaxed: 1.8;
|
||||
--atlas-tracking-tight: -0.02em;
|
||||
--atlas-tracking-normal: 0;
|
||||
--atlas-tracking-wide: 0.05em;
|
||||
|
||||
/* ── Spacing (4pt grid) ──────────────────────────── */
|
||||
--atlas-space-xxs: 4px;
|
||||
--atlas-space-xs: 6px;
|
||||
--atlas-space-sm: 8px;
|
||||
--atlas-space-md: 12px;
|
||||
--atlas-space-lg: 16px;
|
||||
--atlas-space-xl: 20px;
|
||||
--atlas-space-xxl: 24px;
|
||||
--atlas-space-screen-h: 28px;
|
||||
--atlas-space-section: 32px;
|
||||
--atlas-space-section-gap: 80px;
|
||||
--atlas-space-section-gap-lg: 120px;
|
||||
|
||||
/* ── Radius ──────────────────────────────────────── */
|
||||
--atlas-radius-sm: 8px;
|
||||
--atlas-radius-md: 12px;
|
||||
--atlas-radius-lg: 16px;
|
||||
--atlas-radius-xl: 20px;
|
||||
--atlas-radius-xxl: 24px;
|
||||
--atlas-radius-full: 9999px;
|
||||
|
||||
/* ── Elevation ───────────────────────────────────── */
|
||||
--atlas-shadow-flat: none;
|
||||
--atlas-shadow-raised: 0 10px 18px rgba(0, 0, 0, 0.05);
|
||||
--atlas-shadow-prominent: 0 16px 28px rgba(0, 0, 0, 0.09);
|
||||
--atlas-shadow-cta: 0 6px 12px rgba(20, 144, 133, 0.25);
|
||||
--atlas-shadow-cta-hover: 0 8px 20px rgba(20, 144, 133, 0.35);
|
||||
|
||||
/* ── Motion ──────────────────────────────────────── */
|
||||
--atlas-motion-fast: 150ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--atlas-motion-standard: 220ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--atlas-motion-slow: 350ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--atlas-stagger-delay: 80ms;
|
||||
|
||||
/* ── Layout ──────────────────────────────────────── */
|
||||
--atlas-width-reading: 920px;
|
||||
--atlas-width-content: 1080px;
|
||||
--atlas-width-workspace: 1200px;
|
||||
}
|
||||
87
Apps/LandingSite/src/styles/utilities.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/* ── Layout Utilities ──────────────────────────────── */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--atlas-width-content);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--atlas-space-xl);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding-inline: var(--atlas-space-section);
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-block: var(--atlas-space-section-gap);
|
||||
}
|
||||
|
||||
.section--lg {
|
||||
padding-block: var(--atlas-space-section-gap-lg);
|
||||
}
|
||||
|
||||
.band--dark {
|
||||
background-color: var(--atlas-color-bg-base);
|
||||
}
|
||||
|
||||
.band--surface {
|
||||
background-color: var(--atlas-color-bg-surface);
|
||||
}
|
||||
|
||||
/* ── Accessibility ─────────────────────────────────── */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* ── Text Utilities ────────────────────────────────── */
|
||||
|
||||
.text-secondary {
|
||||
color: var(--atlas-color-text-secondary);
|
||||
}
|
||||
|
||||
.text-tertiary {
|
||||
color: var(--atlas-color-text-tertiary);
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: var(--atlas-color-accent);
|
||||
}
|
||||
|
||||
.text-brand {
|
||||
color: var(--atlas-color-brand);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--atlas-color-warning);
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--atlas-font-display);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--atlas-font-mono);
|
||||
}
|
||||
|
||||
/* ── Animation Utilities ──────────────────────────── */
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
transition: opacity var(--atlas-motion-slow), transform var(--atlas-motion-slow);
|
||||
}
|
||||
|
||||
.fade-in.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
9
Apps/LandingSite/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
- `EPIC-18` Public Beta Feedback and Trust Closure
|
||||
- `EPIC-19` GA Recovery and Execution Hardening
|
||||
- `EPIC-20` GA Launch Readiness
|
||||
- `EPIC-21` Marketing Site and Direct Distribution Landing Page
|
||||
|
||||
## Now / Next / Later
|
||||
|
||||
@@ -282,6 +283,15 @@
|
||||
- `ATL-235` Run a trusted hardware-diverse signed beta cohort — `Product Agent`
|
||||
- `ATL-236` Triage public-beta issues before any GA candidate naming — `Product Agent`
|
||||
|
||||
#### Launch Surface Phase 5: Landing Page and Domain
|
||||
|
||||
- `ATL-241` Finalize landing-page PRD, CTA policy, and bilingual information architecture — `Product Agent`
|
||||
- `ATL-242` Design and implement the marketing site in `Apps/LandingSite/` — `Mac App Agent`
|
||||
- `ATL-243` Add GitHub Pages deployment workflow and environment protection — `Release Agent`
|
||||
- `ATL-244` Bind and verify a dedicated custom domain with HTTPS enforcement — `Release Agent`
|
||||
- `ATL-245` Surface release-channel state, download guidance, and prerelease install help on the page — `UX Agent`
|
||||
- `ATL-246` Add privacy-respecting analytics and launch QA for desktop/mobile — `QA Agent`
|
||||
|
||||
## Definition of Ready
|
||||
|
||||
- Scope is clear and bounded
|
||||
|
||||
509
Docs/Execution/Landing-Page-PRD-2026-03-14.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# Landing Page PRD — 2026-03-14
|
||||
|
||||
## Summary
|
||||
|
||||
- Product: `Atlas for Mac` landing page
|
||||
- Type: marketing site + release-distribution entry
|
||||
- Primary goal: turn qualified visitors into `download`, `GitHub visit/star`, and `beta updates` conversions
|
||||
- Deployment baseline: `GitHub Pages` with a custom domain and GitHub Actions deployment workflow
|
||||
- Recommended source location at implementation time: `Apps/LandingSite/`
|
||||
|
||||
This PRD is intentionally separate from the core app MVP PRD. It does not change the frozen app-module scope. It defines a branded acquisition and trust surface for direct distribution.
|
||||
|
||||
## Background
|
||||
|
||||
Atlas for Mac now has a usable direct-distribution path, GitHub Releases, packaging automation, and prerelease support. What it does not yet have is a purpose-built landing page that:
|
||||
|
||||
- explains the product quickly to first-time visitors
|
||||
- distinguishes signed releases from prereleases honestly
|
||||
- reduces fear around cleanup, permissions, and Gatekeeper warnings
|
||||
- converts open-source interest into actual downloads
|
||||
- gives the project a canonical domain instead of relying on the repository README alone
|
||||
|
||||
The landing page must behave like a launch surface, not a generic OSS README mirror.
|
||||
|
||||
## Goals
|
||||
|
||||
- Present Atlas as a modern, trust-first Mac utility, not a vague “cleaner”
|
||||
- Clarify the product promise in under 10 seconds
|
||||
- Make direct download, GitHub proof, and safety signals visible above the fold
|
||||
- Support both `English` and `简体中文`
|
||||
- Explain prerelease installation honestly when Apple-signed distribution is unavailable
|
||||
- Provide a stable deployment path on GitHub with a separately managed custom domain
|
||||
- Stay lightweight, static-first, and easy to maintain by a small team
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No full documentation portal in v1
|
||||
- No in-browser disk scan demo
|
||||
- No account system
|
||||
- No pricing or checkout flow in v1
|
||||
- No blog/CMS dependency required for initial launch
|
||||
- No analytics stack that requires invasive tracking cookies by default
|
||||
|
||||
## Target Users
|
||||
|
||||
### Primary
|
||||
|
||||
- Mac users with recurring disk pressure or system clutter
|
||||
- Developers with Xcode, simulators, containers, caches, package-manager artifacts, and build leftovers
|
||||
- Users evaluating alternatives to opaque commercial cleanup apps
|
||||
|
||||
### Secondary
|
||||
|
||||
- Open-source-oriented users who want to inspect the code before installing
|
||||
- Tech creators and reviewers looking for screenshots, positioning, and trust signals
|
||||
- Early beta testers willing to tolerate prerelease install friction
|
||||
|
||||
## User Jobs
|
||||
|
||||
- “Tell me what Atlas actually does in one screen.”
|
||||
- “Help me decide whether this is safe enough to install.”
|
||||
- “Show me how Atlas is different from aggressive one-click cleaners.”
|
||||
- “Let me download the latest build without digging through GitHub.”
|
||||
- “Explain whether this is a signed release or a prerelease before I install.”
|
||||
|
||||
## Positioning
|
||||
|
||||
### Core Positioning Statement
|
||||
|
||||
Atlas for Mac is an explainable, recovery-first Mac maintenance workspace that helps people understand why their Mac is slow, full, or disorganized, then take safer action.
|
||||
|
||||
### Core Differentiators To Surface
|
||||
|
||||
- `Explainable` instead of opaque
|
||||
- `Recovery-first` instead of destructive-by-default
|
||||
- `Developer-aware` instead of mainstream-only cleanup
|
||||
- `Open source` instead of black-box utility
|
||||
- `Least-privilege` instead of front-loaded permission pressure
|
||||
|
||||
### Messaging Constraints
|
||||
|
||||
- Do not use the `Mole` brand in user-facing naming
|
||||
- Do not claim malware protection or antivirus behavior
|
||||
- Do not overstate physical recovery coverage
|
||||
- Do not imply that all releases are Apple-signed if they are not
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Primary Metrics
|
||||
|
||||
- Landing-page visitor to download CTA click-through rate
|
||||
- Download CTA click to GitHub Release visit rate
|
||||
- Stable-release vs prerelease download split
|
||||
- GitHub repo visit/star rate from the landing page
|
||||
- Beta updates signup conversion rate if email capture ships
|
||||
|
||||
### Secondary Metrics
|
||||
|
||||
- Time on page
|
||||
- Scroll depth to trust/safety section
|
||||
- Screenshot gallery interaction rate
|
||||
- FAQ expansion rate
|
||||
- Core Web Vitals pass rate
|
||||
|
||||
## Release and Distribution Strategy
|
||||
|
||||
The page must treat release channel status as product truth, not as hidden implementation detail.
|
||||
|
||||
### Required States
|
||||
|
||||
- `Signed Release Available`
|
||||
- `Prerelease Only`
|
||||
- `No Public Download Yet`
|
||||
|
||||
### CTA Behavior
|
||||
|
||||
- If a signed stable release exists: primary CTA is `Download for macOS`
|
||||
- If only prerelease exists: primary CTA is `Download Prerelease`, with an explicit warning label
|
||||
- If no downloadable release exists: primary CTA becomes `View on GitHub` or `Join Beta Updates`
|
||||
|
||||
### Required Disclosure
|
||||
|
||||
- Show exact version number and release date
|
||||
- Show channel badge: `Stable`, `Prerelease`, or `Internal Beta`
|
||||
- Show a short install note if the selected asset may trigger Gatekeeper friction
|
||||
|
||||
## Information Architecture
|
||||
|
||||
The landing page should be a single-scroll page with anchored sections and a sticky top nav.
|
||||
|
||||
### Top Navigation
|
||||
|
||||
- Logo / wordmark
|
||||
- `Why Atlas`
|
||||
- `How It Works`
|
||||
- `Developers`
|
||||
- `Safety`
|
||||
- `FAQ`
|
||||
- language toggle
|
||||
- primary CTA
|
||||
|
||||
### Page Sections
|
||||
|
||||
#### 1. Hero
|
||||
|
||||
- Purpose: make the product understandable immediately
|
||||
- Content:
|
||||
- strong product headline
|
||||
- short subheadline
|
||||
- primary CTA
|
||||
- secondary CTA to GitHub
|
||||
- release-state badge
|
||||
- macOS screenshot or stylized product frame
|
||||
- Copy angle:
|
||||
- “Understand what’s taking space”
|
||||
- “Review before cleaning”
|
||||
- “Recover when supported”
|
||||
|
||||
#### 2. Trust Signal Strip
|
||||
|
||||
- Open source
|
||||
- Recovery-first
|
||||
- Developer-aware cleanup
|
||||
- macOS native workspace
|
||||
- Direct download / GitHub Releases
|
||||
|
||||
#### 3. Problem-to-Outcome Narrative
|
||||
|
||||
- “Mac is full” -> Atlas explains why
|
||||
- “Caches and leftovers are unclear” -> Atlas turns findings into an action plan
|
||||
- “Cleanup feels risky” -> Atlas emphasizes reversibility and history
|
||||
|
||||
#### 4. Feature Story Grid
|
||||
|
||||
- `Overview`
|
||||
- `Smart Clean`
|
||||
- `Apps`
|
||||
- `History`
|
||||
- `Recovery`
|
||||
- `Permissions`
|
||||
|
||||
Each card must show:
|
||||
|
||||
- user-facing value
|
||||
- one concrete example
|
||||
- one trust cue
|
||||
|
||||
#### 5. How It Works
|
||||
|
||||
- Scan
|
||||
- Review plan
|
||||
- Execute safe actions
|
||||
- Restore when supported
|
||||
|
||||
This section should visualize the workflow as a clear four-step progression.
|
||||
|
||||
#### 6. Developer Cleanup Section
|
||||
|
||||
- Xcode derived data
|
||||
- simulators
|
||||
- package manager caches
|
||||
- build artifacts
|
||||
- developer-oriented disk pressure scenarios
|
||||
|
||||
This section exists because developer cleanup coverage is one of Atlas’s most differentiated acquisition angles.
|
||||
|
||||
#### 7. Safety and Permissions Section
|
||||
|
||||
- explain least-privilege behavior
|
||||
- explain why Atlas does not request everything up front
|
||||
- explain release channel status honestly
|
||||
- include the prerelease Gatekeeper warning visual when relevant
|
||||
|
||||
#### 8. Screenshots / Product Tour
|
||||
|
||||
- 4 to 6 macOS screenshots
|
||||
- captions tied to user outcomes, not just feature names
|
||||
- desktop-first layout with mobile fallback carousel
|
||||
|
||||
#### 9. Open Source and Credibility
|
||||
|
||||
- GitHub repo link
|
||||
- MIT license
|
||||
- attribution statement
|
||||
- optional changelog/release notes links
|
||||
|
||||
#### 10. FAQ
|
||||
|
||||
- Is Atlas signed and notarized?
|
||||
- What happens in prerelease installs?
|
||||
- Does Atlas upload my files?
|
||||
- What does recovery actually mean?
|
||||
- Does Atlas require Full Disk Access?
|
||||
- Is this a Mac App Store app?
|
||||
|
||||
#### 11. Footer
|
||||
|
||||
- download links
|
||||
- GitHub
|
||||
- documentation
|
||||
- privacy statement
|
||||
- security contact
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### FR-01 Release Metadata
|
||||
|
||||
The page must display:
|
||||
|
||||
- latest downloadable version
|
||||
- release channel
|
||||
- published date
|
||||
- links to `.dmg`, `.zip`, `.pkg` or the GitHub release page
|
||||
|
||||
### FR-02 Channel-Aware UI
|
||||
|
||||
If the latest downloadable build is a prerelease, the UI must:
|
||||
|
||||
- display a `Prerelease` badge above the primary CTA
|
||||
- show a short warning near the CTA
|
||||
- expose a help path for Gatekeeper friction
|
||||
|
||||
### FR-03 Bilingual Support
|
||||
|
||||
The page must support `English` and `简体中文` with:
|
||||
|
||||
- manual language switch
|
||||
- stable URL strategy or query/path handling
|
||||
- localized metadata where feasible
|
||||
|
||||
### FR-04 Download Path Clarity
|
||||
|
||||
Users must be able to tell:
|
||||
|
||||
- where to download
|
||||
- which file is recommended
|
||||
- whether they are installing a prerelease
|
||||
- what to do if macOS blocks the app
|
||||
|
||||
### FR-05 Trust Links
|
||||
|
||||
The page must link to:
|
||||
|
||||
- GitHub repository
|
||||
- GitHub Releases
|
||||
- changelog or release notes
|
||||
- security disclosure path
|
||||
- open-source attribution / license references
|
||||
|
||||
### FR-06 Optional Beta Updates Capture
|
||||
|
||||
The page should support an optional email capture block using a third-party form endpoint or mailing-list provider without introducing a custom backend in v1.
|
||||
|
||||
### FR-07 Responsive Behavior
|
||||
|
||||
The experience must work on:
|
||||
|
||||
- desktop
|
||||
- iPad/tablet portrait
|
||||
- mobile phones
|
||||
|
||||
No critical CTA or release information may fall below inaccessible accordion depth on mobile.
|
||||
|
||||
## Visual Direction
|
||||
|
||||
### Design Theme
|
||||
|
||||
`Precision Utility`
|
||||
|
||||
The page should feel like a modern macOS-native operations console translated into a polished marketing surface. It should avoid generic SaaS gradients, purple-heavy palettes, and interchangeable startup layouts.
|
||||
|
||||
### Visual Principles
|
||||
|
||||
- clean but not sterile
|
||||
- high trust over hype
|
||||
- native Mac feel over abstract Web3-style spectacle
|
||||
- strong hierarchy over crowded feature dumping
|
||||
- product screenshots as proof, not decoration
|
||||
|
||||
### Typography
|
||||
|
||||
- Display: `Space Grotesk`
|
||||
- Body: `Instrument Sans`
|
||||
- Utility / telemetry labels: `IBM Plex Mono`
|
||||
|
||||
### Color System
|
||||
|
||||
- Background base: graphite / near-black
|
||||
- Surface: warm slate cards
|
||||
- Primary accent: mint-teal
|
||||
- Secondary accent: cold cyan
|
||||
- Support accent: controlled amber for caution or prerelease states
|
||||
|
||||
### Layout Style
|
||||
|
||||
- strong hero with diagonal or staggered composition
|
||||
- large product framing device
|
||||
- generous spacing
|
||||
- rounded but not bubbly surfaces
|
||||
- visual rhythm built from alternating dark/light emphasis bands
|
||||
|
||||
### Motion
|
||||
|
||||
- staggered section reveal on first load
|
||||
- subtle screenshot parallax only if performance budget permits
|
||||
- badge/CTA hover states with restrained motion
|
||||
- no endless floating particles or decorative loops
|
||||
|
||||
## Copy Direction
|
||||
|
||||
- tone: direct, calm, technically credible
|
||||
- avoid hype words like “ultimate”, “magic”, or “AI cleaner”
|
||||
- prefer concrete verbs: `Scan`, `Review`, `Restore`, `Download`
|
||||
- always qualify prerelease or unsigned friction honestly
|
||||
|
||||
## SEO Requirements
|
||||
|
||||
- page title and meta description in both supported languages
|
||||
- Open Graph and Twitter card metadata
|
||||
- software-application structured data
|
||||
- canonical URL
|
||||
- `hreflang` support if separate localized URLs are used
|
||||
- crawlable, text-based headings; no hero text rendered only inside images
|
||||
|
||||
## Analytics Requirements
|
||||
|
||||
Use privacy-respecting analytics by default.
|
||||
|
||||
### Required Events
|
||||
|
||||
- primary CTA click
|
||||
- GitHub CTA click
|
||||
- release file choice click
|
||||
- FAQ expand
|
||||
- screenshot gallery interaction
|
||||
- beta signup submit
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
- `Plausible` or `Umami`
|
||||
- Google Search Console for indexing and query monitoring
|
||||
|
||||
## Technical and Deployment Requirements
|
||||
|
||||
### Hosting
|
||||
|
||||
- Use `GitHub Pages`
|
||||
- Deploy via `GitHub Actions` custom workflow
|
||||
- Keep the site static-first
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
Build job:
|
||||
|
||||
- install dependencies
|
||||
- build static output
|
||||
- upload Pages artifact
|
||||
|
||||
Deploy job:
|
||||
|
||||
- use `actions/deploy-pages`
|
||||
- grant `pages: write` and `id-token: write`
|
||||
- publish to the `github-pages` environment
|
||||
|
||||
This aligns with GitHub’s current Pages guidance for custom workflows and deployment permissions.
|
||||
|
||||
### Custom Domain Strategy
|
||||
|
||||
- Use a dedicated brand domain
|
||||
- Prefer `www` as canonical host
|
||||
- Redirect apex to `www` or configure both correctly
|
||||
- Verify the domain at the GitHub account or organization level before binding it to the repository
|
||||
- Enforce HTTPS after DNS propagation
|
||||
- Do not use wildcard DNS
|
||||
- Do not rely on a manually committed `CNAME` file when using a custom GitHub Actions Pages workflow
|
||||
|
||||
### DNS Requirements
|
||||
|
||||
For `www`:
|
||||
|
||||
- configure `CNAME` -> `<account>.github.io`
|
||||
|
||||
For apex:
|
||||
|
||||
- use `A`/`AAAA` records or `ALIAS/ANAME` according to GitHub Pages documentation
|
||||
|
||||
### Repository Integration
|
||||
|
||||
- Source should live in this repository under `Apps/LandingSite/`
|
||||
- Landing page deployment should not interfere with app release workflows
|
||||
- Landing page builds should trigger on landing-page source changes and optionally on GitHub Release publication
|
||||
|
||||
### Release Integration
|
||||
|
||||
The landing page should not depend on client-side GitHub API fetches for critical first-paint release messaging if it can be avoided. Preferred order:
|
||||
|
||||
1. build-time generated release manifest
|
||||
2. static embedded release metadata
|
||||
3. client-side GitHub API fetch as fallback
|
||||
|
||||
## Recommended MVP Scope
|
||||
|
||||
### Included
|
||||
|
||||
- one bilingual single-page site
|
||||
- responsive hero
|
||||
- feature story sections
|
||||
- screenshot gallery
|
||||
- trust/safety section
|
||||
- FAQ
|
||||
- dynamic release state block
|
||||
- GitHub Pages deployment
|
||||
- custom domain binding
|
||||
- privacy-respecting analytics
|
||||
|
||||
### Deferred
|
||||
|
||||
- blog
|
||||
- changelog microsite
|
||||
- testimonials from named users
|
||||
- interactive benchmark calculator
|
||||
- gated PDF lead magnet
|
||||
- multi-page docs hub
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A first-time visitor can understand Atlas in under 10 seconds from the hero
|
||||
- The page clearly distinguishes `Stable` vs `Prerelease`
|
||||
- A prerelease visitor can discover the `Open Anyway` recovery path without leaving the page confused
|
||||
- The site is deployable from GitHub to a custom domain with HTTPS
|
||||
- Desktop and mobile layouts preserve screenshot clarity and CTA visibility
|
||||
- The page links cleanly to GitHub Releases and the repository
|
||||
- Language switching works without broken layout or missing content
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
### Phase 1
|
||||
|
||||
- finalize PRD
|
||||
- confirm domain choice
|
||||
- confirm release CTA policy for prerelease vs stable
|
||||
|
||||
### Phase 2
|
||||
|
||||
- design mock or coded prototype
|
||||
- implement static site in `Apps/LandingSite/`
|
||||
- add GitHub Pages workflow
|
||||
|
||||
### Phase 3
|
||||
|
||||
- bind custom domain
|
||||
- enable HTTPS
|
||||
- add analytics and search-console verification
|
||||
- run launch QA on desktop and mobile
|
||||
|
||||
## Risks
|
||||
|
||||
- The site may overpromise signed-distribution status if release metadata is not surfaced dynamically
|
||||
- GitHub Pages custom-domain misconfiguration can create HTTPS or takeover risk
|
||||
- A generic SaaS aesthetic would dilute Atlas’s product differentiation
|
||||
- Screenshots can become stale if app UI evolves faster than site updates
|
||||
- Email capture can add privacy or maintenance overhead if introduced too early
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the canonical domain be a product-only brand domain or a broader studio-owned domain path?
|
||||
- Should prerelease downloads be direct asset links or always route through the GitHub Release page first?
|
||||
- Is email capture required for v1, or is GitHub + download conversion sufficient?
|
||||
- Should bilingual content use one URL with client-side switching or separate localized routes?
|
||||
@@ -32,6 +32,7 @@ This directory contains the working product, design, engineering, and compliance
|
||||
- `Execution/Execution-Credibility-Gate-Review-2026-03-12.md` — gate review for ATL-211, ATL-212, and ATL-215 Smart Clean execution credibility work
|
||||
- `Execution/Recovery-Contract-2026-03-13.md` — frozen recovery semantics, claim boundaries, and acceptance evidence for ATL-221 through ATL-224
|
||||
- `Execution/Recovery-Credibility-Gate-Review-2026-03-13.md` — gate review for ATL-221 through ATL-225 recovery credibility work
|
||||
- `Execution/Landing-Page-PRD-2026-03-14.md` — PRD for the public landing page, GitHub Pages deployment, custom domain strategy, and launch-surface requirements
|
||||
- `Execution/Smart-Clean-Execution-Coverage-2026-03-09.md` — user-facing summary of what Smart Clean can execute for real today
|
||||
- `Execution/Smart-Clean-QA-Checklist-2026-03-09.md` — QA checklist for scan, execute, rescan, and physical restore validation
|
||||
- `Execution/Smart-Clean-Manual-Verification-2026-03-09.md` — local-machine fixture workflow for validating real Smart Clean execution and restore
|
||||
|
||||
@@ -112,3 +112,11 @@
|
||||
- Owner: `Product Agent`
|
||||
- Risk: GA release notes, README copy, or in-app messaging may overstate Atlas's recovery model before physical restore is actually shipped for file-backed recoverable actions.
|
||||
- Mitigation: Treat recovery wording as a gated release artifact. Either ship physical restore for file-backed recoverable actions before GA or narrow all GA-facing recovery claims to the shipped behavior.
|
||||
|
||||
## R-015 Launch Surface Trust Drift
|
||||
|
||||
- Impact: High
|
||||
- Probability: Medium
|
||||
- Owner: `Product Agent`
|
||||
- Risk: A future landing page or custom-domain launch surface may overstate release readiness, signed-install status, recovery behavior, or permission expectations relative to the actual downloadable build.
|
||||
- Mitigation: Make release-channel state and install guidance dynamic, keep prerelease warnings visible, and gate launch-surface copy review with the same trust standards used for README and release materials.
|
||||
|
||||