Compare commits
2
Commits
c8d30c7b0d
...
9624270d9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9624270d9b | ||
|
|
c8bf6534e0 |
@@ -4,8 +4,8 @@
|
|||||||
# Usage : attach-release-assets.sh <tag> <release-name> <fichier...>
|
# Usage : attach-release-assets.sh <tag> <release-name> <fichier...>
|
||||||
# Env : RELEASE_TOKEN (token Gitea avec write:repository), GITHUB_SERVER_URL, GITHUB_REPOSITORY.
|
# Env : RELEASE_TOKEN (token Gitea avec write:repository), GITHUB_SERVER_URL, GITHUB_REPOSITORY.
|
||||||
#
|
#
|
||||||
# Partagé par tous les jobs de release desktop (Linux, Windows, canal flottant) : la logique était
|
# Partagé par tous les jobs de release desktop et par le VSIX : la logique était dupliquée, et toute
|
||||||
# dupliquée dans chaque job, et toute correction devait être faite trois fois.
|
# correction devait être faite trois fois.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
tag="${1:?tag manquant}"
|
tag="${1:?tag manquant}"
|
||||||
@@ -23,40 +23,74 @@ fi
|
|||||||
|
|
||||||
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
auth="Authorization: token ${RELEASE_TOKEN}"
|
auth="Authorization: token ${RELEASE_TOKEN}"
|
||||||
|
body=$(mktemp)
|
||||||
|
trap 'rm -f "$body"' EXIT
|
||||||
|
|
||||||
release_id=$(curl -fsSL -H "$auth" "${api}/releases/tags/${tag}" \
|
# Lecture d'un champ JSON TOLÉRANTE : une réponse vide ou non-JSON (401, 403, 404) doit donner une
|
||||||
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).id || ''" || true)
|
# chaîne vide, pas une pile d'appels Node. Sans ça, deux `SyntaxError: Unexpected end of JSON input`
|
||||||
|
# s'affichaient avant le vrai message d'erreur et noyaient le diagnostic.
|
||||||
|
json_field() {
|
||||||
|
node -e "let s='';process.stdin.on('data',(d)=>{s+=d}).on('end',()=>{try{const o=JSON.parse(s);const v=o?.[process.argv[1]];process.stdout.write(v==null?'':String(v))}catch{process.stdout.write('')}})" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# `curl` silencieux qui écrit le corps dans $body et renvoie le code HTTP sur stdout.
|
||||||
|
http_call() {
|
||||||
|
curl -sS -o "$body" -w '%{http_code}' "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- résolution de la release (existante, sinon création) -------------------------------------
|
||||||
|
code=$(http_call -H "$auth" "${api}/releases/tags/${tag}")
|
||||||
|
release_id=$(json_field id < "$body")
|
||||||
|
|
||||||
if [ -z "$release_id" ]; then
|
if [ -z "$release_id" ]; then
|
||||||
release_id=$(curl -fsSL -X POST -H "$auth" -H 'Content-Type: application/json' \
|
# 401/403 sur une simple lecture : inutile de tenter la création, le token est en cause.
|
||||||
-d "{\"tag_name\":\"${tag}\",\"name\":\"${release_name}\"}" \
|
case "$code" in
|
||||||
"${api}/releases" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).id || ''")
|
401)
|
||||||
fi
|
echo "::error::le token de release est refusé (HTTP 401) : il est invalide, révoqué ou expiré."
|
||||||
|
echo "::error::régénérer un token Gitea et mettre à jour le secret NPM_TOKEN du dépôt."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
403)
|
||||||
|
echo "::error::le token de release manque de droits (HTTP 403) sur ${GITHUB_REPOSITORY}."
|
||||||
|
echo "::error::portées attendues : write:repository (releases et assets) et write:package (publication npm)."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
create_code=$(http_call -X POST -H "$auth" -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"tag_name\":\"${tag}\",\"name\":\"${release_name}\"}" "${api}/releases")
|
||||||
|
release_id=$(json_field id < "$body")
|
||||||
if [ -z "$release_id" ]; then
|
if [ -z "$release_id" ]; then
|
||||||
echo "::error::impossible de résoudre ou créer la release ${tag} avec le token fourni."
|
echo "::error::impossible de créer la release ${tag} (HTTP ${create_code})."
|
||||||
echo "::error::vérifier que le secret porte les portées write:repository et write:package, et qu'il n'a pas expiré."
|
echo "::error::réponse de l'API : $(head -c 300 "$body")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "release ${tag} créée (id ${release_id})."
|
||||||
|
else
|
||||||
|
echo "release ${tag} trouvée (id ${release_id})."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- attache des fichiers ----------------------------------------------------------------------
|
||||||
|
failed=0
|
||||||
for f in "$@"; do
|
for f in "$@"; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
name=$(basename "$f")
|
name=$(basename "$f")
|
||||||
# L'API Gitea refuse un asset de même nom : on supprime l'ancien pour que le dernier build gagne.
|
# L'API Gitea refuse un asset de même nom : on supprime l'ancien pour que le dernier build gagne.
|
||||||
existing=$(curl -fsSL -H "$auth" "${api}/releases/${release_id}/assets" \
|
http_call -H "$auth" "${api}/releases/${release_id}/assets" > /dev/null
|
||||||
| node -e "const a=JSON.parse(require('fs').readFileSync(0,'utf8'));const m=Array.isArray(a)?a.find(x=>x.name===process.argv[1]):null;process.stdout.write(m?String(m.id):'')" "$name" || true)
|
existing=$(node -e "let s='';process.stdin.on('data',(d)=>{s+=d}).on('end',()=>{try{const a=JSON.parse(s);const m=Array.isArray(a)?a.find((x)=>x.name===process.argv[1]):null;process.stdout.write(m?String(m.id):'')}catch{process.stdout.write('')}})" "$name" < "$body")
|
||||||
if [ -n "$existing" ]; then
|
if [ -n "$existing" ]; then
|
||||||
echo "replacing existing $name (asset $existing)"
|
echo "remplacement de $name (asset $existing)"
|
||||||
curl -fsSL -X DELETE -H "$auth" "${api}/releases/${release_id}/assets/${existing}" || true
|
http_call -X DELETE -H "$auth" "${api}/releases/${release_id}/assets/${existing}" > /dev/null
|
||||||
fi
|
fi
|
||||||
echo "attaching $name"
|
upload_code=$(http_call -X POST -H "$auth" -F "attachment=@${f}" "${api}/releases/${release_id}/assets?name=${name}")
|
||||||
if ! curl -fsSL -X POST -H "$auth" -F "attachment=@${f}" "${api}/releases/${release_id}/assets?name=${name}"; then
|
if [ "$upload_code" -ge 200 ] && [ "$upload_code" -lt 300 ]; then
|
||||||
echo "::error::échec de l'upload de ${name}"
|
echo "attaché : $name"
|
||||||
|
else
|
||||||
|
echo "::error::échec de l'upload de ${name} (HTTP ${upload_code}) : $(head -c 200 "$body")"
|
||||||
failed=1
|
failed=1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "${failed:-0}" != "0" ]; then
|
if [ "$failed" != "0" ]; then
|
||||||
echo "::error::au moins un asset n'a pas pu être attaché à ${tag}."
|
echo "::error::au moins un asset n'a pas pu être attaché à ${tag}."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import SecuritySection from './components/SecuritySection.vue';
|
|||||||
import FaqSection from './components/FaqSection.vue';
|
import FaqSection from './components/FaqSection.vue';
|
||||||
import FinalCta from './components/FinalCta.vue';
|
import FinalCta from './components/FinalCta.vue';
|
||||||
import AppFooter from './components/AppFooter.vue';
|
import AppFooter from './components/AppFooter.vue';
|
||||||
|
import BackToTop from './components/BackToTop.vue';
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
||||||
@@ -82,5 +83,6 @@ const glowStyle = {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
<BackToTop />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { REPO } from '../lib/links';
|
import { REPO } from '../lib/links';
|
||||||
import LangToggle from './LangToggle.vue';
|
import LangToggle from './LangToggle.vue';
|
||||||
@@ -7,26 +8,50 @@ import IconGitea from './icons/IconGitea.vue';
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// Les sections #launch (« Démarrer le projet ») et #remotegit (services git distants) existaient sans
|
/**
|
||||||
// aucun lien de navigation : elles n'étaient atteignables qu'en scrollant à l'aveugle.
|
* Navigation du site. `tier` = largeur à partir de laquelle le lien apparaît dans la barre :
|
||||||
|
* 1 = dès 900px, 2 = à partir de 1024px, 3 = à partir de 1180px.
|
||||||
|
*
|
||||||
|
* Les huit liens ne tiennent qu'au-delà de ~1180px. En dessous, la barre flex les compressait au lieu
|
||||||
|
* de les masquer, cassant « Start project », « Git services » et « How it works » sur deux ou trois
|
||||||
|
* lignes, chevauchant le logo et expulsant le bouton Gitea. Le menu compact, lui, montre toujours la
|
||||||
|
* liste complète : aucun lien n'est perdu, il change juste de place.
|
||||||
|
*/
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: '#features', key: 'navFeatures' },
|
{ href: '#features', key: 'navFeatures', tier: 1 },
|
||||||
{ href: '#workspace', key: 'navWorkspace' },
|
{ href: '#workspace', key: 'navWorkspace', tier: 1 },
|
||||||
{ href: '#launch', key: 'navLaunch' },
|
{ href: '#launch', key: 'navLaunch', tier: 2 },
|
||||||
{ href: '#remotegit', key: 'navRemoteGit' },
|
{ href: '#remotegit', key: 'navRemoteGit', tier: 3 },
|
||||||
{ href: '#download', key: 'navDownload' },
|
{ href: '#download', key: 'navDownload', tier: 1 },
|
||||||
{ href: '#how', key: 'navHow' },
|
{ href: '#how', key: 'navHow', tier: 3 },
|
||||||
{ href: '#security', key: 'navSecurity' },
|
{ href: '#security', key: 'navSecurity', tier: 2 },
|
||||||
{ href: '#faq', key: 'navFaq' },
|
{ href: '#faq', key: 'navFaq', tier: 1 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/** Tailwind ne génère que des classes littérales : la table évite toute classe construite à la volée. */
|
||||||
|
const TIER_CLASS: Record<number, string> = {
|
||||||
|
1: '',
|
||||||
|
2: 'hidden min-[1024px]:inline',
|
||||||
|
3: 'hidden min-[1180px]:inline',
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuOpen = ref(false);
|
||||||
|
const closeMenu = (): void => {
|
||||||
|
menuOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onKey(e: KeyboardEvent): void {
|
||||||
|
if (e.key === 'Escape') closeMenu();
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener('keydown', onKey));
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('keydown', onKey));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header class="sticky top-0 z-50 border-b border-border-soft bg-surface-0/72 backdrop-blur-[14px]">
|
||||||
class="sticky top-0 z-50 border-b border-border-soft bg-surface-0/72 backdrop-blur-[14px]"
|
<div class="mx-auto flex h-16 max-w-[1200px] items-center gap-4 px-6">
|
||||||
>
|
<!-- shrink-0 : le logo ne doit jamais être rogné ni recouvert par la nav. -->
|
||||||
<div class="mx-auto flex h-16 max-w-[1200px] items-center justify-between gap-6 px-6">
|
<a href="#top" class="flex shrink-0 items-center gap-2.5 text-fg no-underline" @click="closeMenu">
|
||||||
<a href="#top" class="flex items-center gap-2.5 text-fg no-underline">
|
|
||||||
<img
|
<img
|
||||||
src="/assets/arboretum-mark.png"
|
src="/assets/arboretum-mark.png"
|
||||||
alt="Arboretum"
|
alt="Arboretum"
|
||||||
@@ -37,31 +62,83 @@ const navLinks = [
|
|||||||
<span class="font-mono text-[17px] font-semibold tracking-[-0.01em]">Arboretum</span>
|
<span class="font-mono text-[17px] font-semibold tracking-[-0.01em]">Arboretum</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="hidden items-center gap-[30px] min-[900px]:flex">
|
<nav class="hidden min-w-0 flex-1 items-center justify-center gap-6 min-[900px]:flex min-[1180px]:gap-[30px]">
|
||||||
<a
|
<a
|
||||||
v-for="link in navLinks"
|
v-for="link in navLinks"
|
||||||
:key="link.href"
|
:key="link.href"
|
||||||
:href="link.href"
|
:href="link.href"
|
||||||
class="text-[14.5px] text-fg-muted no-underline transition-colors hover:text-fg"
|
class="shrink-0 whitespace-nowrap text-[14.5px] text-fg-muted no-underline transition-colors hover:text-fg"
|
||||||
|
:class="TIER_CLASS[link.tier]"
|
||||||
>
|
>
|
||||||
{{ t(link.key) }}
|
{{ t(link.key) }}
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="flex items-center gap-3.5">
|
<div class="ml-auto flex shrink-0 items-center gap-2 min-[900px]:ml-0 min-[900px]:gap-3.5">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<LangToggle />
|
<LangToggle />
|
||||||
|
<!-- Sous 900px, Gitea vit dans le panneau : à 390px, logo + 3 contrôles + menu débordaient et
|
||||||
|
le bouton menu se retrouvait tronqué au bord de l'écran. -->
|
||||||
<a
|
<a
|
||||||
:href="REPO"
|
:href="REPO"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
aria-label="Gitea"
|
aria-label="Gitea"
|
||||||
class="inline-flex items-center gap-[7px] rounded-lg border border-border px-[13px] py-[7px] text-[13.5px] font-medium text-fg-muted no-underline transition-colors hover:border-accent hover:text-accent"
|
class="hidden items-center gap-[7px] rounded-lg border border-border px-[13px] py-[7px] text-[13.5px] font-medium text-fg-muted no-underline transition-colors hover:border-accent hover:text-accent min-[900px]:inline-flex"
|
||||||
|
>
|
||||||
|
<IconGitea :size="16" />
|
||||||
|
<!-- Libellé masqué tant que la barre est serrée : l'icône suffit, l'aria-label reste. -->
|
||||||
|
<span class="hidden min-[1180px]:inline">Gitea</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center rounded-lg border border-border p-[7px] text-fg-muted transition-colors hover:border-accent hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70 min-[900px]:hidden"
|
||||||
|
:aria-label="t('navMenu')"
|
||||||
|
:aria-expanded="menuOpen"
|
||||||
|
aria-controls="site-mobile-nav"
|
||||||
|
@click="menuOpen = !menuOpen"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||||
|
<template v-if="menuOpen">
|
||||||
|
<path d="M18 6 6 18" />
|
||||||
|
<path d="m6 6 12 12" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<path d="M4 7h16" />
|
||||||
|
<path d="M4 12h16" />
|
||||||
|
<path d="M4 17h16" />
|
||||||
|
</template>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau compact sous 900px : liste complète, une entrée par ligne, refermée au choix d'un lien. -->
|
||||||
|
<nav
|
||||||
|
v-if="menuOpen"
|
||||||
|
id="site-mobile-nav"
|
||||||
|
class="border-t border-border-soft bg-surface-0/95 px-6 py-2 backdrop-blur-[14px] min-[900px]:hidden"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="link in navLinks"
|
||||||
|
:key="link.href"
|
||||||
|
:href="link.href"
|
||||||
|
class="block py-2 text-[15px] text-fg-muted no-underline transition-colors hover:text-fg"
|
||||||
|
@click="closeMenu"
|
||||||
|
>
|
||||||
|
{{ t(link.key) }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="REPO"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="mt-1 flex items-center gap-2 border-t border-border-soft py-2.5 text-[15px] text-fg-muted no-underline transition-colors hover:text-accent"
|
||||||
|
@click="closeMenu"
|
||||||
>
|
>
|
||||||
<IconGitea :size="16" />
|
<IconGitea :size="16" />
|
||||||
Gitea
|
Gitea
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Bouton « remonter en haut », en bas à droite. Reprend les tokens existants (bordure `border`, fond
|
||||||
|
// `surface-1`, accent au survol, radius 12px, ombre `shadow-card`) : rien de neuf visuellement.
|
||||||
|
// Il n'apparaît qu'après un vrai défilement et disparaît en haut de page, pour ne jamais recouvrir le
|
||||||
|
// contenu sans raison.
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
/** Au-delà d'un écran de défilement, remonter rend un vrai service. */
|
||||||
|
const SHOW_AFTER = 600;
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const onScroll = (): void => {
|
||||||
|
visible.value = window.scrollY > SHOW_AFTER;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toTop(): void {
|
||||||
|
// Respecte la préférence système : pas de défilement animé si l'utilisateur les a réduites.
|
||||||
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Transition name="btt">
|
||||||
|
<button
|
||||||
|
v-if="visible"
|
||||||
|
type="button"
|
||||||
|
class="fixed right-5 bottom-5 z-40 inline-flex h-11 w-11 items-center justify-center rounded-[12px] border border-border bg-surface-1/90 text-fg-muted shadow-card backdrop-blur-[10px] transition-colors hover:border-accent hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70 sm:right-7 sm:bottom-7"
|
||||||
|
:aria-label="t('backToTop')"
|
||||||
|
:title="t('backToTop')"
|
||||||
|
@click="toTop"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m18 15-6-6-6 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btt-enter-active,
|
||||||
|
.btt-leave-active {
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
|
}
|
||||||
|
.btt-enter-from,
|
||||||
|
.btt-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.btt-enter-active,
|
||||||
|
.btt-leave-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,8 @@ export default {
|
|||||||
navHow: 'How it works',
|
navHow: 'How it works',
|
||||||
navSecurity: 'Security',
|
navSecurity: 'Security',
|
||||||
navFaq: 'FAQ',
|
navFaq: 'FAQ',
|
||||||
|
navMenu: 'Menu',
|
||||||
|
backToTop: 'Back to top',
|
||||||
themeToggle: 'Toggle theme',
|
themeToggle: 'Toggle theme',
|
||||||
gitea: 'View on Gitea',
|
gitea: 'View on Gitea',
|
||||||
heroBadge: 'Mission control for AI coding agents',
|
heroBadge: 'Mission control for AI coding agents',
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default {
|
|||||||
navHow: 'Comment ça marche',
|
navHow: 'Comment ça marche',
|
||||||
navSecurity: 'Sécurité',
|
navSecurity: 'Sécurité',
|
||||||
navFaq: 'FAQ',
|
navFaq: 'FAQ',
|
||||||
|
navMenu: 'Menu',
|
||||||
|
backToTop: 'Revenir en haut',
|
||||||
themeToggle: 'Changer de thème',
|
themeToggle: 'Changer de thème',
|
||||||
gitea: 'Voir sur Gitea',
|
gitea: 'Voir sur Gitea',
|
||||||
heroBadge: 'Poste de commandement pour agents de code IA',
|
heroBadge: 'Poste de commandement pour agents de code IA',
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { computed, ref, watch, type ComputedRef } from 'vue';
|
|||||||
export type ThemeMode = 'dark' | 'light' | 'system';
|
export type ThemeMode = 'dark' | 'light' | 'system';
|
||||||
export type ResolvedTheme = 'dark' | 'light';
|
export type ResolvedTheme = 'dark' | 'light';
|
||||||
|
|
||||||
// MÊME clé que l'app (packages/web/src/lib/theme.ts) : le site se présente comme aligné sur elle, et
|
// Même NOM de clé que l'app (packages/web/src/lib/theme.ts), par cohérence de nommage. À noter que la
|
||||||
// un visiteur qui bascule le thème ici retrouve le même sur son instance.
|
// préférence n'est pas partagée pour autant : le site et une instance Arboretum vivent sur des
|
||||||
|
// origines différentes (donc des localStorage séparés), et l'app sérialise sa valeur en JSON là où le
|
||||||
|
// site stocke la chaîne brute, lue telle quelle par le script anti-FOUC de index.html.
|
||||||
const STORAGE_KEY = 'arb.theme';
|
const STORAGE_KEY = 'arb.theme';
|
||||||
// Doit rester synchronisé avec --color-surface-0 (style.css) : fond du <html> + metas.
|
// Doit rester synchronisé avec --color-surface-0 (style.css) : fond du <html> + metas.
|
||||||
const BG: Record<ResolvedTheme, string> = { dark: '#09090b', light: '#fafafa' };
|
const BG: Record<ResolvedTheme, string> = { dark: '#09090b', light: '#fafafa' };
|
||||||
|
|||||||
Reference in New Issue
Block a user