diff --git a/.gitea/workflows/prod.yml b/.gitea/workflows/prod.yml new file mode 100644 index 0000000..5322212 --- /dev/null +++ b/.gitea/workflows/prod.yml @@ -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 }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29a31e8..5fa2b8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: - run: npm ci - run: npm run typecheck - run: npm run build + - run: npm run build:site - run: npx vitest run pack-smoke: diff --git a/package.json b/package.json index ff86b98..299742f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "pack": "npm run build && npm pack -w @johanleroy/git-arboretum", "test": "vitest run", "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": { "@types/node": "^22.10.0", diff --git a/packages/site/index.html b/packages/site/index.html new file mode 100644 index 0000000..1ff6e15 --- /dev/null +++ b/packages/site/index.html @@ -0,0 +1,53 @@ + + + + + + + + + Arboretum — Mission control for your AI coding agents + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/packages/site/package.json b/packages/site/package.json new file mode 100644 index 0000000..5c157e7 --- /dev/null +++ b/packages/site/package.json @@ -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" + } +} diff --git a/packages/site/public/apple-touch-icon.png b/packages/site/public/apple-touch-icon.png new file mode 100644 index 0000000..c975719 Binary files /dev/null and b/packages/site/public/apple-touch-icon.png differ diff --git a/packages/site/public/assets/arboretum-mark.png b/packages/site/public/assets/arboretum-mark.png new file mode 100644 index 0000000..2df562c Binary files /dev/null and b/packages/site/public/assets/arboretum-mark.png differ diff --git a/packages/site/public/assets/og-cover.png b/packages/site/public/assets/og-cover.png new file mode 100644 index 0000000..fddcc01 Binary files /dev/null and b/packages/site/public/assets/og-cover.png differ diff --git a/packages/site/public/favicon.ico b/packages/site/public/favicon.ico new file mode 100644 index 0000000..94b9ac9 Binary files /dev/null and b/packages/site/public/favicon.ico differ diff --git a/packages/site/public/icon.svg b/packages/site/public/icon.svg new file mode 100644 index 0000000..12a05d7 --- /dev/null +++ b/packages/site/public/icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/site/public/robots.txt b/packages/site/public/robots.txt new file mode 100644 index 0000000..4f2de61 --- /dev/null +++ b/packages/site/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://git-arboretum.com/sitemap.xml diff --git a/packages/site/public/sitemap.xml b/packages/site/public/sitemap.xml new file mode 100644 index 0000000..080d1bb --- /dev/null +++ b/packages/site/public/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://git-arboretum.com/ + 2026-06-19 + monthly + 1.0 + + diff --git a/packages/site/src/App.vue b/packages/site/src/App.vue new file mode 100644 index 0000000..98414d7 --- /dev/null +++ b/packages/site/src/App.vue @@ -0,0 +1,74 @@ + + + diff --git a/packages/site/src/components/AppFooter.vue b/packages/site/src/components/AppFooter.vue new file mode 100644 index 0000000..b424c9c --- /dev/null +++ b/packages/site/src/components/AppFooter.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/site/src/components/AppHeader.vue b/packages/site/src/components/AppHeader.vue new file mode 100644 index 0000000..924500a --- /dev/null +++ b/packages/site/src/components/AppHeader.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/site/src/components/FaqSection.vue b/packages/site/src/components/FaqSection.vue new file mode 100644 index 0000000..0ce72b4 --- /dev/null +++ b/packages/site/src/components/FaqSection.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/site/src/components/FeatureCard.vue b/packages/site/src/components/FeatureCard.vue new file mode 100644 index 0000000..677f179 --- /dev/null +++ b/packages/site/src/components/FeatureCard.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/site/src/components/FeaturesSection.vue b/packages/site/src/components/FeaturesSection.vue new file mode 100644 index 0000000..3a09aeb --- /dev/null +++ b/packages/site/src/components/FeaturesSection.vue @@ -0,0 +1,76 @@ + + + diff --git a/packages/site/src/components/FinalCta.vue b/packages/site/src/components/FinalCta.vue new file mode 100644 index 0000000..085812b --- /dev/null +++ b/packages/site/src/components/FinalCta.vue @@ -0,0 +1,61 @@ + + + diff --git a/packages/site/src/components/HeroMockup.vue b/packages/site/src/components/HeroMockup.vue new file mode 100644 index 0000000..1cc4da0 --- /dev/null +++ b/packages/site/src/components/HeroMockup.vue @@ -0,0 +1,212 @@ + + + diff --git a/packages/site/src/components/HeroSection.vue b/packages/site/src/components/HeroSection.vue new file mode 100644 index 0000000..56d1201 --- /dev/null +++ b/packages/site/src/components/HeroSection.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/site/src/components/HowItWorksSection.vue b/packages/site/src/components/HowItWorksSection.vue new file mode 100644 index 0000000..403156f --- /dev/null +++ b/packages/site/src/components/HowItWorksSection.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/site/src/components/LangToggle.vue b/packages/site/src/components/LangToggle.vue new file mode 100644 index 0000000..46fbdc1 --- /dev/null +++ b/packages/site/src/components/LangToggle.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/site/src/components/ProblemSection.vue b/packages/site/src/components/ProblemSection.vue new file mode 100644 index 0000000..e9f2c2c --- /dev/null +++ b/packages/site/src/components/ProblemSection.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/site/src/components/SecuritySection.vue b/packages/site/src/components/SecuritySection.vue new file mode 100644 index 0000000..5366d0b --- /dev/null +++ b/packages/site/src/components/SecuritySection.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/site/src/components/ShowcaseSection.vue b/packages/site/src/components/ShowcaseSection.vue new file mode 100644 index 0000000..23ae01d --- /dev/null +++ b/packages/site/src/components/ShowcaseSection.vue @@ -0,0 +1,170 @@ + + + diff --git a/packages/site/src/components/WorkGroupsSection.vue b/packages/site/src/components/WorkGroupsSection.vue new file mode 100644 index 0000000..05ba752 --- /dev/null +++ b/packages/site/src/components/WorkGroupsSection.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/site/src/components/icons/IconCopy.vue b/packages/site/src/components/icons/IconCopy.vue new file mode 100644 index 0000000..66b129d --- /dev/null +++ b/packages/site/src/components/icons/IconCopy.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/site/src/components/icons/IconGitea.vue b/packages/site/src/components/icons/IconGitea.vue new file mode 100644 index 0000000..38b9835 --- /dev/null +++ b/packages/site/src/components/icons/IconGitea.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/site/src/composables/useCopy.ts b/packages/site/src/composables/useCopy.ts new file mode 100644 index 0000000..2bb8662 --- /dev/null +++ b/packages/site/src/composables/useCopy.ts @@ -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 | undefined; + + async function copy(text: string = INSTALL_COMMAND): Promise { + 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 }; +} diff --git a/packages/site/src/composables/useHeroTimeline.ts b/packages/site/src/composables/useHeroTimeline.ts new file mode 100644 index 0000000..b142195 --- /dev/null +++ b/packages/site/src/composables/useHeroTimeline.ts @@ -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(deriveAnim(0)); + let clock = 0; + let visible = true; + let everVisible = false; + let timer: ReturnType | 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 }; +} diff --git a/packages/site/src/composables/useScrollReveal.ts b/packages/site/src/composables/useScrollReveal.ts new file mode 100644 index 0000000..c015f0a --- /dev/null +++ b/packages/site/src/composables/useScrollReveal.ts @@ -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 = { + 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); + }, +}; diff --git a/packages/site/src/env.d.ts b/packages/site/src/env.d.ts new file mode 100644 index 0000000..51c682d --- /dev/null +++ b/packages/site/src/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent, Record, unknown>; + export default component; +} diff --git a/packages/site/src/i18n/content.ts b/packages/site/src/i18n/content.ts new file mode 100644 index 0000000..7b08bcc --- /dev/null +++ b/packages/site/src/i18n/content.ts @@ -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 = { + 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 = { + 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'], +}; diff --git a/packages/site/src/i18n/en.ts b/packages/site/src/i18n/en.ts new file mode 100644 index 0000000..3b3d672 --- /dev/null +++ b/packages/site/src/i18n/en.ts @@ -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', +}; diff --git a/packages/site/src/i18n/fr.ts b/packages/site/src/i18n/fr.ts new file mode 100644 index 0000000..68fc6a8 --- /dev/null +++ b/packages/site/src/i18n/fr.ts @@ -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é', +}; diff --git a/packages/site/src/i18n/index.ts b/packages/site/src/i18n/index.ts new file mode 100644 index 0000000..e7811a7 --- /dev/null +++ b/packages/site/src/i18n/index.ts @@ -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 */ + } +} diff --git a/packages/site/src/main.ts b/packages/site/src/main.ts new file mode 100644 index 0000000..e460191 --- /dev/null +++ b/packages/site/src/main.ts @@ -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'); diff --git a/packages/site/src/style.css b/packages/site/src/style.css new file mode 100644 index 0000000..37d2f6d --- /dev/null +++ b/packages/site/src/style.css @@ -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; + } +} diff --git a/packages/site/tsconfig.json b/packages/site/tsconfig.json new file mode 100644 index 0000000..eed9011 --- /dev/null +++ b/packages/site/tsconfig.json @@ -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"] +} diff --git a/packages/site/vite.config.ts b/packages/site/vite.config.ts new file mode 100644 index 0000000..2f168a4 --- /dev/null +++ b/packages/site/vite.config.ts @@ -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', + }, +});