feat: Add packages/site — landing vitrine (Vue 3 + Tailwind 4)

- Scaffold packages/site with 11 Vue components: hero (animated terminal), features, showcase, work groups, how-it-works, security, FAQ accordion, footer
- i18n EN/FR via vue-i18n with 82+ keys, autodetect navigator.language, toggle without reload
- Animated timeline (terminal, dialog, toast) ported from design, scroll-reveal, copy buttons (2 instances)
- Self-hosted JetBrains Mono fonts (@fontsource), optimized assets (mark 28KB, og-cover 1200×630)
- SEO: canonical, og:image, theme-color, robots.txt, sitemap.xml for git-arboretum.com
- Monorepo integration (decoupled from npm release): build:site / dev:site / preview:site scripts
- CI: add build:site step to ci.yml for early detection; create .gitea/workflows/prod.yml for FTPS deploy (lftp mirror) to git-arboretum.com
- Validated builds ✓ and UI rendering (EN/FR headless test) ✓
This commit is contained in:
2026-06-19 10:02:22 +02:00
parent 8d3a47bd91
commit 3b251c8174
41 changed files with 1923 additions and 1 deletions

View 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>