style: Update default theme
This commit is contained in:
@@ -17,21 +17,24 @@ Owns the public-facing Angular 19 marketing site — landing pages, screenshots,
|
||||
|------|------------|------------------|
|
||||
| **Marketing site** | The Angular 19 app under `website/`, served separately from the product client. | "landing page" (it has multiple pages) |
|
||||
| **Release manifest** | The release-metadata JSON the marketing site links to for download buttons; produced by `tools/generate-release-manifest.js` and published by Gitea Workflows. | "version manifest" |
|
||||
| **IIS SSR bundle** | The built `website/dist/toju-website/` output containing Angular's `browser/`, `server/`, and root `web.config` for iisnode hosting. | "static-only website build" |
|
||||
|
||||
## Relationships
|
||||
|
||||
- The **Marketing site** links to release artifacts produced by the Gitea Workflows under `.gitea/workflows/release-draft.yml` and `publish-draft-release.yml`.
|
||||
- `.gitea/workflows/deploy-web-apps.yml` deploys the **IIS SSR bundle** by mirroring the full `website/dist/toju-website/` folder to IIS.
|
||||
- It does **not** consume the signaling server, the product client, or shared kernel types — independent codebase.
|
||||
|
||||
## Boundaries / IO
|
||||
|
||||
- **Exposes:** the public website bundle, deployed by `.gitea/workflows/deploy-web-apps.yml`.
|
||||
- **Exposes:** the public website SSR bundle, deployed by `.gitea/workflows/deploy-web-apps.yml`.
|
||||
- **Consumes:** the release manifest URL and download links; static assets under `website/src/images/`.
|
||||
|
||||
## Invariants
|
||||
|
||||
- The marketing site has its own `package.json` and its own Angular version — do **not** hoist its dependencies into the root workspace.
|
||||
- It must remain functional with no backend (static deploy); any dynamic behavior should fail gracefully.
|
||||
- It must not depend on the product runtime or signaling server; SSR and the `/api/releases` proxy are website-local Node/IIS concerns.
|
||||
- `npm run build` must leave `public/web.config` at `dist/toju-website/web.config` so IIS can route requests to `server/server.mjs`.
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ Angular 19 marketing site for MetoYou / Toju. This package is separate from the
|
||||
## Commands
|
||||
|
||||
- `npm run start` starts the local dev server and uses `proxy.conf.json`.
|
||||
- `npm run build` builds the site to `dist/toju-website` with the configured SSR/prerender setup.
|
||||
- `npm run build` builds the site to `dist/toju-website` with the configured SSR/prerender setup and copies `public/web.config` to the release root for IIS.
|
||||
- `npm run watch` rebuilds in development mode.
|
||||
- `npm run test` runs the Karma test suite.
|
||||
- `npm run test:design` runs lightweight Node guards for the homepage design direction and IIS SSR release config.
|
||||
- `npm run serve:ssr:toju-website` serves the built SSR output.
|
||||
|
||||
## Structure
|
||||
@@ -22,6 +23,8 @@ Angular 19 marketing site for MetoYou / Toju. This package is separate from the
|
||||
| `src/app/` | Website pages, sections, and shared UI |
|
||||
| `src/images/` | Marketing images copied to `/images` during build |
|
||||
| `public/` | Static public assets |
|
||||
| `public/web.config` | IIS/iisnode entry point for the Angular SSR server |
|
||||
| `tools/copy-iis-web-config.mjs` | Post-build step that places `web.config` beside `browser/` and `server/` in the release output |
|
||||
| `proxy.conf.json` | Local development proxy configuration |
|
||||
| `angular.json` | Angular build, serve, SSR, prerender, and test targets |
|
||||
|
||||
@@ -29,4 +32,5 @@ Angular 19 marketing site for MetoYou / Toju. This package is separate from the
|
||||
|
||||
- The website is its own Angular workspace and is not installed by the root `npm install`.
|
||||
- Build output in `dist/toju-website/` is generated.
|
||||
- IIS deployments publish the full `dist/toju-website/` folder so `server/server.mjs`, `browser/`, and root `web.config` stay together.
|
||||
- Keep website code isolated from `toju-app/`, `electron/`, and `server/`.
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
"input": "public",
|
||||
"ignore": [
|
||||
"web.config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
@@ -130,7 +133,10 @@
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
"input": "public",
|
||||
"ignore": [
|
||||
"web.config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"postbuild": "node tools/copy-iis-web-config.mjs",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"test:design": "node --test tools/website-design.test.mjs tools/iis-release-config.test.mjs",
|
||||
"serve:ssr:toju-website": "node dist/toju-website/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="iisnode" path="server/server.mjs" verb="*" modules="iisnode" />
|
||||
</handlers>
|
||||
<iisnode node_env="production" />
|
||||
<staticContent>
|
||||
<remove fileExtension=".wasm" />
|
||||
<remove fileExtension=".webmanifest" />
|
||||
@@ -9,13 +13,13 @@
|
||||
</staticContent>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Angular Routes" stopProcessing="true">
|
||||
<rule name="Release API proxy" stopProcessing="true">
|
||||
<match url="^api/releases$" />
|
||||
<action type="Rewrite" url="server/server.mjs" />
|
||||
</rule>
|
||||
<rule name="Angular SSR" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/index.html" />
|
||||
<action type="Rewrite" url="server/server.mjs" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
/>
|
||||
<span class="text-xl font-bold text-foreground">{{ 'common.brand' | translate }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="ml-2 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider"
|
||||
>
|
||||
<span class="ml-2 text-[10px] font-semibold px-1.5 py-0.5 rounded-sm bg-primary/15 text-primary border border-primary/25 uppercase tracking-wider">
|
||||
{{ 'components.header.beta' | translate }}
|
||||
</span>
|
||||
</a>
|
||||
@@ -78,7 +76,7 @@
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-md bg-primary text-primary-foreground text-sm font-semibold hover:bg-accent transition-all"
|
||||
>
|
||||
{{ 'components.header.useWebVersion' | translate }}
|
||||
<svg
|
||||
@@ -175,7 +173,7 @@
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-md bg-primary text-primary-foreground text-sm font-semibold"
|
||||
>
|
||||
{{ 'components.header.useWebVersion' | translate }}
|
||||
</a>
|
||||
|
||||
@@ -1,538 +1,184 @@
|
||||
<!-- Hero -->
|
||||
<section class="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<!-- Gradient orbs -->
|
||||
<div
|
||||
[appParallax]="0.15"
|
||||
class="absolute top-1/4 -left-32 w-96 h-96 bg-purple-600/20 rounded-full blur-[128px] animate-float"
|
||||
></div>
|
||||
<div
|
||||
[appParallax]="0.25"
|
||||
class="absolute bottom-1/4 -right-32 w-80 h-80 bg-violet-500/15 rounded-full blur-[100px] animate-float"
|
||||
style="animation-delay: -3s"
|
||||
></div>
|
||||
<section class="home-shell">
|
||||
<div class="hero-grid">
|
||||
<div class="hero-copy section-fade">
|
||||
<p class="eyebrow">{{ 'pages.home.hero.badge' | translate }}</p>
|
||||
<h1>
|
||||
<span>{{ 'pages.home.hero.titleLine1' | translate }}</span>
|
||||
<span>{{ 'pages.home.hero.titleLine2' | translate }}</span>
|
||||
</h1>
|
||||
<p class="hero-description">{{ 'pages.home.hero.description' | translate }}</p>
|
||||
|
||||
<div class="relative z-10 container mx-auto px-6 text-center pt-32 pb-20">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 text-purple-400 text-sm font-medium mb-8 animate-fade-in"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-purple-400 animate-pulse"></span>
|
||||
{{ 'pages.home.hero.badge' | translate }}
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
@if (downloadUrl()) {
|
||||
<a
|
||||
[href]="downloadUrl()"
|
||||
class="button-primary"
|
||||
>
|
||||
{{ 'common.actions.downloadFor' | translate:{ os: getDetectedOsLabel() } }}
|
||||
<span aria-hidden="true">{{ detectedOS().icon }}</span>
|
||||
</a>
|
||||
} @else {
|
||||
<a
|
||||
routerLink="/downloads"
|
||||
class="button-primary"
|
||||
>
|
||||
{{ 'common.actions.downloadBrand' | translate }}
|
||||
</a>
|
||||
}
|
||||
|
||||
<h1 class="text-5xl md:text-7xl lg:text-8xl font-extrabold tracking-tight mb-6 animate-fade-in-up">
|
||||
<span class="text-foreground">{{ 'pages.home.hero.titleLine1' | translate }}</span><br />
|
||||
<span class="gradient-text">{{ 'pages.home.hero.titleLine2' | translate }}</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 leading-relaxed animate-fade-in-up"
|
||||
style="animation-delay: 0.2s"
|
||||
>
|
||||
{{ 'pages.home.hero.description' | translate }}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6 animate-fade-in-up"
|
||||
style="animation-delay: 0.4s"
|
||||
>
|
||||
@if (downloadUrl()) {
|
||||
<a
|
||||
[href]="downloadUrl()"
|
||||
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40 animate-glow-pulse"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
{{ 'common.actions.downloadFor' | translate:{ os: getDetectedOsLabel() } }}
|
||||
<span class="text-sm opacity-75">{{ detectedOS().icon }}</span>
|
||||
</a>
|
||||
} @else {
|
||||
<a
|
||||
routerLink="/downloads"
|
||||
class="group inline-flex items-center gap-3 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold text-lg hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
{{ 'common.actions.downloadBrand' | translate }}
|
||||
</a>
|
||||
}
|
||||
|
||||
<a
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium text-lg hover:bg-card hover:border-purple-500/30 transition-all backdrop-blur-sm"
|
||||
>
|
||||
{{ 'common.actions.openInBrowser' | translate }}
|
||||
<svg
|
||||
class="w-5 h-5 opacity-60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (latestVersion()) {
|
||||
<p
|
||||
class="text-xs text-muted-foreground/60 animate-fade-in"
|
||||
style="animation-delay: 0.6s"
|
||||
>
|
||||
{{ 'pages.home.hero.version' | translate:{ version: latestVersion() } }} ·
|
||||
<a
|
||||
routerLink="/downloads"
|
||||
class="underline hover:text-muted-foreground transition-colors"
|
||||
>{{ 'pages.home.hero.allPlatforms' | translate }}</a
|
||||
>
|
||||
</p>
|
||||
}
|
||||
|
||||
<!-- Scroll indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
||||
<svg
|
||||
class="w-6 h-6 text-muted-foreground/40"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-ad-slot />
|
||||
|
||||
<!-- Features Grid -->
|
||||
<section class="relative py-32">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="text-center mb-20 section-fade">
|
||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-4">
|
||||
{{ 'pages.home.features.titleLine1' | translate }}<br />
|
||||
<span class="gradient-text">{{ 'pages.home.features.titleLine2' | translate }}</span>
|
||||
</h2>
|
||||
<p class="text-muted-foreground text-lg max-w-xl mx-auto">{{ 'pages.home.features.description' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Voice Calls -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-6 group-hover:bg-purple-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-purple-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.voiceCalls.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.voiceCalls.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Screen Sharing -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
style="transition-delay: 0.1s"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-violet-500/10 flex items-center justify-center mb-6 group-hover:bg-violet-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-violet-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.screenSharing.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.screenSharing.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- File Sharing -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
style="transition-delay: 0.2s"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-6 group-hover:bg-pink-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-pink-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.fileSharing.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.fileSharing.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Privacy -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center mb-6 group-hover:bg-emerald-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-emerald-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.privacy.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.privacy.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Open Source -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
style="transition-delay: 0.1s"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center mb-6 group-hover:bg-blue-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-blue-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.openSource.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.openSource.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Free -->
|
||||
<div
|
||||
class="section-fade group relative rounded-2xl border border-border/30 bg-card/30 backdrop-blur-sm p-8 hover:border-purple-500/30 hover:bg-card/50 transition-all duration-300"
|
||||
style="transition-delay: 0.2s"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-6 group-hover:bg-amber-500/20 transition-colors">
|
||||
<svg
|
||||
class="w-6 h-6 text-amber-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground mb-3">{{ 'pages.home.features.items.free.title' | translate }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{{ 'pages.home.features.items.free.description' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-ad-slot />
|
||||
|
||||
<!-- Gaming Section -->
|
||||
<section class="relative py-32">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-purple-950/10 to-transparent"></div>
|
||||
<div class="relative container mx-auto px-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<div class="section-fade">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-medium mb-6"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{{ 'pages.home.gaming.badge' | translate }}
|
||||
</div>
|
||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
||||
{{ 'pages.home.gaming.titleLine1' | translate }}<br />
|
||||
<span class="gradient-text">{{ 'pages.home.gaming.titleLine2' | translate }}</span>
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||
{{ 'pages.home.gaming.description' | translate }}
|
||||
</p>
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-center gap-3 text-muted-foreground">
|
||||
<svg
|
||||
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ 'pages.home.gaming.bullets.lowLatency' | translate }}</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-muted-foreground">
|
||||
<svg
|
||||
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ 'pages.home.gaming.bullets.noiseSuppression' | translate }}</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-muted-foreground">
|
||||
<svg
|
||||
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ 'pages.home.gaming.bullets.screenShare' | translate }}</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-muted-foreground">
|
||||
<svg
|
||||
class="w-5 h-5 text-purple-400 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ 'pages.home.gaming.bullets.fileTransfers' | translate }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section-fade relative">
|
||||
<div class="relative rounded-2xl overflow-hidden border border-border/30 bg-card/30 backdrop-blur-sm aspect-video">
|
||||
<img
|
||||
ngSrc="/images/screenshots/screenshare_gaming.png"
|
||||
fill
|
||||
priority
|
||||
[attr.alt]="'pages.home.gaming.imageAlt' | translate"
|
||||
class="object-cover"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent"></div>
|
||||
<div class="absolute bottom-4 left-4 text-sm text-muted-foreground/60">{{ 'pages.home.gaming.caption' | translate }}</div>
|
||||
</div>
|
||||
<!-- Glow effect -->
|
||||
<div class="absolute -inset-4 bg-purple-600/5 rounded-3xl blur-2xl -z-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-ad-slot />
|
||||
|
||||
<!-- Self-hostable Section -->
|
||||
<section class="relative py-32">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="section-fade max-w-3xl mx-auto text-center">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-medium mb-6"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"
|
||||
/>
|
||||
</svg>
|
||||
{{ 'pages.home.selfHostable.badge' | translate }}
|
||||
</div>
|
||||
<h2 class="text-3xl md:text-5xl font-bold text-foreground mb-6">
|
||||
{{ 'pages.home.selfHostable.titleLine1' | translate }}<br />
|
||||
<span class="gradient-text">{{ 'pages.home.selfHostable.titleLine2' | translate }}</span>
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||
{{ 'pages.home.selfHostable.description' | translate }}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a
|
||||
routerLink="/what-is-toju"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||
>
|
||||
{{ 'common.actions.learnHowItWorks' | translate }}
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://git.azaaxin.com/myxelium/Toju"
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 text-muted-foreground hover:text-foreground transition-colors text-sm"
|
||||
class="button-secondary"
|
||||
>
|
||||
<img
|
||||
src="/images/gitea.png"
|
||||
alt=""
|
||||
width="16"
|
||||
height="16"
|
||||
class="w-4 h-4 object-contain"
|
||||
/>
|
||||
{{ 'common.actions.viewSourceCode' | translate }}
|
||||
{{ 'common.actions.openInBrowser' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (latestVersion()) {
|
||||
<p class="release-note">
|
||||
{{ 'pages.home.hero.version' | translate:{ version: latestVersion() } }}
|
||||
<a routerLink="/downloads">{{ 'pages.home.hero.allPlatforms' | translate }}</a>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hero-product section-fade">
|
||||
<img
|
||||
ngSrc="/images/screenshots/screenshot_main.png"
|
||||
width="1320"
|
||||
height="860"
|
||||
priority
|
||||
[attr.alt]="'pages.gallery.featured.imageAlt' | translate"
|
||||
/>
|
||||
<div class="product-note">
|
||||
<strong>{{ 'pages.home.features.items.privacy.title' | translate }}</strong>
|
||||
<span>{{ 'pages.home.features.items.privacy.description' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Banner -->
|
||||
<section class="relative py-24">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-purple-950/20 via-violet-950/30 to-purple-950/20"></div>
|
||||
<div class="relative container mx-auto px-6 text-center section-fade">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-foreground mb-4">{{ 'pages.home.cta.title' | translate }}</h2>
|
||||
<p class="text-muted-foreground text-lg mb-8 max-w-lg mx-auto">{{ 'pages.home.cta.description' | translate }}</p>
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
@if (downloadUrl()) {
|
||||
<a
|
||||
[href]="downloadUrl()"
|
||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
||||
>
|
||||
{{ 'common.actions.downloadFor' | translate:{ os: getDetectedOsLabel() } }}
|
||||
</a>
|
||||
} @else {
|
||||
<a
|
||||
routerLink="/downloads"
|
||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-purple-600 to-violet-600 text-white font-semibold hover:from-purple-500 hover:to-violet-500 transition-all shadow-2xl shadow-purple-500/25"
|
||||
>
|
||||
{{ 'common.actions.downloadBrand' | translate }}
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-8 py-4 rounded-xl border border-border/50 bg-card/50 text-foreground font-medium hover:border-purple-500/30 transition-all"
|
||||
>
|
||||
{{ 'common.actions.tryInBrowser' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
<app-ad-slot />
|
||||
|
||||
<section class="feature-editorial">
|
||||
<div class="section-heading section-fade">
|
||||
<p class="eyebrow">{{ 'pages.home.features.titleLine1' | translate }}</p>
|
||||
<h2>{{ 'pages.home.features.titleLine2' | translate }}</h2>
|
||||
<p>{{ 'pages.home.features.description' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-layout">
|
||||
<article class="feature-panel feature-panel-large section-fade">
|
||||
<span>01</span>
|
||||
<h3>{{ 'pages.home.features.items.voiceCalls.title' | translate }}</h3>
|
||||
<p>{{ 'pages.home.features.items.voiceCalls.description' | translate }}</p>
|
||||
</article>
|
||||
|
||||
<article class="feature-panel section-fade">
|
||||
<span>02</span>
|
||||
<h3>{{ 'pages.home.features.items.screenSharing.title' | translate }}</h3>
|
||||
<p>{{ 'pages.home.features.items.screenSharing.description' | translate }}</p>
|
||||
</article>
|
||||
|
||||
<article class="feature-panel section-fade">
|
||||
<span>03</span>
|
||||
<h3>{{ 'pages.home.features.items.fileSharing.title' | translate }}</h3>
|
||||
<p>{{ 'pages.home.features.items.fileSharing.description' | translate }}</p>
|
||||
</article>
|
||||
|
||||
<article class="feature-panel feature-panel-wide section-fade">
|
||||
<span>04</span>
|
||||
<h3>{{ 'pages.home.features.items.openSource.title' | translate }}</h3>
|
||||
<p>{{ 'pages.home.features.items.openSource.description' | translate }}</p>
|
||||
</article>
|
||||
|
||||
<article class="feature-panel feature-panel-quiet section-fade">
|
||||
<span>05</span>
|
||||
<h3>{{ 'pages.home.features.items.free.title' | translate }}</h3>
|
||||
<p>{{ 'pages.home.features.items.free.description' | translate }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-ad-slot />
|
||||
|
||||
<section class="proof-section">
|
||||
<div class="proof-image section-fade">
|
||||
<img
|
||||
ngSrc="/images/screenshots/screenshare_gaming.png"
|
||||
width="1280"
|
||||
height="720"
|
||||
[attr.alt]="'pages.home.gaming.imageAlt' | translate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="proof-copy section-fade">
|
||||
<p class="eyebrow">{{ 'pages.home.gaming.badge' | translate }}</p>
|
||||
<h2>
|
||||
{{ 'pages.home.gaming.titleLine1' | translate }}
|
||||
{{ 'pages.home.gaming.titleLine2' | translate }}
|
||||
</h2>
|
||||
<p>{{ 'pages.home.gaming.description' | translate }}</p>
|
||||
|
||||
<ul>
|
||||
<li>{{ 'pages.home.gaming.bullets.lowLatency' | translate }}</li>
|
||||
<li>{{ 'pages.home.gaming.bullets.noiseSuppression' | translate }}</li>
|
||||
<li>{{ 'pages.home.gaming.bullets.screenShare' | translate }}</li>
|
||||
<li>{{ 'pages.home.gaming.bullets.fileTransfers' | translate }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-ad-slot />
|
||||
|
||||
<section class="host-section section-fade">
|
||||
<div>
|
||||
<p class="eyebrow">{{ 'pages.home.selfHostable.badge' | translate }}</p>
|
||||
<h2>
|
||||
{{ 'pages.home.selfHostable.titleLine1' | translate }}
|
||||
{{ 'pages.home.selfHostable.titleLine2' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="host-section-copy">
|
||||
<p>{{ 'pages.home.selfHostable.description' | translate }}</p>
|
||||
<a
|
||||
routerLink="/what-is-toju"
|
||||
class="text-link"
|
||||
>
|
||||
{{ 'common.actions.learnHowItWorks' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="closing-cta section-fade">
|
||||
<p class="eyebrow">Toju</p>
|
||||
<h2>{{ 'pages.home.cta.title' | translate }}</h2>
|
||||
<p>{{ 'pages.home.cta.description' | translate }}</p>
|
||||
<div class="hero-actions">
|
||||
@if (downloadUrl()) {
|
||||
<a
|
||||
[href]="downloadUrl()"
|
||||
class="button-primary"
|
||||
>
|
||||
{{ 'common.actions.downloadFor' | translate:{ os: getDetectedOsLabel() } }}
|
||||
</a>
|
||||
} @else {
|
||||
<a
|
||||
routerLink="/downloads"
|
||||
class="button-primary"
|
||||
>
|
||||
{{ 'common.actions.downloadBrand' | translate }}
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="button-secondary"
|
||||
>
|
||||
{{ 'common.actions.tryInBrowser' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
341
website/src/app/pages/home/home.component.scss
Normal file
341
website/src/app/pages/home/home.component.scss
Normal file
@@ -0,0 +1,341 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-shell,
|
||||
.feature-editorial,
|
||||
.proof-section,
|
||||
.host-section,
|
||||
.closing-cta {
|
||||
width: min(100% - 2rem, 1240px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.home-shell {
|
||||
min-height: 100dvh;
|
||||
padding: 9rem 0 5rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.78fr) minmax(22rem, 1fr);
|
||||
gap: clamp(2rem, 5vw, 5.5rem);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-copy h1,
|
||||
.section-heading h2,
|
||||
.proof-copy h2,
|
||||
.host-section h2,
|
||||
.closing-cta h2 {
|
||||
margin: 0;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: clamp(3.5rem, 8vw, 7.5rem);
|
||||
line-height: 0.92;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.hero-copy h1 span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 1.25rem;
|
||||
color: hsl(var(--primary));
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-description,
|
||||
.section-heading p,
|
||||
.proof-copy p,
|
||||
.host-section p,
|
||||
.closing-cta p,
|
||||
.feature-panel p {
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.75;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
max-width: 35rem;
|
||||
margin: 2rem 0 0;
|
||||
font-size: clamp(1.05rem, 2vw, 1.35rem);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.85rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
.button-secondary,
|
||||
.text-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.45rem;
|
||||
font-weight: 700;
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
padding: 0.85rem 1.2rem;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
box-shadow: 0 1rem 2.5rem hsl(var(--primary) / 0.16);
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
padding: 0.85rem 1.1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--card) / 0.74);
|
||||
}
|
||||
|
||||
.button-primary:hover,
|
||||
.button-secondary:hover,
|
||||
.text-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.button-primary:active,
|
||||
.button-secondary:active,
|
||||
.text-link:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.release-note {
|
||||
margin-top: 1.25rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.release-note a,
|
||||
.text-link {
|
||||
margin-left: 0.75rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.hero-product {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.85rem;
|
||||
background: linear-gradient(180deg, hsl(var(--card)), hsl(var(--background)));
|
||||
box-shadow: 0 2rem 5rem hsl(0 0% 0% / 0.28);
|
||||
}
|
||||
|
||||
.hero-product img,
|
||||
.proof-image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.product-note {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
width: min(22rem, calc(100% - 2rem));
|
||||
padding: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.65rem;
|
||||
background: hsl(var(--background) / 0.84);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.product-note strong,
|
||||
.product-note span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-note span {
|
||||
margin-top: 0.35rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feature-editorial,
|
||||
.proof-section,
|
||||
.host-section,
|
||||
.closing-cta {
|
||||
padding: 6rem 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
max-width: 54rem;
|
||||
margin-bottom: 2.75rem;
|
||||
}
|
||||
|
||||
.section-heading h2,
|
||||
.proof-copy h2,
|
||||
.host-section h2,
|
||||
.closing-cta h2 {
|
||||
font-size: clamp(2.4rem, 5vw, 5.25rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
max-width: 36rem;
|
||||
margin: 1rem 0 0;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.feature-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.85fr 0.85fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-panel {
|
||||
min-height: 18rem;
|
||||
padding: clamp(1.25rem, 3vw, 2rem);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.7rem;
|
||||
background: hsl(var(--card));
|
||||
}
|
||||
|
||||
.feature-panel-large {
|
||||
grid-row: span 2;
|
||||
min-height: 37rem;
|
||||
background: hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.feature-panel-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.feature-panel-quiet {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.feature-panel span {
|
||||
color: hsl(var(--primary));
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.82rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.feature-panel h3 {
|
||||
margin: 3rem 0 0.8rem;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: clamp(1.4rem, 3vw, 2.6rem);
|
||||
line-height: 1.06;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.proof-section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(20rem, 0.7fr);
|
||||
gap: clamp(2rem, 5vw, 5rem);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proof-image {
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.8rem;
|
||||
background: hsl(var(--card));
|
||||
}
|
||||
|
||||
.proof-copy ul {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 0;
|
||||
margin: 1.5rem 0 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.proof-copy li {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid hsl(var(--primary));
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.host-section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.82fr) minmax(0, 1fr);
|
||||
gap: clamp(2rem, 6vw, 5rem);
|
||||
align-items: start;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.host-section p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.host-section-copy {
|
||||
display: grid;
|
||||
gap: 1.2rem;
|
||||
max-width: 38rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.host-section-copy .text-link {
|
||||
justify-self: start;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.closing-cta {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.closing-cta p:not(.eyebrow) {
|
||||
max-width: 34rem;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.hero-grid,
|
||||
.proof-section,
|
||||
.host-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-layout {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.feature-panel-large,
|
||||
.feature-panel-wide {
|
||||
grid-column: span 2;
|
||||
min-height: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.home-shell {
|
||||
padding-top: 7rem;
|
||||
}
|
||||
|
||||
.feature-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-panel-large,
|
||||
.feature-panel-wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.product-note {
|
||||
position: static;
|
||||
width: auto;
|
||||
border-width: 1px 0 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
.button-secondary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { ReleaseService, DetectedOS } from '../../services/release.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ScrollAnimationService } from '../../services/scroll-animation.service';
|
||||
import { AdSlotComponent } from '../../components/ad-slot/ad-slot.component';
|
||||
import { ParallaxDirective } from '../../directives/parallax.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -23,10 +22,10 @@ import { ParallaxDirective } from '../../directives/parallax.directive';
|
||||
NgOptimizedImage,
|
||||
TranslateModule,
|
||||
RouterLink,
|
||||
AdSlotComponent,
|
||||
ParallaxDirective
|
||||
AdSlotComponent
|
||||
],
|
||||
templateUrl: './home.component.html'
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss'
|
||||
})
|
||||
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
readonly detectedOS = signal<DetectedOS>({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700;800&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -6,22 +6,24 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 6%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 262.1 83.3% 57.8%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.75rem;
|
||||
--background: 210 18% 7%;
|
||||
--foreground: 42 33% 94%;
|
||||
--card: 210 17% 10%;
|
||||
--card-foreground: 42 33% 94%;
|
||||
--primary: 154 49% 55%;
|
||||
--primary-foreground: 210 18% 7%;
|
||||
--secondary: 210 14% 15%;
|
||||
--secondary-foreground: 42 33% 94%;
|
||||
--muted: 210 14% 15%;
|
||||
--muted-foreground: 42 13% 67%;
|
||||
--accent: 38 64% 61%;
|
||||
--accent-foreground: 210 18% 7%;
|
||||
--border: 210 13% 22%;
|
||||
--input: 210 13% 22%;
|
||||
--ring: 154 49% 55%;
|
||||
--radius: 0.6rem;
|
||||
--font-sans: 'Outfit', system-ui, sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -33,8 +35,29 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground font-sans antialiased;
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||
background:
|
||||
radial-gradient(circle at top left, hsl(var(--primary) / 0.08), transparent 26rem),
|
||||
linear-gradient(180deg, hsl(210 18% 8%), hsl(var(--background)) 34rem);
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
opacity: 0.035;
|
||||
background-image: linear-gradient(90deg, hsl(var(--foreground)) 1px, transparent 1px), linear-gradient(0deg, hsl(var(--foreground)) 1px, transparent 1px);
|
||||
background-size: 22px 22px;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
outline-color: hsl(var(--ring));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +80,12 @@
|
||||
/* Utility classes */
|
||||
.glass {
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
background: hsl(var(--background) / 0.7);
|
||||
border: 1px solid hsl(var(--border) / 0.3);
|
||||
background: hsl(var(--background) / 0.82);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, hsl(var(--primary)), hsl(280 90% 70%), hsl(320 80% 65%));
|
||||
background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -76,7 +99,7 @@
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, hsl(var(--primary)), hsl(280 90% 70%), transparent);
|
||||
background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)), transparent);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
|
||||
@@ -44,8 +44,8 @@ module.exports = {
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
||||
sans: ['Outfit', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
mono: ['IBM Plex Mono', 'Fira Code', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.6s ease-out forwards',
|
||||
|
||||
10
website/tools/copy-iis-web-config.mjs
Normal file
10
website/tools/copy-iis-web-config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
import { copyFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const websiteRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const source = resolve(websiteRoot, 'public/web.config');
|
||||
const destination = resolve(websiteRoot, 'dist/toju-website/web.config');
|
||||
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
copyFileSync(source, destination);
|
||||
22
website/tools/iis-release-config.test.mjs
Normal file
22
website/tools/iis-release-config.test.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
|
||||
const webConfig = readFileSync(new URL('../public/web.config', import.meta.url), 'utf8');
|
||||
const deployScript = readFileSync(new URL('../../tools/deploy-web-apps.ps1', import.meta.url), 'utf8');
|
||||
const angularConfig = readFileSync(new URL('../angular.json', import.meta.url), 'utf8');
|
||||
|
||||
test('website web.config routes IIS requests through the Angular SSR server', () => {
|
||||
assert.match(webConfig, /iisnode/i);
|
||||
assert.match(webConfig, /server\.mjs/);
|
||||
assert.match(webConfig, /node_env/i);
|
||||
assert.match(webConfig, /api\/releases/i);
|
||||
});
|
||||
|
||||
test('IIS deployment publishes the full SSR output instead of only browser assets', () => {
|
||||
assert.match(deployScript, /website\\dist\\toju-website(?!\\browser)/);
|
||||
});
|
||||
|
||||
test('web.config is kept at the SSR release root, not copied as a browser asset', () => {
|
||||
assert.match(angularConfig, /"ignore":\s*\[\s*"web\.config"\s*\]/);
|
||||
});
|
||||
38
website/tools/website-design.test.mjs
Normal file
38
website/tools/website-design.test.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
|
||||
const homeTemplate = readFileSync(new URL('../src/app/pages/home/home.component.html', import.meta.url), 'utf8');
|
||||
const homeStyles = readFileSync(new URL('../src/app/pages/home/home.component.scss', import.meta.url), 'utf8');
|
||||
const styles = readFileSync(new URL('../src/styles.scss', import.meta.url), 'utf8');
|
||||
|
||||
test('home page avoids common AI-generated visual patterns', () => {
|
||||
const bannedPatterns = [
|
||||
/Gradient orbs/i,
|
||||
/gradient-text/,
|
||||
/from-purple-600\s+to-violet-600/,
|
||||
/lg:grid-cols-3/,
|
||||
/blur-\[128px\]/
|
||||
];
|
||||
|
||||
for (const pattern of bannedPatterns) {
|
||||
assert.doesNotMatch(homeTemplate, pattern);
|
||||
}
|
||||
});
|
||||
|
||||
test('global palette uses a restrained product-led brand system', () => {
|
||||
assert.doesNotMatch(styles, /Inter:wght/);
|
||||
assert.doesNotMatch(styles, /hsl\(280\s+90%\s+70%\)/);
|
||||
assert.match(styles, /--primary:\s*\d+\s+\d+%\s+\d+%/);
|
||||
});
|
||||
|
||||
test('hero screenshot does not add duplicate app chrome', () => {
|
||||
assert.doesNotMatch(homeTemplate, /window-bar/);
|
||||
assert.doesNotMatch(homeStyles, /\.window-bar/);
|
||||
});
|
||||
|
||||
test('self-hostable section keeps title and copy in separate readable columns', () => {
|
||||
assert.doesNotMatch(homeStyles, /\.host-section\s*{[^}]*align-items:\s*end/s);
|
||||
assert.match(homeStyles, /\.host-section\s*{[^}]*align-items:\s*start/s);
|
||||
assert.match(homeStyles, /\.host-section-copy/);
|
||||
});
|
||||
Reference in New Issue
Block a user