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

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

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

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

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

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

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>

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

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

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

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

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

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

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

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

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