Compare commits
2 Commits
v1.4.1
...
dcb6cd1ec7
| Author | SHA1 | Date | |
|---|---|---|---|
| dcb6cd1ec7 | |||
| 3b251c8174 |
55
.gitea/workflows/prod.yml
Normal file
55
.gitea/workflows/prod.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Deploy site (production)
|
||||||
|
|
||||||
|
# Déploie le site vitrine (packages/site) sur Plesk via FTPS (lftp), à l'identique
|
||||||
|
# de lidge_web / tracksniff-web. Ne se déclenche que sur un changement du site.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'packages/site/**'
|
||||||
|
- '.gitea/workflows/prod.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-site
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:22-bookworm
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Install scopé au workspace du site : pas de build des deps natives du
|
||||||
|
# serveur (node-pty / node:sqlite) qui n'ont rien à faire ici.
|
||||||
|
- name: Install deps (@arboretum/site)
|
||||||
|
run: npm install -w @arboretum/site --no-audit --no-fund
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: npm run build:site
|
||||||
|
|
||||||
|
- name: Check build output
|
||||||
|
run: ls -la packages/site/dist
|
||||||
|
|
||||||
|
- name: Deploy via FTPS (lftp)
|
||||||
|
run: |
|
||||||
|
if [ -z "$REMOTE_PATH" ]; then
|
||||||
|
echo "::error::Secret SITE_REMOTE_PATH manquant (chemin docroot du vhost git-arboretum.com)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
apt-get update && apt-get install -y lftp
|
||||||
|
lftp -c "
|
||||||
|
open -u \"$FTP_USER\",\"$FTP_PASSWORD\" \"$FTP_HOST\"
|
||||||
|
set ssl:verify-certificate no
|
||||||
|
mirror -R --delete --verbose packages/site/dist \"$REMOTE_PATH\"
|
||||||
|
"
|
||||||
|
env:
|
||||||
|
FTP_HOST: ${{ secrets.FTP_HOST }}
|
||||||
|
FTP_USER: ${{ secrets.FTP_USER }}
|
||||||
|
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||||
|
# Chemin docroot du vhost git-arboretum.com sur Plesk (ex. /httpdocs/arboretum/public).
|
||||||
|
REMOTE_PATH: ${{ secrets.SITE_REMOTE_PATH }}
|
||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run typecheck
|
- run: npm run typecheck
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
- run: npm run build:site
|
||||||
- run: npx vitest run
|
- run: npx vitest run
|
||||||
|
|
||||||
pack-smoke:
|
pack-smoke:
|
||||||
|
|||||||
109
package-lock.json
generated
109
package-lock.json
generated
@@ -23,6 +23,10 @@
|
|||||||
"resolved": "packages/shared",
|
"resolved": "packages/shared",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@arboretum/site": {
|
||||||
|
"resolved": "packages/site",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@arboretum/web": {
|
"node_modules/@arboretum/web": {
|
||||||
"resolved": "packages/web",
|
"resolved": "packages/web",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -764,6 +768,16 @@
|
|||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/jetbrains-mono": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@homebridge/node-pty-prebuilt-multiarch": {
|
"node_modules/@homebridge/node-pty-prebuilt-multiarch": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.13.1.tgz",
|
||||||
@@ -4871,6 +4885,101 @@
|
|||||||
"name": "@arboretum/shared",
|
"name": "@arboretum/shared",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
|
"packages/site": {
|
||||||
|
"name": "@arboretum/site",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.38",
|
||||||
|
"vue-i18n": "^11.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fontsource/jetbrains-mono": "^5.1.0",
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vue-tsc": "^3.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/site/node_modules/vite": {
|
||||||
|
"version": "8.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"rolldown": "1.0.3",
|
||||||
|
"tinyglobby": "^0.2.17"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"@vitejs/devtools": "^0.1.18",
|
||||||
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitejs/devtools": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@arboretum/web",
|
"name": "@arboretum/web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
"pack": "npm run build && npm pack -w @johanleroy/git-arboretum",
|
"pack": "npm run build && npm pack -w @johanleroy/git-arboretum",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev:server": "npm run dev -w @johanleroy/git-arboretum",
|
"dev:server": "npm run dev -w @johanleroy/git-arboretum",
|
||||||
"dev:web": "npm run dev -w @arboretum/web"
|
"dev:web": "npm run dev -w @arboretum/web",
|
||||||
|
"build:site": "npm run build -w @arboretum/site",
|
||||||
|
"dev:site": "npm run dev -w @arboretum/site",
|
||||||
|
"preview:site": "npm run preview -w @arboretum/site"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
|||||||
53
packages/site/index.html
Normal file
53
packages/site/index.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" style="background-color: #09090b">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<meta name="theme-color" content="#09090b" />
|
||||||
|
|
||||||
|
<title>Arboretum — Mission control for your AI coding agents</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Arboretum is a local-first daemon you launch with npx that serves a web dashboard to run and supervise many Claude Code sessions across git worktrees — from any device, even your phone."
|
||||||
|
/>
|
||||||
|
<link rel="canonical" href="https://git-arboretum.com/" />
|
||||||
|
|
||||||
|
<!-- Open Graph / Twitter -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://git-arboretum.com/" />
|
||||||
|
<meta property="og:title" content="Arboretum — Mission control for your AI coding agents" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Run and supervise many Claude Code sessions across every git worktree — from any device, even your phone."
|
||||||
|
/>
|
||||||
|
<meta property="og:site_name" content="Arboretum" />
|
||||||
|
<meta property="og:image" content="https://git-arboretum.com/assets/og-cover.png" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:alt" content="Arboretum — mission control for your AI coding agents" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Arboretum — Mission control for your AI coding agents" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Run and supervise many Claude Code sessions across every git worktree — from any device, even your phone."
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content="https://git-arboretum.com/assets/og-cover.png" />
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<!-- Polices JetBrains Mono self-host via @fontsource (importées dans src/main.ts). -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<noscript>
|
||||||
|
<div style="padding: 24px; font-family: system-ui, sans-serif; color: #f4f4f5; text-align: center">
|
||||||
|
Arboretum — mission control for your AI coding agents.
|
||||||
|
<a href="https://git.lidge.fr/johanleroy/git-arboretum" style="color: #34d399">View on Gitea</a>.
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
packages/site/package.json
Normal file
24
packages/site/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@arboretum/site",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.38",
|
||||||
|
"vue-i18n": "^11.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fontsource/jetbrains-mono": "^5.1.0",
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vue-tsc": "^3.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/site/public/apple-touch-icon.png
Normal file
BIN
packages/site/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/site/public/assets/arboretum-mark.png
Normal file
BIN
packages/site/public/assets/arboretum-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
packages/site/public/assets/og-cover.png
Normal file
BIN
packages/site/public/assets/og-cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
packages/site/public/favicon.ico
Normal file
BIN
packages/site/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
46
packages/site/public/icon.svg
Normal file
46
packages/site/public/icon.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Arboretum">
|
||||||
|
<defs>
|
||||||
|
<!-- glow néon : flou doux derrière les traits et les nœuds -->
|
||||||
|
<filter id="glow" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feGaussianBlur stdDeviation="6" result="b"/>
|
||||||
|
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="branch" x1="256" y1="430" x2="256" y2="90" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#10b981"/>
|
||||||
|
<stop offset="1" stop-color="#34d399"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="512" height="512" fill="#09090b"/>
|
||||||
|
|
||||||
|
<!-- branches / circuit (tronc + fourches symétriques), tracées dans la zone sûre maskable -->
|
||||||
|
<g fill="none" stroke="url(#branch)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round" filter="url(#glow)">
|
||||||
|
<!-- tronc : du nœud sommet jusqu'à la base avec le curseur >_ -->
|
||||||
|
<path d="M256 96 V392"/>
|
||||||
|
<!-- paire haute -->
|
||||||
|
<path d="M256 232 L150 232 V150"/>
|
||||||
|
<path d="M256 232 L362 232 V150"/>
|
||||||
|
<!-- paire médiane -->
|
||||||
|
<path d="M256 300 L104 300 V214"/>
|
||||||
|
<path d="M256 300 L408 300 V214"/>
|
||||||
|
<!-- paire basse -->
|
||||||
|
<path d="M256 356 L150 356 V300"/>
|
||||||
|
<path d="M256 356 L362 356 V300"/>
|
||||||
|
<!-- base : invite de commande >_ -->
|
||||||
|
<path d="M232 404 L246 418 L232 432" stroke-width="14"/>
|
||||||
|
<path d="M258 434 H286" stroke-width="14"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- nœuds (sessions) : anneaux cyan lumineux, centre sombre -->
|
||||||
|
<g filter="url(#glow)">
|
||||||
|
<g fill="#09090b" stroke="#22d3ee" stroke-width="9">
|
||||||
|
<circle cx="256" cy="96" r="20"/>
|
||||||
|
<circle cx="150" cy="150" r="16"/>
|
||||||
|
<circle cx="362" cy="150" r="16"/>
|
||||||
|
<circle cx="104" cy="214" r="16"/>
|
||||||
|
<circle cx="408" cy="214" r="16"/>
|
||||||
|
<circle cx="150" cy="300" r="16"/>
|
||||||
|
<circle cx="362" cy="300" r="16"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
4
packages/site/public/robots.txt
Normal file
4
packages/site/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://git-arboretum.com/sitemap.xml
|
||||||
9
packages/site/public/sitemap.xml
Normal file
9
packages/site/public/sitemap.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://git-arboretum.com/</loc>
|
||||||
|
<lastmod>2026-06-19</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
74
packages/site/src/App.vue
Normal file
74
packages/site/src/App.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import AppHeader from './components/AppHeader.vue';
|
||||||
|
import HeroSection from './components/HeroSection.vue';
|
||||||
|
import ProblemSection from './components/ProblemSection.vue';
|
||||||
|
import FeaturesSection from './components/FeaturesSection.vue';
|
||||||
|
import ShowcaseSection from './components/ShowcaseSection.vue';
|
||||||
|
import WorkGroupsSection from './components/WorkGroupsSection.vue';
|
||||||
|
import HowItWorksSection from './components/HowItWorksSection.vue';
|
||||||
|
import SecuritySection from './components/SecuritySection.vue';
|
||||||
|
import FaqSection from './components/FaqSection.vue';
|
||||||
|
import FinalCta from './components/FinalCta.vue';
|
||||||
|
import AppFooter from './components/AppFooter.vue';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
// Tient l'attribut <html lang> synchronisé avec la langue active.
|
||||||
|
watch(
|
||||||
|
locale,
|
||||||
|
(l) => {
|
||||||
|
document.documentElement.lang = l;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Grain de fond + halo emerald (décoratifs) — portés tels quels du design.
|
||||||
|
const grainStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
inset: '0',
|
||||||
|
zIndex: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
backgroundImage: 'radial-gradient(rgba(255,255,255,.025) 1px,transparent 1.4px)',
|
||||||
|
backgroundSize: '34px 34px',
|
||||||
|
WebkitMaskImage: 'radial-gradient(ellipse 120% 70% at 50% -4%,#000,transparent 70%)',
|
||||||
|
maskImage: 'radial-gradient(ellipse 120% 70% at 50% -4%,#000,transparent 70%)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const glowStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '-26%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: '1100px',
|
||||||
|
height: '680px',
|
||||||
|
zIndex: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
background: 'radial-gradient(50% 50% at 50% 50%,rgba(16,185,129,.12),transparent 64%)',
|
||||||
|
filter: 'blur(18px)',
|
||||||
|
} as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative overflow-x-hidden">
|
||||||
|
<div aria-hidden="true" :style="grainStyle"></div>
|
||||||
|
<div aria-hidden="true" :style="glowStyle"></div>
|
||||||
|
|
||||||
|
<AppHeader />
|
||||||
|
|
||||||
|
<main id="top" class="relative z-[1]">
|
||||||
|
<HeroSection />
|
||||||
|
<ProblemSection />
|
||||||
|
<FeaturesSection />
|
||||||
|
<ShowcaseSection />
|
||||||
|
<WorkGroupsSection />
|
||||||
|
<HowItWorksSection />
|
||||||
|
<SecuritySection />
|
||||||
|
<FaqSection />
|
||||||
|
<FinalCta />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
47
packages/site/src/components/AppFooter.vue
Normal file
47
packages/site/src/components/AppFooter.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import IconGitea from './icons/IconGitea.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const REPO = 'https://git.lidge.fr/johanleroy/git-arboretum';
|
||||||
|
const LICENSE = 'https://git.lidge.fr/johanleroy/git-arboretum/src/branch/main/LICENSE';
|
||||||
|
const COFFEE = 'https://buymeacoffee.com/johanleroy';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="relative z-[1] border-t border-[#1c1c1f]">
|
||||||
|
<div class="mx-auto flex max-w-[1200px] flex-wrap items-center justify-between gap-5 px-6 py-[34px]">
|
||||||
|
<div class="flex items-center gap-[11px]">
|
||||||
|
<img
|
||||||
|
src="/assets/arboretum-mark.png"
|
||||||
|
alt=""
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
class="h-[22px] w-[22px] object-contain"
|
||||||
|
style="filter: grayscale(1) brightness(1.5); opacity: 0.4"
|
||||||
|
/>
|
||||||
|
<span class="font-mono text-sm text-zinc-300">git-arboretum.com</span>
|
||||||
|
<span class="ml-1 text-[13px] text-zinc-600">— {{ t('footTag') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-[22px] text-sm">
|
||||||
|
<a :href="REPO" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 text-zinc-400 no-underline transition-colors hover:text-emerald-400">
|
||||||
|
<IconGitea :size="15" />
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
<a :href="LICENSE" target="_blank" rel="noopener" class="text-zinc-400 no-underline transition-colors hover:text-emerald-400">
|
||||||
|
{{ t('license') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="COFFEE"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-1.5 text-zinc-400 no-underline transition-colors hover:text-amber-400 focus-visible:rounded-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-400/70"
|
||||||
|
>
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2v2" /><path d="M14 2v2" /><path d="M5 8h13a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1Z" /><path d="M6 15a4 4 0 0 0 4 4h2a4 4 0 0 0 4-4" /><path d="M19 9h1.5a2.5 2.5 0 0 1 0 5H18" /></svg>
|
||||||
|
{{ t('coffee') }}
|
||||||
|
</a>
|
||||||
|
<span class="font-mono text-[12.5px] text-zinc-600">npx @johanleroy/git-arboretum</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
59
packages/site/src/components/AppHeader.vue
Normal file
59
packages/site/src/components/AppHeader.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import LangToggle from './LangToggle.vue';
|
||||||
|
import IconGitea from './icons/IconGitea.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const REPO = 'https://git.lidge.fr/johanleroy/git-arboretum';
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ href: '#features', key: 'navFeatures' },
|
||||||
|
{ href: '#how', key: 'navHow' },
|
||||||
|
{ href: '#security', key: 'navSecurity' },
|
||||||
|
{ href: '#faq', key: 'navFaq' },
|
||||||
|
] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-50 border-b border-[#1c1c1f] bg-[rgba(9,9,11,0.72)] backdrop-blur-[14px]"
|
||||||
|
>
|
||||||
|
<div class="mx-auto flex h-16 max-w-[1200px] items-center justify-between gap-6 px-6">
|
||||||
|
<a href="#top" class="flex items-center gap-2.5 text-zinc-100 no-underline">
|
||||||
|
<img
|
||||||
|
src="/assets/arboretum-mark.png"
|
||||||
|
alt="Arboretum"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
class="block h-7 w-7 object-contain"
|
||||||
|
/>
|
||||||
|
<span class="font-mono text-[17px] font-semibold tracking-[-0.01em]">Arboretum</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="hidden items-center gap-[30px] min-[900px]:flex">
|
||||||
|
<a
|
||||||
|
v-for="link in navLinks"
|
||||||
|
:key="link.href"
|
||||||
|
:href="link.href"
|
||||||
|
class="text-[14.5px] text-zinc-400 no-underline transition-colors hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
{{ t(link.key) }}
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3.5">
|
||||||
|
<LangToggle />
|
||||||
|
<a
|
||||||
|
:href="REPO"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
aria-label="Gitea"
|
||||||
|
class="inline-flex items-center gap-[7px] rounded-lg border border-zinc-800 px-[13px] py-[7px] text-[13.5px] font-medium text-zinc-300 no-underline transition-colors hover:border-emerald-500/50 hover:text-emerald-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
>
|
||||||
|
<IconGitea :size="16" />
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
52
packages/site/src/components/FaqSection.vue
Normal file
52
packages/site/src/components/FaqSection.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { FAQS } from '../i18n/content';
|
||||||
|
import type { AppLocale } from '../i18n';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const open = ref(0);
|
||||||
|
const items = computed(() => FAQS[locale.value as AppLocale]);
|
||||||
|
|
||||||
|
function toggle(i: number): void {
|
||||||
|
open.value = open.value === i ? -1 : i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelStyle(i: number) {
|
||||||
|
return {
|
||||||
|
maxHeight: open.value === i ? '300px' : '0px',
|
||||||
|
opacity: open.value === i ? 1 : 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'max-height .4s ease, opacity .35s ease',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section id="faq" class="mx-auto max-w-[780px] scroll-mt-[84px] px-6 pb-[100px]">
|
||||||
|
<div v-reveal class="mb-[38px] text-center">
|
||||||
|
<div class="mb-3.5 font-mono text-xs uppercase tracking-[0.14em] text-emerald-400">{{ t('navFaq') }}</div>
|
||||||
|
<h2 class="m-0 text-[clamp(28px,3.4vw,40px)] font-semibold tracking-[-0.025em] text-zinc-50">{{ t('faqTitle') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-reveal>
|
||||||
|
<div v-for="(f, i) in items" :key="i" class="border-t" :class="open === i ? 'border-emerald-400/40' : 'border-zinc-800'">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="open === i"
|
||||||
|
:aria-controls="`faq-panel-${i}`"
|
||||||
|
class="flex w-full cursor-pointer items-center justify-between gap-4 border-none bg-transparent px-1 py-5 text-left text-[clamp(16px,2vw,18px)] font-medium text-zinc-50 focus-visible:rounded-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
@click="toggle(i)"
|
||||||
|
>
|
||||||
|
<span>{{ f.q }}</span>
|
||||||
|
<span class="flex flex-none text-emerald-400 transition-transform duration-300" :class="open === i ? 'rotate-180' : ''">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div :id="`faq-panel-${i}`" :style="panelStyle(i)" :inert="open !== i" :aria-hidden="open !== i">
|
||||||
|
<p class="m-0 max-w-[640px] px-1 pb-5 text-[15.5px] leading-[1.6] text-zinc-400">{{ f.a }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
17
packages/site/src/components/FeatureCard.vue
Normal file
17
packages/site/src/components/FeatureCard.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{ title: string }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6 transition-[transform,border-color] duration-[180ms] ease-in-out hover:-translate-y-[3px] hover:border-emerald-400/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mb-4 inline-flex h-[42px] w-[42px] items-center justify-center rounded-[10px] bg-emerald-500/10 text-emerald-400"
|
||||||
|
>
|
||||||
|
<slot name="icon" />
|
||||||
|
</div>
|
||||||
|
<h3 class="m-0 mb-[7px] text-lg font-semibold text-zinc-50">{{ title }}</h3>
|
||||||
|
<p class="m-0 text-[14.5px] leading-[1.55] text-zinc-400"><slot /></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
76
packages/site/src/components/FeaturesSection.vue
Normal file
76
packages/site/src/components/FeaturesSection.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import FeatureCard from './FeatureCard.vue';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section id="features" class="mx-auto max-w-[1200px] scroll-mt-[84px] px-6 pb-[100px]">
|
||||||
|
<div v-reveal class="mb-[46px] text-center">
|
||||||
|
<div class="mb-3.5 font-mono text-xs uppercase tracking-[0.14em] text-emerald-400">{{ t('featKicker') }}</div>
|
||||||
|
<h2 class="m-0 text-[clamp(28px,3.4vw,40px)] font-semibold tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('featTitle') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-reveal class="grid grid-cols-[repeat(auto-fit,minmax(264px,1fr))] gap-4">
|
||||||
|
<FeatureCard :title="t('feat1Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="6" y1="3" x2="6" y2="15" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat1Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat2Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat2Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat3Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2" /></svg>
|
||||||
|
</template>
|
||||||
|
<span class="text-sky-300">{{ t('busy') }}</span> ·
|
||||||
|
<span class="text-amber-300">{{ t('waiting') }}</span> ·
|
||||||
|
<span class="text-emerald-400">{{ t('idle') }}</span> — {{ t('feat3Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat4Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2.5" /><path d="M12 18h.01" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat4Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat5Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.27 21a1.94 1.94 0 0 0 3.46 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat5Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat6Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat6Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat7Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /><path d="m7 16.5-4.74-2.85" /><path d="m7 16.5 5-3" /><path d="M7 16.5v5.17" /><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /><path d="m17 16.5 4.74-2.85" /><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat7Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard :title="t('feat8Title')">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></svg>
|
||||||
|
</template>
|
||||||
|
{{ t('feat8Desc') }}
|
||||||
|
</FeatureCard>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
61
packages/site/src/components/FinalCta.vue
Normal file
61
packages/site/src/components/FinalCta.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useCopy, INSTALL_COMMAND } from '../composables/useCopy';
|
||||||
|
import IconGitea from './icons/IconGitea.vue';
|
||||||
|
import IconCopy from './icons/IconCopy.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { copied, copy } = useCopy();
|
||||||
|
const REPO = 'https://git.lidge.fr/johanleroy/git-arboretum';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="mx-auto max-w-[1200px] px-6 pb-[100px]">
|
||||||
|
<div
|
||||||
|
v-reveal
|
||||||
|
class="rounded-[20px] border border-zinc-800 px-6 py-[clamp(36px,5vw,64px)] text-center"
|
||||||
|
style="background: radial-gradient(120% 140% at 50% 0%, rgba(16, 185, 129, 0.1), transparent 60%), #0c0c0e"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/assets/arboretum-mark.png"
|
||||||
|
alt=""
|
||||||
|
width="52"
|
||||||
|
height="52"
|
||||||
|
class="mx-auto mb-5 block h-[52px] w-[52px] object-contain"
|
||||||
|
/>
|
||||||
|
<h2 class="m-0 mb-3.5 text-[clamp(30px,4vw,46px)] font-bold leading-[1.08] tracking-[-0.03em] text-zinc-50">
|
||||||
|
{{ t('ctaTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mx-auto mb-7 max-w-[480px] text-[17px] text-zinc-400">{{ t('ctaBody') }}</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mx-auto flex max-w-[460px] items-stretch overflow-hidden rounded-[10px] border border-zinc-800 bg-zinc-950"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-1 items-center gap-2.5 overflow-x-auto whitespace-nowrap px-4 font-mono text-sm">
|
||||||
|
<span class="text-emerald-400">$</span><span class="text-zinc-200">{{ INSTALL_COMMAND }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Copy install command"
|
||||||
|
class="inline-flex flex-none items-center gap-[7px] border-l border-zinc-800 bg-zinc-900 px-4 font-mono text-[13px] font-semibold text-zinc-300 transition-colors hover:bg-zinc-800 hover:text-emerald-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
@click="copy()"
|
||||||
|
>
|
||||||
|
<IconCopy :size="15" />
|
||||||
|
<span aria-live="polite">{{ copied ? t('copied') : t('copy') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-[18px]">
|
||||||
|
<a
|
||||||
|
:href="REPO"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-2 text-[14.5px] text-zinc-400 no-underline transition-colors hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<IconGitea :size="16" />
|
||||||
|
{{ t('ctaSource') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
212
packages/site/src/components/HeroMockup.vue
Normal file
212
packages/site/src/components/HeroMockup.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useHeroTimeline, type SessionState } from '../composables/useHeroTimeline';
|
||||||
|
import { DLG_OPTS } from '../i18n/content';
|
||||||
|
import type { AppLocale } from '../i18n';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const { anim } = useHeroTimeline('hero-stage');
|
||||||
|
|
||||||
|
// Couleurs des lignes de terminal par type (porté de renderVals.C).
|
||||||
|
const LINE_COLORS: Record<string, string> = {
|
||||||
|
cmd: '#52525b',
|
||||||
|
tool: '#a1a1aa',
|
||||||
|
edit: '#34d399',
|
||||||
|
add: '#34d399',
|
||||||
|
del: '#f87171',
|
||||||
|
ok: '#34d399',
|
||||||
|
warn: '#fcd34d',
|
||||||
|
run: '#7dd3fc',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PILL: Record<SessionState, { bg: string; fg: string; dot: string; pulse: string }> = {
|
||||||
|
idle: { bg: 'rgba(2,44,34,.6)', fg: '#34d399', dot: '#34d399', pulse: '' },
|
||||||
|
busy: { bg: 'rgba(8,47,73,.7)', fg: '#7dd3fc', dot: '#7dd3fc', pulse: 'pulseSky 2s ease-in-out infinite' },
|
||||||
|
waiting: { bg: 'rgba(69,26,3,.7)', fg: '#fcd34d', dot: '#fcd34d', pulse: 'pulseAmber 2.2s ease-in-out infinite' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const pill = computed(() => PILL[anim.value.sess]);
|
||||||
|
const sessText = computed(() => t(anim.value.sess));
|
||||||
|
|
||||||
|
const termLines = computed(() =>
|
||||||
|
anim.value.lines.map((l) => ({ text: l.text, color: LINE_COLORS[l.k] ?? '#d4d4d8' })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dlgOptions = computed(() =>
|
||||||
|
DLG_OPTS[locale.value as AppLocale].map((txt, i) => {
|
||||||
|
const highlighted = anim.value.hl === i;
|
||||||
|
const answered = anim.value.ans === i;
|
||||||
|
const row: Record<string, string> = {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '9px',
|
||||||
|
padding: '7px 9px',
|
||||||
|
borderRadius: '7px',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
background: 'rgba(9,9,11,.4)',
|
||||||
|
transition: 'background .2s, border-color .2s',
|
||||||
|
};
|
||||||
|
if (highlighted) {
|
||||||
|
row.background = 'rgba(69,26,3,.5)';
|
||||||
|
row.border = '1px solid rgba(217,119,6,.6)';
|
||||||
|
}
|
||||||
|
if (answered) {
|
||||||
|
row.background = 'rgba(2,44,34,.45)';
|
||||||
|
row.border = '1px solid rgba(5,150,105,.6)';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
num: answered ? '✓' : String(i + 1),
|
||||||
|
text: txt,
|
||||||
|
rowStyle: row,
|
||||||
|
numColor: answered ? '#34d399' : '#fbbf24',
|
||||||
|
textColor: answered ? '#a7f3d0' : highlighted ? '#fde68a' : '#d4d4d8',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="hero-stage" class="relative min-w-[300px] flex-[1_1_480px]">
|
||||||
|
<div
|
||||||
|
class="relative overflow-hidden rounded-[14px] border border-zinc-800 bg-zinc-950 shadow-hero"
|
||||||
|
>
|
||||||
|
<!-- barre de fenêtre -->
|
||||||
|
<div class="flex items-center gap-2 border-b border-[#1c1c1f] bg-[#0c0c0e] px-[14px] py-[11px]">
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-zinc-700"></span>
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-zinc-700"></span>
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-zinc-700"></span>
|
||||||
|
<span class="ml-2 font-mono text-[11.5px] text-zinc-600">arboretum · localhost:7777</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-h-[360px]">
|
||||||
|
<!-- sidebar -->
|
||||||
|
<div
|
||||||
|
class="flex w-[168px] flex-none flex-col border-r border-[#1c1c1f] bg-zinc-900/40 px-2.5 py-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 px-1 pb-3">
|
||||||
|
<img
|
||||||
|
src="/assets/arboretum-mark.png"
|
||||||
|
alt=""
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
class="h-5 w-5 object-contain"
|
||||||
|
/>
|
||||||
|
<span class="font-mono text-[13px] font-semibold text-zinc-200">Arboretum</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mb-3 flex items-center gap-2 rounded-lg border border-zinc-800 bg-zinc-950/50 px-[9px] py-[7px]"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#52525b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||||
|
<span class="flex-1 text-[11.5px] text-zinc-600">{{ t('mSearch') }}</span>
|
||||||
|
<span class="rounded border border-zinc-800 px-[5px] py-px font-mono text-[10px] text-zinc-500">⌘K</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<div class="flex items-center gap-[9px] rounded-lg bg-zinc-800 px-[9px] py-[7px] text-zinc-100">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" /></svg>
|
||||||
|
<span class="text-[12.5px]">{{ t('mDash') }}</span>
|
||||||
|
<span
|
||||||
|
v-show="anim.att > 0"
|
||||||
|
class="ml-auto inline-flex h-[17px] min-w-[17px] items-center justify-center rounded-full px-[5px] font-mono text-[10px] font-bold"
|
||||||
|
style="background: #f59e0b; color: #1c1300"
|
||||||
|
>{{ anim.att }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-[9px] rounded-lg px-[9px] py-[7px] text-zinc-400">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /><path d="M7 16.5 12 13.5" /><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /><path d="m17 16.5 4.74-2.85" /><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0z" /></svg>
|
||||||
|
<span class="text-[12.5px]">{{ t('mGroups') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-0.5 border-t border-[#1c1c1f] pt-2.5">
|
||||||
|
<div class="flex items-center gap-[9px] rounded-lg px-[9px] py-1.5 text-zinc-400">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>
|
||||||
|
<span class="text-[12.5px]">{{ t('mSettings') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-[9px] rounded-lg px-[9px] py-1.5 text-zinc-400">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></svg>
|
||||||
|
<span class="text-[12.5px]">{{ t('mHelp') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-[9px] pt-1.5 font-mono text-[10px] text-zinc-700">v0.4.2</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- main -->
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col gap-[11px] p-[13px]">
|
||||||
|
<!-- terminal pane -->
|
||||||
|
<div
|
||||||
|
class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[10px] border border-zinc-800 bg-zinc-950"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between border-b border-[#1c1c1f] px-[11px] py-2">
|
||||||
|
<span class="font-mono text-[11.5px] text-zinc-400">api · feat/auth</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-full px-[9px] py-[3px] font-mono text-[11px]"
|
||||||
|
:style="{ background: pill.bg, color: pill.fg }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="h-1.5 w-1.5 flex-none rounded-full"
|
||||||
|
:style="{ background: pill.dot, animation: pill.pulse }"
|
||||||
|
></span>
|
||||||
|
{{ sessText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex min-h-[150px] flex-1 flex-col justify-end overflow-hidden p-[11px] font-mono text-xs leading-[1.7] text-zinc-300"
|
||||||
|
>
|
||||||
|
<div v-for="(ln, i) in termLines" :key="i" :style="{ color: ln.color, whiteSpace: 'pre' }">
|
||||||
|
{{ ln.text }}
|
||||||
|
</div>
|
||||||
|
<div v-if="anim.cur">
|
||||||
|
<span class="text-emerald-400">$</span>
|
||||||
|
<span class="ml-1 inline-block h-[13px] w-[7px] animate-blink align-middle bg-zinc-200"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- dialogue de permission -->
|
||||||
|
<div
|
||||||
|
v-if="anim.dlg"
|
||||||
|
class="animate-dlg-in rounded-[10px] border border-[rgba(120,53,15,0.6)] bg-[rgba(69,26,3,0.3)] p-[11px]"
|
||||||
|
>
|
||||||
|
<div class="mb-[9px] flex items-center gap-2">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
|
||||||
|
<span class="text-[12.5px] text-zinc-200">{{ t('dlgQ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-[5px]">
|
||||||
|
<div v-for="(op, i) in dlgOptions" :key="i" :style="op.rowStyle">
|
||||||
|
<span
|
||||||
|
class="w-[15px] flex-none text-center font-mono text-[11px] font-bold"
|
||||||
|
:style="{ color: op.numColor }"
|
||||||
|
>{{ op.num }}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs" :style="{ color: op.textColor }">{{ op.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- toast push -->
|
||||||
|
<div
|
||||||
|
v-if="anim.toast"
|
||||||
|
class="animate-toast-in absolute bottom-[14px] right-[14px] flex w-[248px] items-start gap-2.5 rounded-[11px] border border-[rgba(120,53,15,0.5)] bg-[#0c0c0e] p-3 shadow-toast"
|
||||||
|
>
|
||||||
|
<span class="inline-flex h-[30px] w-[30px] flex-none items-center justify-center rounded-lg bg-emerald-500/15">
|
||||||
|
<img src="/assets/arboretum-mark.png" alt="" width="18" height="18" class="h-[18px] w-[18px] object-contain" />
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-mono text-[11px] text-emerald-400">Arboretum</span>
|
||||||
|
<span class="text-[10px] text-zinc-600">now</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[3px] text-[13px] font-semibold text-zinc-100">{{ t('toastTitle') }}</div>
|
||||||
|
<div class="mt-px text-[11.5px] leading-[1.4] text-zinc-400">{{ t('toastBody') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
80
packages/site/src/components/HeroSection.vue
Normal file
80
packages/site/src/components/HeroSection.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useCopy, INSTALL_COMMAND } from '../composables/useCopy';
|
||||||
|
import HeroMockup from './HeroMockup.vue';
|
||||||
|
import IconGitea from './icons/IconGitea.vue';
|
||||||
|
import IconCopy from './icons/IconCopy.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { copied, copy } = useCopy();
|
||||||
|
const REPO = 'https://git.lidge.fr/johanleroy/git-arboretum';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="mx-auto flex max-w-[1200px] flex-wrap items-center gap-12 px-6 pb-16 pt-[72px]">
|
||||||
|
<div class="min-w-[300px] flex-[1_1_430px]">
|
||||||
|
<div class="mb-[22px] flex items-center gap-[13px]">
|
||||||
|
<img
|
||||||
|
src="/assets/arboretum-mark.png"
|
||||||
|
alt=""
|
||||||
|
width="46"
|
||||||
|
height="46"
|
||||||
|
class="block h-[46px] w-[46px] object-contain"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/[0.07] px-[11px] py-[5px] font-mono text-[11.5px] uppercase tracking-[0.1em] text-emerald-400"
|
||||||
|
>
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400 shadow-[0_0_8px_#34d399]"></span>
|
||||||
|
{{ t('heroBadge') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="m-0 text-[clamp(40px,5.6vw,64px)] font-bold leading-[1.04] tracking-[-0.035em] text-zinc-50">
|
||||||
|
{{ t('heroTitle') }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-[22px] max-w-[520px] text-[clamp(17px,1.7vw,20px)] leading-[1.55] text-zinc-400">
|
||||||
|
{{ t('heroSub') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-[30px] flex max-w-[480px] items-stretch overflow-hidden rounded-[10px] border border-zinc-800 bg-zinc-950/60"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex min-w-0 flex-1 items-center gap-2.5 overflow-x-auto whitespace-nowrap px-[15px] font-mono text-sm"
|
||||||
|
>
|
||||||
|
<span class="text-emerald-400">$</span><span class="text-zinc-200">{{ INSTALL_COMMAND }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Copy install command"
|
||||||
|
class="inline-flex flex-none items-center gap-[7px] border-l border-zinc-800 bg-zinc-900 px-[15px] font-mono text-[13px] font-semibold text-zinc-300 transition-colors hover:bg-zinc-800 hover:text-emerald-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
@click="copy()"
|
||||||
|
>
|
||||||
|
<IconCopy :size="15" />
|
||||||
|
<span aria-live="polite">{{ copied ? t('copied') : t('copy') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-wrap gap-[13px]">
|
||||||
|
<a
|
||||||
|
href="#how"
|
||||||
|
class="inline-flex items-center gap-[9px] rounded-[9px] bg-emerald-600 px-6 py-[13px] text-[15.5px] font-semibold text-white no-underline transition-[background,transform] duration-150 ease-in-out hover:-translate-y-px hover:bg-emerald-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
>
|
||||||
|
{{ t('getStarted') }}
|
||||||
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="REPO"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="inline-flex items-center gap-[9px] rounded-[9px] border border-zinc-800 px-[22px] py-[13px] text-[15.5px] font-medium text-zinc-100 no-underline transition-colors hover:border-emerald-500/45 hover:text-emerald-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500/70"
|
||||||
|
>
|
||||||
|
<IconGitea :size="17" />
|
||||||
|
{{ t('gitea') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroMockup />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
42
packages/site/src/components/HowItWorksSection.vue
Normal file
42
packages/site/src/components/HowItWorksSection.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section id="how" class="mx-auto max-w-[1200px] scroll-mt-[84px] px-6 pb-[100px]">
|
||||||
|
<div v-reveal class="mb-[46px] text-center">
|
||||||
|
<div class="mb-3.5 font-mono text-xs uppercase tracking-[0.14em] text-emerald-400">{{ t('howKicker') }}</div>
|
||||||
|
<h2 class="m-0 text-[clamp(28px,3.4vw,40px)] font-semibold tracking-[-0.025em] text-zinc-50">{{ t('howTitle') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-reveal class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
|
||||||
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-[26px]">
|
||||||
|
<div class="mb-[18px] font-mono text-[13px] text-emerald-400">01</div>
|
||||||
|
<h3 class="m-0 mb-2.5 text-lg font-semibold text-zinc-50">{{ t('step1Title') }}</h3>
|
||||||
|
<p class="m-0 mb-4 text-[14.5px] leading-[1.55] text-zinc-400">{{ t('step1Desc') }}</p>
|
||||||
|
<div class="overflow-x-auto whitespace-nowrap rounded-lg border border-zinc-800 bg-zinc-950 p-[11px] font-mono text-[12.5px] text-zinc-200">
|
||||||
|
<span class="text-emerald-400">$ </span>npx @johanleroy/git-arboretum
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-[26px]">
|
||||||
|
<div class="mb-[18px] font-mono text-[13px] text-emerald-400">02</div>
|
||||||
|
<h3 class="m-0 mb-2.5 text-lg font-semibold text-zinc-50">{{ t('step2Title') }}</h3>
|
||||||
|
<p class="m-0 mb-4 text-[14.5px] leading-[1.55] text-zinc-400">{{ t('step2Desc') }}</p>
|
||||||
|
<div class="overflow-x-auto whitespace-nowrap rounded-lg border border-zinc-800 bg-zinc-950 p-[11px] font-mono text-[12.5px] text-sky-300">
|
||||||
|
→ http://localhost:7777
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-[26px]">
|
||||||
|
<div class="mb-[18px] font-mono text-[13px] text-emerald-400">03</div>
|
||||||
|
<h3 class="m-0 mb-2.5 text-lg font-semibold text-zinc-50">{{ t('step3Title') }}</h3>
|
||||||
|
<p class="m-0 mb-4 text-[14.5px] leading-[1.55] text-zinc-400">{{ t('step3Desc') }}</p>
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<span class="rounded-md bg-emerald-500/10 px-[9px] py-1 font-mono text-[11px] text-emerald-400">{{ t('idle') }}</span>
|
||||||
|
<span class="rounded-md bg-[rgba(8,47,73,0.7)] px-[9px] py-1 font-mono text-[11px] text-sky-300">{{ t('busy') }}</span>
|
||||||
|
<span class="rounded-md bg-[rgba(69,26,3,0.6)] px-[9px] py-1 font-mono text-[11px] text-amber-300">{{ t('waiting') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
37
packages/site/src/components/LangToggle.vue
Normal file
37
packages/site/src/components/LangToggle.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { setLocale, type AppLocale } from '../i18n';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
function set(l: AppLocale): void {
|
||||||
|
setLocale(l);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-label="Language"
|
||||||
|
class="flex items-center overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950/40"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-pressed="locale === 'en'"
|
||||||
|
class="cursor-pointer border-none px-[11px] py-1.5 font-mono text-xs font-semibold focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-emerald-500/70"
|
||||||
|
:class="locale === 'en' ? 'bg-zinc-800 text-emerald-400' : 'bg-transparent text-zinc-500'"
|
||||||
|
@click="set('en')"
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-pressed="locale === 'fr'"
|
||||||
|
class="cursor-pointer border-none px-[11px] py-1.5 font-mono text-xs font-semibold focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-emerald-500/70"
|
||||||
|
:class="locale === 'fr' ? 'bg-zinc-800 text-emerald-400' : 'bg-transparent text-zinc-500'"
|
||||||
|
@click="set('fr')"
|
||||||
|
>
|
||||||
|
FR
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
packages/site/src/components/ProblemSection.vue
Normal file
14
packages/site/src/components/ProblemSection.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-reveal class="mx-auto max-w-[920px] px-6 pb-[92px] pt-12 text-center">
|
||||||
|
<div class="mb-4 font-mono text-xs uppercase tracking-[0.14em] text-emerald-400">{{ t('probKicker') }}</div>
|
||||||
|
<h2 class="m-0 text-[clamp(28px,3.6vw,42px)] font-semibold leading-[1.18] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('probTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mx-auto mt-[22px] max-w-[640px] text-[clamp(17px,1.6vw,19px)] text-zinc-400">{{ t('probBody') }}</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
47
packages/site/src/components/SecuritySection.vue
Normal file
47
packages/site/src/components/SecuritySection.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section id="security" class="mx-auto max-w-[1200px] scroll-mt-[84px] px-6 pb-[100px]">
|
||||||
|
<div
|
||||||
|
v-reveal
|
||||||
|
class="rounded-[18px] border border-emerald-500/[0.18] p-[clamp(28px,4vw,48px)]"
|
||||||
|
style="background: linear-gradient(180deg, rgba(16, 185, 129, 0.05), rgba(16, 185, 129, 0))"
|
||||||
|
>
|
||||||
|
<div class="mb-[30px] flex flex-wrap items-end justify-between gap-3.5">
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 font-mono text-xs uppercase tracking-[0.14em] text-emerald-400">{{ t('secKicker') }}</div>
|
||||||
|
<h2 class="m-0 text-[clamp(26px,3.2vw,38px)] font-semibold leading-[1.12] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('secTitle') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 max-w-[380px] text-[15.5px] leading-[1.55] text-zinc-400">{{ t('secIntro') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-3.5">
|
||||||
|
<div class="flex flex-col gap-2.5 rounded-[11px] border border-zinc-800 bg-[#0c0c0e] p-[18px]">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="11" x="3" y="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
|
||||||
|
<div class="text-[15px] font-semibold text-zinc-50">{{ t('sec1Title') }}</div>
|
||||||
|
<div class="text-[13.5px] leading-[1.5] text-zinc-400">{{ t('sec1Desc') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2.5 rounded-[11px] border border-zinc-800 bg-[#0c0c0e] p-[18px]">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" /><path d="m21 2-9.6 9.6" /><circle cx="7.5" cy="15.5" r="5.5" /></svg>
|
||||||
|
<div class="text-[15px] font-semibold text-zinc-50">{{ t('sec2Title') }}</div>
|
||||||
|
<div class="text-[13.5px] leading-[1.5] text-zinc-400">{{ t('sec2Desc') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2.5 rounded-[11px] border border-zinc-800 bg-[#0c0c0e] p-[18px]">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="m9 12 2 2 4-4" /></svg>
|
||||||
|
<div class="text-[15px] font-semibold text-zinc-50">{{ t('sec3Title') }}</div>
|
||||||
|
<div class="text-[13.5px] leading-[1.5] text-zinc-400">{{ t('sec3Desc') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2.5 rounded-[11px] border border-zinc-800 bg-[#0c0c0e] p-[18px]">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.55a11 11 0 0 1 14.08 0" /><path d="M1.42 9a16 16 0 0 1 21.16 0" /><path d="M8.53 16.11a6 6 0 0 1 6.95 0" /><path d="M12 20h.01" /></svg>
|
||||||
|
<div class="text-[15px] font-semibold text-zinc-50">{{ t('sec4Title') }}</div>
|
||||||
|
<div class="text-[13.5px] leading-[1.5] text-zinc-400">{{ t('sec4Desc') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
170
packages/site/src/components/ShowcaseSection.vue
Normal file
170
packages/site/src/components/ShowcaseSection.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section id="showcase" class="mx-auto flex max-w-[1200px] scroll-mt-[84px] flex-col gap-[92px] px-6">
|
||||||
|
<!-- a) dashboard + needs attention -->
|
||||||
|
<div v-reveal class="flex flex-wrap items-center gap-12">
|
||||||
|
<div class="min-w-[280px] flex-[1_1_380px]">
|
||||||
|
<div class="mb-3 font-mono text-xs uppercase tracking-[0.12em] text-emerald-400">{{ t('scAKicker') }}</div>
|
||||||
|
<h2 class="m-0 mb-4 text-[clamp(26px,3vw,34px)] font-semibold leading-[1.15] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('scATitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 text-[16.5px] leading-[1.6] text-zinc-400">{{ t('scABody') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-[280px] flex-[1_1_420px]">
|
||||||
|
<div class="overflow-hidden rounded-xl border border-zinc-800 bg-[#0c0c0e] shadow-card">
|
||||||
|
<div class="flex items-center gap-[9px] border-b border-[#1c1c1f] bg-[rgba(69,26,3,0.18)] px-[15px] py-3">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fcd34d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
|
||||||
|
<span class="text-sm font-semibold text-zinc-50">{{ t('mAttn') }}</span>
|
||||||
|
<span class="ml-auto rounded-full bg-amber-300/[0.12] px-[9px] py-0.5 font-mono text-[11px] text-amber-300">2</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 p-2.5">
|
||||||
|
<div class="flex items-center gap-3 rounded-[9px] border border-[rgba(120,53,15,0.55)] bg-[rgba(69,26,3,0.3)] px-[13px] py-[11px]">
|
||||||
|
<span class="h-2 w-2 flex-none animate-pulse-amber rounded-full bg-amber-300"></span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="font-mono text-[13px] text-zinc-200">api · feat/auth</div>
|
||||||
|
<div class="text-xs text-[#d4a55a]">Approve change to login.ts?</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="flex-none rounded-[7px] border border-amber-300/30 bg-amber-300/[0.12] px-3 py-[5px] text-xs font-semibold text-amber-300">Answer</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 rounded-[9px] border border-[rgba(120,53,15,0.55)] bg-[rgba(69,26,3,0.3)] px-[13px] py-[11px]">
|
||||||
|
<span class="h-2 w-2 flex-none animate-pulse-amber rounded-full bg-amber-300"></span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="font-mono text-[13px] text-zinc-200">web · checkout</div>
|
||||||
|
<div class="text-xs text-[#d4a55a]">Run the failing test again?</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="flex-none rounded-[7px] border border-amber-300/30 bg-amber-300/[0.12] px-3 py-[5px] text-xs font-semibold text-amber-300">Answer</button>
|
||||||
|
</div>
|
||||||
|
<div class="mx-0.5 my-[3px] h-px bg-[#1c1c1f]"></div>
|
||||||
|
<div class="flex items-center gap-3 rounded-[9px] px-[13px] py-[9px]">
|
||||||
|
<span class="h-2 w-2 flex-none animate-pulse-sky rounded-full bg-sky-300"></span>
|
||||||
|
<div class="min-w-0 flex-1 font-mono text-[13px] text-zinc-400">db · refactor</div>
|
||||||
|
<span class="flex-none font-mono text-[11px] text-sky-300">{{ t('busy') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 rounded-[9px] px-[13px] py-[9px]">
|
||||||
|
<span class="h-2 w-2 flex-none rounded-full bg-emerald-400"></span>
|
||||||
|
<div class="min-w-0 flex-1 font-mono text-[13px] text-zinc-400">docs · main</div>
|
||||||
|
<span class="flex-none font-mono text-[11px] text-emerald-400">{{ t('idle') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- b) multi-terminal grid -->
|
||||||
|
<div v-reveal class="flex flex-wrap-reverse items-center gap-12">
|
||||||
|
<div class="min-w-[280px] flex-[1_1_420px]">
|
||||||
|
<div class="grid grid-cols-2 gap-2.5 overflow-hidden rounded-xl border border-zinc-800 bg-[#0c0c0e] p-3 shadow-card">
|
||||||
|
<div class="overflow-hidden rounded-[9px] border border-zinc-800 bg-zinc-950">
|
||||||
|
<div class="flex items-center justify-between border-b border-[#1c1c1f] px-[11px] py-2">
|
||||||
|
<span class="font-mono text-[11px] text-zinc-400">api · feat/auth</span>
|
||||||
|
<span class="h-1.5 w-1.5 animate-pulse-sky rounded-full bg-sky-300"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-[11px] font-mono text-[11px] leading-[1.75] text-zinc-300">
|
||||||
|
<div class="text-emerald-400">● Edit session.ts</div>
|
||||||
|
<div class="text-emerald-400">+ const token = sign(...)</div>
|
||||||
|
<div class="text-red-400">- legacy cookie auth</div>
|
||||||
|
<div class="text-zinc-500">refactoring guards…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-[9px] border border-zinc-800 bg-zinc-950">
|
||||||
|
<div class="flex items-center justify-between border-b border-[#1c1c1f] px-[11px] py-2">
|
||||||
|
<span class="font-mono text-[11px] text-zinc-400">web · fix/cart</span>
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-[11px] font-mono text-[11px] leading-[1.75] text-zinc-300">
|
||||||
|
<div class="text-emerald-400">✓ all tests passing</div>
|
||||||
|
<div class="text-zinc-500">24 passed, 0 failed</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-emerald-400">$</span>
|
||||||
|
<span class="ml-1 inline-block h-[11px] w-1.5 animate-blink align-middle bg-zinc-200"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-[9px] border border-zinc-800 bg-zinc-950">
|
||||||
|
<div class="flex items-center justify-between border-b border-[#1c1c1f] px-[11px] py-2">
|
||||||
|
<span class="font-mono text-[11px] text-zinc-400">db · refactor</span>
|
||||||
|
<span class="h-1.5 w-1.5 animate-pulse-sky rounded-full bg-sky-300"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-[11px] font-mono text-[11px] leading-[1.75] text-zinc-300">
|
||||||
|
<div class="text-emerald-400">● Write migration</div>
|
||||||
|
<div class="text-zinc-500">0003_add_index.sql</div>
|
||||||
|
<div class="text-zinc-500">applying to schema…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-[9px] border border-zinc-800 bg-zinc-950">
|
||||||
|
<div class="flex items-center justify-between border-b border-[#1c1c1f] px-[11px] py-2">
|
||||||
|
<span class="font-mono text-[11px] text-zinc-400">docs · api</span>
|
||||||
|
<span class="h-1.5 w-1.5 animate-pulse-sky rounded-full bg-sky-300"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-[11px] font-mono text-[11px] leading-[1.75] text-zinc-300">
|
||||||
|
<div class="text-emerald-400">● Read README.md</div>
|
||||||
|
<div class="text-zinc-500">## Quick start</div>
|
||||||
|
<div class="text-zinc-500">writing examples…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-[280px] flex-[1_1_380px]">
|
||||||
|
<div class="mb-3 font-mono text-xs uppercase tracking-[0.12em] text-emerald-400">{{ t('scBKicker') }}</div>
|
||||||
|
<h2 class="m-0 mb-4 text-[clamp(26px,3vw,34px)] font-semibold leading-[1.15] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('scBTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 text-[16.5px] leading-[1.6] text-zinc-400">{{ t('scBBody') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- c) mobile -->
|
||||||
|
<div v-reveal class="flex flex-wrap items-center gap-12">
|
||||||
|
<div class="min-w-[280px] flex-[1_1_380px]">
|
||||||
|
<div class="mb-3 font-mono text-xs uppercase tracking-[0.12em] text-emerald-400">{{ t('scCKicker') }}</div>
|
||||||
|
<h2 class="m-0 mb-4 text-[clamp(26px,3vw,34px)] font-semibold leading-[1.15] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('scCTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 text-[16.5px] leading-[1.6] text-zinc-400">{{ t('scCBody') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-[280px] flex-[1_1_320px] justify-center">
|
||||||
|
<div class="relative w-[288px] overflow-hidden rounded-[40px] border-[9px] border-zinc-900 bg-zinc-950 shadow-phone">
|
||||||
|
<div class="flex h-[30px] items-center justify-center">
|
||||||
|
<div class="h-1.5 w-[90px] rounded-full bg-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="px-[13px] pb-[22px]">
|
||||||
|
<div class="flex items-start gap-[11px] rounded-[15px] border border-[rgba(120,53,15,0.5)] bg-[#0c0c0e] p-[13px] shadow-toast-sm">
|
||||||
|
<span class="inline-flex h-[30px] w-[30px] flex-none items-center justify-center rounded-lg bg-emerald-500/[0.14]">
|
||||||
|
<img src="/assets/arboretum-mark.png" alt="" width="18" height="18" class="h-[18px] w-[18px] object-contain" />
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-mono text-[11px] text-emerald-400">Arboretum</span>
|
||||||
|
<span class="text-[10px] text-zinc-600">now</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[3px] text-[13.5px] font-semibold text-zinc-50">{{ t('toastTitle') }}</div>
|
||||||
|
<div class="text-xs leading-[1.45] text-zinc-400">{{ t('toastBody') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[14px] overflow-hidden rounded-[15px] border border-zinc-800 bg-zinc-950">
|
||||||
|
<div class="flex items-center gap-2 border-b border-[#1c1c1f] px-[13px] py-[11px]">
|
||||||
|
<span class="h-[7px] w-[7px] animate-pulse-amber rounded-full bg-amber-300"></span>
|
||||||
|
<span class="font-mono text-[11px] text-zinc-300">feat/auth</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-[13px]">
|
||||||
|
<div class="text-[13px] leading-[1.5] text-zinc-300">{{ t('dlgQ') }}</div>
|
||||||
|
<div class="mt-2 rounded-lg border border-[#1c1c1f] bg-[#0c0c0e] p-[9px] font-mono text-[11px] leading-[1.6] text-zinc-500">
|
||||||
|
<div class="text-emerald-400">+ verifyToken(req)</div>
|
||||||
|
<div class="text-red-400">- skipAuth()</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[13px] flex gap-2">
|
||||||
|
<button type="button" class="flex-1 rounded-lg bg-emerald-600 py-2.5 text-[13px] font-semibold text-white">{{ t('approve') }}</button>
|
||||||
|
<button type="button" class="flex-1 rounded-lg border border-zinc-800 bg-transparent py-2.5 text-[13px] font-medium text-zinc-300">{{ t('reject') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
55
packages/site/src/components/WorkGroupsSection.vue
Normal file
55
packages/site/src/components/WorkGroupsSection.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="mx-auto max-w-[1200px] scroll-mt-[84px] px-6 py-[92px]">
|
||||||
|
<div
|
||||||
|
v-reveal
|
||||||
|
class="flex flex-wrap items-center gap-12 rounded-[18px] border border-zinc-800 p-[clamp(28px,4vw,52px)]"
|
||||||
|
style="background: linear-gradient(180deg, rgba(24, 24, 27, 0.55), rgba(12, 12, 14, 0.55))"
|
||||||
|
>
|
||||||
|
<div class="min-w-[280px] flex-[1_1_360px]">
|
||||||
|
<div class="mb-3 font-mono text-xs uppercase tracking-[0.12em] text-emerald-400">{{ t('grpKicker') }}</div>
|
||||||
|
<h2 class="m-0 mb-4 text-[clamp(26px,3vw,36px)] font-semibold leading-[1.15] tracking-[-0.025em] text-zinc-50">
|
||||||
|
{{ t('grpTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 text-[16.5px] leading-[1.6] text-zinc-400">{{ t('grpBody') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-[280px] flex-[1_1_360px] justify-center">
|
||||||
|
<div class="flex w-full max-w-[380px] items-center">
|
||||||
|
<div class="flex flex-none flex-col items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="inline-flex h-[62px] w-[62px] items-center justify-center rounded-[15px] border border-emerald-500/35 bg-emerald-500/[0.12] text-emerald-400"
|
||||||
|
>
|
||||||
|
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /><path d="M7 16.5 12 13.5" /><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /><path d="m17 16.5 4.74-2.85" /><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0z" /></svg>
|
||||||
|
</div>
|
||||||
|
<span class="font-mono text-[11px] text-zinc-300">payments</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-[96px] flex-1">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 100 96" preserveAspectRatio="none" class="absolute inset-0" aria-hidden="true">
|
||||||
|
<path d="M0 48 H40 V14 H100" fill="none" stroke="rgba(16,185,129,.45)" stroke-width="1.4" />
|
||||||
|
<path d="M0 48 H40 V48 H100" fill="none" stroke="rgba(16,185,129,.45)" stroke-width="1.4" />
|
||||||
|
<path d="M0 48 H40 V82 H100" fill="none" stroke="rgba(16,185,129,.45)" stroke-width="1.4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-none flex-col gap-2.5">
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border border-zinc-800 bg-[#0c0c0e] px-3 py-2">
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-sky-300"></span>
|
||||||
|
<span class="font-mono text-[11px] text-zinc-300">api</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border border-zinc-800 bg-[#0c0c0e] px-3 py-2">
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-sky-300"></span>
|
||||||
|
<span class="font-mono text-[11px] text-zinc-300">web</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border border-zinc-800 bg-[#0c0c0e] px-3 py-2">
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-sky-300"></span>
|
||||||
|
<span class="font-mono text-[11px] text-zinc-300">sdk</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
20
packages/site/src/components/icons/IconCopy.vue
Normal file
20
packages/site/src/components/icons/IconCopy.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(defineProps<{ size?: number }>(), { size: 15 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.9"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<rect width="13" height="13" x="9" y="9" rx="2" />
|
||||||
|
<path d="M5 15c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
22
packages/site/src/components/icons/IconGitea.vue
Normal file
22
packages/site/src/components/icons/IconGitea.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(defineProps<{ size?: number }>(), { size: 16 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.7"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M4 10h12v4a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4z" />
|
||||||
|
<path d="M16 11h2.5a2.5 2.5 0 0 1 0 5H16" />
|
||||||
|
<path d="M7 3c-.5.8.5 1.6 0 2.5" />
|
||||||
|
<path d="M11 3c-.5.8.5 1.6 0 2.5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
24
packages/site/src/composables/useCopy.ts
Normal file
24
packages/site/src/composables/useCopy.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const INSTALL_COMMAND = 'npx @johanleroy/git-arboretum';
|
||||||
|
|
||||||
|
// Bouton « copier la commande » : copie + bascule le label 1800 ms (fidèle au design).
|
||||||
|
export function useCopy() {
|
||||||
|
const copied = ref(false);
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
async function copy(text: string = INSTALL_COMMAND): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard) await navigator.clipboard.writeText(text);
|
||||||
|
} catch {
|
||||||
|
/* clipboard indisponible (contexte non sécurisé) */
|
||||||
|
}
|
||||||
|
copied.value = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
copied.value = false;
|
||||||
|
}, 1800);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { copied, copy };
|
||||||
|
}
|
||||||
113
packages/site/src/composables/useHeroTimeline.ts
Normal file
113
packages/site/src/composables/useHeroTimeline.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
// Portage 1:1 de la timeline animée du mockup hero (Arboretum.dc.html).
|
||||||
|
export type SessionState = 'idle' | 'busy' | 'waiting';
|
||||||
|
|
||||||
|
export interface AnimLine {
|
||||||
|
text: string;
|
||||||
|
k: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeroAnim {
|
||||||
|
sess: SessionState;
|
||||||
|
lines: AnimLine[];
|
||||||
|
att: number;
|
||||||
|
toast: boolean;
|
||||||
|
dlg: boolean;
|
||||||
|
hl: number;
|
||||||
|
ans: number;
|
||||||
|
cur: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOTAL = 13000;
|
||||||
|
const TICK = 80;
|
||||||
|
|
||||||
|
interface ScriptEntry {
|
||||||
|
at: number;
|
||||||
|
text: string;
|
||||||
|
k: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function script(): ScriptEntry[] {
|
||||||
|
return [
|
||||||
|
{ at: 850, text: '$ claude --resume feat/auth', k: 'cmd' },
|
||||||
|
{ at: 1400, text: '> Reading src/auth/login.ts', k: 'tool' },
|
||||||
|
{ at: 1950, text: '● Edit login.ts', k: 'edit' },
|
||||||
|
{ at: 2400, text: " + import { verifyToken } from './jwt'", k: 'add' },
|
||||||
|
{ at: 2850, text: ' + const user = await verifyToken(req)', k: 'add' },
|
||||||
|
{ at: 3350, text: '● Bash npm test -- auth', k: 'edit' },
|
||||||
|
{ at: 3850, text: ' ✓ 14 passed (1.24s)', k: 'ok' },
|
||||||
|
{ at: 4300, text: '● Permission required to edit login.ts', k: 'warn' },
|
||||||
|
{ at: 7950, text: '✓ approved · applying change', k: 'ok' },
|
||||||
|
{ at: 8500, text: '● Edit login.ts', k: 'edit' },
|
||||||
|
{ at: 9050, text: '● Running…', k: 'run' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveAnim(clock: number): HeroAnim {
|
||||||
|
const sess: SessionState = clock < 700 ? 'idle' : clock < 4500 ? 'busy' : clock < 7700 ? 'waiting' : 'busy';
|
||||||
|
const sc = script();
|
||||||
|
const lines: AnimLine[] = [];
|
||||||
|
for (const entry of sc) {
|
||||||
|
if (clock >= entry.at) lines.push({ text: entry.text, k: entry.k });
|
||||||
|
}
|
||||||
|
const att = clock >= 4500 && clock < 7700 ? 1 : 0;
|
||||||
|
const toast = clock >= 4650 && clock < 7700;
|
||||||
|
const dlg = clock >= 4600 && clock < 7700;
|
||||||
|
const hl = clock >= 6000 && clock < 7050 ? 0 : -1;
|
||||||
|
const ans = clock >= 7050 && clock < 7700 ? 0 : -1;
|
||||||
|
const cur = sess !== 'waiting';
|
||||||
|
return { sess, lines, att, toast, dlg, hl, ans, cur };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHeroTimeline(stageId = 'hero-stage') {
|
||||||
|
const anim = ref<HeroAnim>(deriveAnim(0));
|
||||||
|
let clock = 0;
|
||||||
|
let visible = true;
|
||||||
|
let everVisible = false;
|
||||||
|
let timer: ReturnType<typeof setInterval> | undefined;
|
||||||
|
let io: IntersectionObserver | undefined;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let reduced = false;
|
||||||
|
try {
|
||||||
|
reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
if (reduced) {
|
||||||
|
anim.value = deriveAnim(5200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stage = document.getElementById(stageId);
|
||||||
|
if (stage && 'IntersectionObserver' in window) {
|
||||||
|
io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const e = entries[0];
|
||||||
|
if (!e) return;
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
visible = true;
|
||||||
|
everVisible = true;
|
||||||
|
} else if (everVisible) {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0, rootMargin: '200px 0px 200px 0px' },
|
||||||
|
);
|
||||||
|
io.observe(stage);
|
||||||
|
}
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
clock += TICK;
|
||||||
|
if (clock >= TOTAL) clock = 0;
|
||||||
|
anim.value = deriveAnim(clock);
|
||||||
|
}, TICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
if (io) io.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { anim };
|
||||||
|
}
|
||||||
52
packages/site/src/composables/useScrollReveal.ts
Normal file
52
packages/site/src/composables/useScrollReveal.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Directive } from 'vue';
|
||||||
|
|
||||||
|
// Directive `v-reveal` : révèle un bloc quand il entre dans le viewport.
|
||||||
|
// Reprend l'intention du setupReveal() du design (seuil ~92% de la hauteur),
|
||||||
|
// via un IntersectionObserver partagé. Respecte prefers-reduced-motion.
|
||||||
|
let reducedMotion = false;
|
||||||
|
try {
|
||||||
|
reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
} catch {
|
||||||
|
/* matchMedia indisponible */
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
function getObserver(): IntersectionObserver | null {
|
||||||
|
if (typeof IntersectionObserver === 'undefined') return null;
|
||||||
|
if (!observer) {
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('is-visible');
|
||||||
|
observer?.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// -8% en bas ≈ déclenche quand le haut du bloc franchit ~92% du viewport.
|
||||||
|
{ threshold: 0, rootMargin: '0px 0px -8% 0px' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reveal: Directive<HTMLElement> = {
|
||||||
|
mounted(el) {
|
||||||
|
el.classList.add('reveal');
|
||||||
|
if (reducedMotion) {
|
||||||
|
el.classList.add('is-visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const io = getObserver();
|
||||||
|
if (!io) {
|
||||||
|
// Pas d'IntersectionObserver : on révèle immédiatement (pas de contenu caché).
|
||||||
|
el.classList.add('is-visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
io.observe(el);
|
||||||
|
},
|
||||||
|
unmounted(el) {
|
||||||
|
observer?.unobserve(el);
|
||||||
|
},
|
||||||
|
};
|
||||||
7
packages/site/src/env.d.ts
vendored
Normal file
7
packages/site/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue';
|
||||||
|
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
60
packages/site/src/i18n/content.ts
Normal file
60
packages/site/src/i18n/content.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Listes structurées (FAQ, options de dialogue) — gardées hors des messages
|
||||||
|
// vue-i18n et indexées par locale, pour rester du texte pur typé (pas d'innerHTML).
|
||||||
|
import type { AppLocale } from './index';
|
||||||
|
|
||||||
|
export interface FaqItem {
|
||||||
|
q: string;
|
||||||
|
a: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FAQS: Record<AppLocale, FaqItem[]> = {
|
||||||
|
en: [
|
||||||
|
{
|
||||||
|
q: 'Is Arboretum an IDE?',
|
||||||
|
a: "No — it's a command center. It doesn't replace your editor; it supervises the AI coding agents working across your repos: spawning sessions, surfacing their state, and letting you answer them.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Does it need the cloud?',
|
||||||
|
a: 'No. Arboretum is local-first. The daemon runs on your own machine and binds to 127.0.0.1 by default. Your code and sessions never leave your network unless you choose to expose the dashboard.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'How do I access it remotely?',
|
||||||
|
a: 'Through Tailscale Serve. Expose the dashboard to your tailnet and reach it securely from any device — no port forwarding, no public endpoint.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Does it work on mobile?',
|
||||||
|
a: 'Yes. The dashboard is an installable PWA. Add it to your home screen, get Web Push the moment a session needs you, and approve or reject a prompt with a single tap.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Is my code safe?',
|
||||||
|
a: 'Arboretum binds to localhost, hashes access tokens at rest, and enforces strict origin checks on every request. Remote access runs through Tailscale rather than an open port.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fr: [
|
||||||
|
{
|
||||||
|
q: 'Arboretum est-il un IDE ?',
|
||||||
|
a: "Non — c'est un poste de commandement. Il ne remplace pas votre éditeur ; il supervise les agents de code IA qui travaillent sur vos repos : lancement des sessions, affichage de leur état, et réponse à leurs questions.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'A-t-il besoin du cloud ?',
|
||||||
|
a: 'Non. Arboretum est local-first. Le daemon tourne sur votre propre machine et se lie à 127.0.0.1 par défaut. Votre code et vos sessions ne quittent jamais votre réseau sauf si vous exposez le dashboard.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Comment y accéder à distance ?',
|
||||||
|
a: "Via Tailscale Serve. Exposez le dashboard à votre tailnet et accédez-y en sécurité depuis n'importe quel appareil — sans redirection de port ni endpoint public.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Fonctionne-t-il sur mobile ?',
|
||||||
|
a: "Oui. Le dashboard est une PWA installable. Ajoutez-le à l'écran d'accueil, recevez un Web Push dès qu'une session a besoin de vous, et approuvez ou refusez une question d'un toucher.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Mon code est-il en sécurité ?',
|
||||||
|
a: "Arboretum se lie à localhost, hashe les tokens au repos, et applique une vérification d'origine stricte sur chaque requête. L'accès distant passe par Tailscale plutôt qu'un port ouvert.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DLG_OPTS: Record<AppLocale, string[]> = {
|
||||||
|
en: ['Yes, apply the change', "Yes, and don't ask again", 'No, keep current version'],
|
||||||
|
fr: ['Oui, appliquer le changement', 'Oui, et ne plus demander', 'Non, garder la version actuelle'],
|
||||||
|
};
|
||||||
93
packages/site/src/i18n/en.ts
Normal file
93
packages/site/src/i18n/en.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Messages EN — copie fidèle du dict() du design Arboretum.dc.html.
|
||||||
|
export default {
|
||||||
|
navFeatures: 'Features',
|
||||||
|
navHow: 'How it works',
|
||||||
|
navSecurity: 'Security',
|
||||||
|
navFaq: 'FAQ',
|
||||||
|
gitea: 'View on Gitea',
|
||||||
|
heroBadge: 'Mission control for AI coding agents',
|
||||||
|
heroTitle: 'Mission control for your AI coding agents',
|
||||||
|
heroSub:
|
||||||
|
'Run many Claude Code sessions across every git worktree — and supervise them from any device, even your phone.',
|
||||||
|
getStarted: 'Get started',
|
||||||
|
probKicker: 'The problem',
|
||||||
|
probTitle: "Many agents across worktrees is chaos — and you're chained to one terminal.",
|
||||||
|
probBody:
|
||||||
|
'Sessions scattered across tabs. No idea which agent is waiting on you. Step away from the desk and everything stalls. Arboretum is the single pane of glass.',
|
||||||
|
featKicker: 'Everything in one view',
|
||||||
|
featTitle: 'Built to supervise agents — not to be an IDE',
|
||||||
|
feat1Title: 'Worktree-first',
|
||||||
|
feat1Desc: 'Every session is pinned to a git worktree. You see branches, not chaos.',
|
||||||
|
feat2Title: 'Many sessions, one view',
|
||||||
|
feat2Desc: 'Run parallel Claude Code agents side by side in a single live grid.',
|
||||||
|
feat3Title: 'Fine-grained states',
|
||||||
|
feat3Desc: 'tell at a glance what each agent is doing.',
|
||||||
|
feat4Title: 'Supervise anywhere',
|
||||||
|
feat4Desc: 'Installable PWA. Check in from your laptop, tablet, or phone.',
|
||||||
|
feat5Title: 'Web Push alerts',
|
||||||
|
feat5Desc: 'Get pinged the moment a session needs a decision from you.',
|
||||||
|
feat6Title: 'Answer without a terminal',
|
||||||
|
feat6Desc: 'Respond to agent prompts and dialogs right from the dashboard.',
|
||||||
|
feat7Title: 'Work groups',
|
||||||
|
feat7Desc: 'Group repos and launch the same feature across all of them at once.',
|
||||||
|
feat8Title: 'Local-first & secure',
|
||||||
|
feat8Desc: 'Binds to localhost, hashes tokens, remote access via Tailscale.',
|
||||||
|
scAKicker: 'Worktree-first dashboard',
|
||||||
|
scATitle: 'See what needs you — before anything stalls',
|
||||||
|
scABody:
|
||||||
|
'A live list of every worktree and its session state. The Needs attention rail floats the agents waiting on a decision to the top, so nothing sits blocked while you are heads-down elsewhere.',
|
||||||
|
scBKicker: 'Multi-terminal grid',
|
||||||
|
scBTitle: "Every agent's output, streaming live",
|
||||||
|
scBBody:
|
||||||
|
'Watch all your sessions at once — real terminal output, color-coded by state, updating in real time. Click any pane to take over, answer a prompt, or stop a run.',
|
||||||
|
scCKicker: 'On your phone',
|
||||||
|
scCTitle: 'Unblock an agent from the kitchen',
|
||||||
|
scCBody:
|
||||||
|
'Install the PWA and get a Web Push the second a session needs you. Tap the notification, read the prompt, answer the dialog — your agent keeps moving while you are away.',
|
||||||
|
grpKicker: 'Work groups · cross-repo',
|
||||||
|
grpTitle: 'Ship one feature across every repo at once',
|
||||||
|
grpBody:
|
||||||
|
'Group the repos that move together — a service, its client, its docs. Launch the same task to all of them and Arboretum spins up a worktree and an agent in each, then tracks them as one unit.',
|
||||||
|
howKicker: 'How it works',
|
||||||
|
howTitle: 'Zero install. Three steps.',
|
||||||
|
step1Title: 'Launch the daemon',
|
||||||
|
step1Desc: 'One command. No install, no config, no account.',
|
||||||
|
step2Title: 'Open the dashboard',
|
||||||
|
step2Desc: 'It serves a web dashboard on localhost. Install it as a PWA on any device.',
|
||||||
|
step3Title: 'Spawn & supervise',
|
||||||
|
step3Desc: 'Start Claude Code sessions on any worktree and watch them from anywhere.',
|
||||||
|
secKicker: 'Security',
|
||||||
|
secTitle: 'A web terminal you can actually trust',
|
||||||
|
secIntro: 'Built local-first. Nothing is exposed unless you choose to — and even then, on your terms.',
|
||||||
|
sec1Title: 'Localhost by default',
|
||||||
|
sec1Desc: 'Binds to 127.0.0.1. Nothing leaves your machine unless you say so.',
|
||||||
|
sec2Title: 'Hashed tokens',
|
||||||
|
sec2Desc: 'Access tokens are hashed (sha256) at rest. No plaintext secrets on disk.',
|
||||||
|
sec3Title: 'Strict origin checks',
|
||||||
|
sec3Desc: 'Every request is origin-validated. No cross-site surprises.',
|
||||||
|
sec4Title: 'Remote via Tailscale',
|
||||||
|
sec4Desc: 'Reach it over Tailscale Serve — no public port, no port forwarding.',
|
||||||
|
faqTitle: 'Questions, answered',
|
||||||
|
ctaTitle: 'Take command of your agents',
|
||||||
|
ctaBody: 'One command to launch. Supervise everything from one place.',
|
||||||
|
ctaSource: 'View the source on Gitea',
|
||||||
|
footTag: 'a garden of branches, one pane of glass.',
|
||||||
|
license: 'MIT License',
|
||||||
|
coffee: 'Buy me a coffee',
|
||||||
|
mDash: 'Dashboard',
|
||||||
|
mGroups: 'Groups',
|
||||||
|
mSettings: 'Settings',
|
||||||
|
mHelp: 'Help',
|
||||||
|
mSearch: 'Search',
|
||||||
|
mAttn: 'Needs attention',
|
||||||
|
waiting: 'waiting',
|
||||||
|
busy: 'busy',
|
||||||
|
idle: 'idle',
|
||||||
|
approve: 'Approve',
|
||||||
|
reject: 'Reject',
|
||||||
|
toastTitle: 'Session waiting for you',
|
||||||
|
toastBody: 'api · feat/auth needs a decision',
|
||||||
|
dlgQ: 'Apply this change to login.ts?',
|
||||||
|
copy: 'Copy',
|
||||||
|
copied: 'Copied',
|
||||||
|
};
|
||||||
93
packages/site/src/i18n/fr.ts
Normal file
93
packages/site/src/i18n/fr.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Messages FR — copie fidèle du dict() du design Arboretum.dc.html.
|
||||||
|
export default {
|
||||||
|
navFeatures: 'Fonctionnalités',
|
||||||
|
navHow: 'Comment ça marche',
|
||||||
|
navSecurity: 'Sécurité',
|
||||||
|
navFaq: 'FAQ',
|
||||||
|
gitea: 'Voir sur Gitea',
|
||||||
|
heroBadge: 'Poste de commandement pour agents de code IA',
|
||||||
|
heroTitle: 'Le poste de commandement de vos agents de code IA',
|
||||||
|
heroSub:
|
||||||
|
"Lancez plusieurs sessions Claude Code à travers chaque worktree git — et supervisez-les depuis n'importe quel appareil, même votre téléphone.",
|
||||||
|
getStarted: 'Commencer',
|
||||||
|
probKicker: 'Le problème',
|
||||||
|
probTitle: "Plein d'agents à travers les worktrees, c'est le chaos — et vous êtes cloué à un seul terminal.",
|
||||||
|
probBody:
|
||||||
|
"Des sessions éparpillées dans des onglets. Aucune idée de quel agent vous attend. Vous quittez le bureau et tout se fige. Arboretum, c'est la vitre unique.",
|
||||||
|
featKicker: 'Tout dans une seule vue',
|
||||||
|
featTitle: 'Conçu pour superviser les agents — pas pour être un IDE',
|
||||||
|
feat1Title: "Worktree d'abord",
|
||||||
|
feat1Desc: 'Chaque session est rattachée à un worktree git. Vous voyez des branches, pas du chaos.',
|
||||||
|
feat2Title: 'Plusieurs sessions, une vue',
|
||||||
|
feat2Desc: 'Lancez des agents Claude Code en parallèle, côte à côte dans une seule grille en direct.',
|
||||||
|
feat3Title: 'États fins',
|
||||||
|
feat3Desc: "sachez d'un coup d'œil ce que fait chaque agent.",
|
||||||
|
feat4Title: 'Supervisez partout',
|
||||||
|
feat4Desc: "PWA installable. Surveillez depuis l'ordinateur, la tablette ou le téléphone.",
|
||||||
|
feat5Title: 'Alertes Web Push',
|
||||||
|
feat5Desc: "Soyez prévenu dès qu'une session attend une décision.",
|
||||||
|
feat6Title: 'Répondre sans terminal',
|
||||||
|
feat6Desc: 'Répondez aux questions et dialogues des agents directement depuis le dashboard.',
|
||||||
|
feat7Title: 'Work groups',
|
||||||
|
feat7Desc: 'Regroupez des repos et lancez la même feature dans tous à la fois.',
|
||||||
|
feat8Title: 'Local-first & sécurisé',
|
||||||
|
feat8Desc: 'Se lie à localhost, hashe les tokens, accès distant via Tailscale.',
|
||||||
|
scAKicker: 'Dashboard worktree-first',
|
||||||
|
scATitle: 'Voyez ce qui vous attend — avant que ça ne bloque',
|
||||||
|
scABody:
|
||||||
|
"Une liste en direct de chaque worktree et de l'état de sa session. Le bandeau À traiter fait remonter les agents en attente d'une décision, pour que rien ne reste bloqué pendant que vous êtes concentré ailleurs.",
|
||||||
|
scBKicker: 'Grille multi-terminaux',
|
||||||
|
scBTitle: 'La sortie de chaque agent, en direct',
|
||||||
|
scBBody:
|
||||||
|
"Surveillez toutes vos sessions en même temps — vraie sortie terminal, couleur selon l'état, mise à jour en temps réel. Cliquez sur un panneau pour reprendre la main, répondre, ou arrêter une exécution.",
|
||||||
|
scCKicker: 'Sur votre téléphone',
|
||||||
|
scCTitle: 'Débloquez un agent depuis la cuisine',
|
||||||
|
scCBody:
|
||||||
|
"Installez la PWA et recevez un Web Push dès qu'une session a besoin de vous. Touchez la notification, lisez la question, répondez au dialogue — votre agent continue pendant que vous êtes loin du bureau.",
|
||||||
|
grpKicker: 'Work groups · cross-repo',
|
||||||
|
grpTitle: "Livrez une feature dans tous les repos d'un coup",
|
||||||
|
grpBody:
|
||||||
|
'Regroupez les repos qui avancent ensemble — un service, son client, sa doc. Lancez la même tâche à tous ; Arboretum crée un worktree et un agent dans chacun, puis les suit comme un seul ensemble.',
|
||||||
|
howKicker: 'Comment ça marche',
|
||||||
|
howTitle: 'Zéro install. Trois étapes.',
|
||||||
|
step1Title: 'Lancez le daemon',
|
||||||
|
step1Desc: "Une commande. Pas d'install, pas de config, pas de compte.",
|
||||||
|
step2Title: 'Ouvrez le dashboard',
|
||||||
|
step2Desc: "Il sert un dashboard web sur localhost. Installez-le en PWA sur n'importe quel appareil.",
|
||||||
|
step3Title: 'Lancez & supervisez',
|
||||||
|
step3Desc: "Démarrez des sessions Claude Code sur n'importe quel worktree et surveillez-les de partout.",
|
||||||
|
secKicker: 'Sécurité',
|
||||||
|
secTitle: 'Un terminal web en qui vous pouvez avoir confiance',
|
||||||
|
secIntro: "Conçu local-first. Rien n'est exposé sauf si vous le décidez — et toujours selon vos règles.",
|
||||||
|
sec1Title: 'Localhost par défaut',
|
||||||
|
sec1Desc: 'Se lie à 127.0.0.1. Rien ne quitte votre machine sans votre accord.',
|
||||||
|
sec2Title: 'Tokens hashés',
|
||||||
|
sec2Desc: 'Les tokens sont hashés (sha256) au repos. Aucun secret en clair sur le disque.',
|
||||||
|
sec3Title: "Vérification d'origine stricte",
|
||||||
|
sec3Desc: 'Chaque requête est validée par origine. Pas de surprise cross-site.',
|
||||||
|
sec4Title: 'Distant via Tailscale',
|
||||||
|
sec4Desc: 'Accédez-y via Tailscale Serve — aucun port public, aucune redirection de port.',
|
||||||
|
faqTitle: 'Vos questions, nos réponses',
|
||||||
|
ctaTitle: 'Prenez le commandement de vos agents',
|
||||||
|
ctaBody: 'Une commande pour lancer. Tout superviser depuis un seul endroit.',
|
||||||
|
ctaSource: 'Voir le code sur Gitea',
|
||||||
|
footTag: 'un jardin de branches, une seule vitre.',
|
||||||
|
license: 'Licence MIT',
|
||||||
|
coffee: 'Paye-moi un café',
|
||||||
|
mDash: 'Tableau de bord',
|
||||||
|
mGroups: 'Groupes',
|
||||||
|
mSettings: 'Réglages',
|
||||||
|
mHelp: 'Aide',
|
||||||
|
mSearch: 'Rechercher',
|
||||||
|
mAttn: 'À traiter',
|
||||||
|
waiting: 'en attente',
|
||||||
|
busy: 'occupé',
|
||||||
|
idle: 'au repos',
|
||||||
|
approve: 'Approuver',
|
||||||
|
reject: 'Refuser',
|
||||||
|
toastTitle: 'Une session vous attend',
|
||||||
|
toastBody: 'api · feat/auth attend une décision',
|
||||||
|
dlgQ: 'Appliquer ce changement à login.ts ?',
|
||||||
|
copy: 'Copier',
|
||||||
|
copied: 'Copié',
|
||||||
|
};
|
||||||
37
packages/site/src/i18n/index.ts
Normal file
37
packages/site/src/i18n/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import en from './en';
|
||||||
|
import fr from './fr';
|
||||||
|
|
||||||
|
export type AppLocale = 'en' | 'fr';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'arb-lang';
|
||||||
|
|
||||||
|
// Détection fidèle au design : localStorage prioritaire, sinon langue navigateur
|
||||||
|
// (fr → FR, sinon EN).
|
||||||
|
function initialLocale(): AppLocale {
|
||||||
|
let saved: string | null = null;
|
||||||
|
try {
|
||||||
|
saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
/* localStorage indisponible (mode privé, etc.) */
|
||||||
|
}
|
||||||
|
if (saved === 'fr' || saved === 'en') return saved;
|
||||||
|
const nav = (typeof navigator !== 'undefined' && navigator.language ? navigator.language : '').toLowerCase();
|
||||||
|
return nav.indexOf('fr') === 0 ? 'fr' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: initialLocale(),
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: { en, fr },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function setLocale(locale: AppLocale): void {
|
||||||
|
i18n.global.locale.value = locale;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, locale);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/site/src/main.ts
Normal file
12
packages/site/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
// Polices self-host (subset latin) — bundlées par Vite, aucune requête Google Fonts.
|
||||||
|
import '@fontsource/jetbrains-mono/latin-400.css';
|
||||||
|
import '@fontsource/jetbrains-mono/latin-500.css';
|
||||||
|
import '@fontsource/jetbrains-mono/latin-600.css';
|
||||||
|
import '@fontsource/jetbrains-mono/latin-700.css';
|
||||||
|
import App from './App.vue';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
import { reveal } from './composables/useScrollReveal';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
createApp(App).use(i18n).directive('reveal', reveal).mount('#app');
|
||||||
120
packages/site/src/style.css
Normal file
120
packages/site/src/style.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* La palette du design correspond à zinc/emerald/amber/sky/red de Tailwind.
|
||||||
|
On ajoute seulement les tokens hors échelle : ombres, police mono, animations. */
|
||||||
|
@theme {
|
||||||
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
|
||||||
|
--shadow-hero: 0 30px 80px -30px rgb(0 0 0 / 0.85);
|
||||||
|
--shadow-card: 0 24px 60px -30px rgb(0 0 0 / 0.8);
|
||||||
|
--shadow-phone: 0 30px 70px -28px rgb(0 0 0 / 0.9);
|
||||||
|
--shadow-toast: 0 12px 32px -12px rgb(0 0 0 / 0.7);
|
||||||
|
--shadow-toast-sm: 0 10px 24px -12px rgb(0 0 0 / 0.6);
|
||||||
|
|
||||||
|
--animate-blink: blink 1.05s steps(1) infinite;
|
||||||
|
--animate-pulse-sky: pulseSky 2s ease-in-out infinite;
|
||||||
|
--animate-pulse-amber: pulseAmber 2.2s ease-in-out infinite;
|
||||||
|
--animate-toast-in: toastIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||||
|
--animate-dlg-in: dlgIn 0.35s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JetBrains Mono (Open Font License) : @font-face fournis par @fontsource,
|
||||||
|
importés dans main.ts et bundlés par Vite (self-host, pas de CDN Google). */
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #09090b;
|
||||||
|
color: #f4f4f5;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background: rgba(16, 185, 129, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
48% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
49%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes pulseSky {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(125, 211, 252, 0.55);
|
||||||
|
}
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(125, 211, 252, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes pulseAmber {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(252, 211, 77, 0.55);
|
||||||
|
}
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(252, 211, 77, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes toastIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes dlgIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/* Tailwind v4 ne met plus cursor:pointer sur les boutons → on le rétablit. */
|
||||||
|
button:not(:disabled),
|
||||||
|
[role='button']:not([aria-disabled='true']) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Révélation au scroll (directive v-reveal) */
|
||||||
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(22px);
|
||||||
|
transition:
|
||||||
|
opacity 0.7s ease,
|
||||||
|
transform 0.7s ease;
|
||||||
|
}
|
||||||
|
.reveal.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
.reveal {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/site/tsconfig.json
Normal file
16
packages/site/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"useDefineForClassFields": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
|
||||||
|
}
|
||||||
15
packages/site/vite.config.ts
Normal file
15
packages/site/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
// Site vitrine statique (git-arboretum.com) : aucune dépendance au daemon, donc
|
||||||
|
// pas de proxy /api /ws (contrairement à packages/web). `base: '/'` car servi
|
||||||
|
// à la racine d'un domaine dédié.
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), tailwindcss()],
|
||||||
|
base: '/',
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
target: 'es2020',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user