feat(oki): fondations de la migration vers la stack souveraine OKI
Architecture décidée et documentée dans ADR.md : SvelteKit natif en fichiers plats plutôt que Strapi (55 outils, données statiques, souveraineté et budget performance prioritaires — doctrine §1.2, §4.4). - frontend/ : SvelteKit 2 + Svelte 5 (runes), adapter-static, 58 pages prérendues, CSS natif en @layer (reset→tokens→base→layouts→components), tokens oklch(), container queries, typo fluide clamp(), 2 polices WOFF2 auto-hébergées (Atkinson Hyperlegible, Fraunces), zéro CDN tiers - i18n FR/gcf typé (clé manquante = erreur de build), bascule Kréyòl persistée en localStorage, micro-textes créoles (« Ou pa manké ayen ») - Composants : ToolCard, DifficultyBadge (forme+texte+couleur, jamais la couleur seule), CategoryNav (≤5 entrées, divulgation progressive), SearchBar (raccourci /), LanguageSwitcher, EndMarker - content/ : 55 fiches migrées depuis src/ (script frontend/scripts/ migrate-tools.mjs, idempotent), licences SPDX reprises de directory.json - docs/ : DESIGN_SYSTEM.md et CONTENT_GUIDE.md Budgets vérifiés : JS 41 Ko gzip, CSS 2,9 Ko gzip, svelte-check 0 erreur. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/state'
|
||||
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
||||
import SearchBar from '$lib/components/SearchBar.svelte'
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import EndMarker from '$lib/components/EndMarker.svelte'
|
||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
let query = $state('')
|
||||
|
||||
const DIFFICULTIES = ['beginner', 'intermediate', 'advanced'] as const
|
||||
|
||||
// Chaque filtre vit dans l'URL (query param) : partageable (mission §5.1).
|
||||
// Le HTML prérendu montre la liste complète ; le filtrage est une
|
||||
// amélioration progressive côté client (ADR-001). Les searchParams ne
|
||||
// sont pas lisibles au prerender, d'où la garde `browser`.
|
||||
let cat = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
let diff = $derived(browser ? page.url.searchParams.get('diff') : null)
|
||||
|
||||
let filtered = $derived(
|
||||
data.tools.filter(
|
||||
(tool) =>
|
||||
(!cat || tool.category === cat) &&
|
||||
(!diff || tool.difficulty === diff) &&
|
||||
(!query ||
|
||||
`${tool.name} ${tool.tags.join(' ')} ${tool.excerpt}`
|
||||
.toLocaleLowerCase('fr')
|
||||
.includes(query.toLocaleLowerCase('fr')))
|
||||
)
|
||||
)
|
||||
|
||||
function diffHref(d: string | null): string {
|
||||
const params = new URLSearchParams(browser ? page.url.searchParams : undefined)
|
||||
if (cat) params.set('cat', cat)
|
||||
if (d) params.set('diff', d)
|
||||
else params.delete('diff')
|
||||
const qs = params.toString()
|
||||
return qs ? `/outils?${qs}` : '/outils'
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('directory.title')} — {t('site.name')}</title>
|
||||
<meta name="description" content={t('directory.intro')} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="stack">
|
||||
<h1>{t('directory.title')}</h1>
|
||||
<p>{t('directory.intro')}</p>
|
||||
</header>
|
||||
|
||||
<div class="stack filters">
|
||||
<SearchBar bind:value={query} />
|
||||
<CategoryNav categories={data.categories} />
|
||||
<nav aria-label={t('directory.filterDifficulty')}>
|
||||
<ul class="cluster">
|
||||
<li>
|
||||
<a href={diffHref(null)} aria-current={diff === null ? 'page' : undefined}>
|
||||
{t('directory.allDifficulties')}
|
||||
</a>
|
||||
</li>
|
||||
{#each DIFFICULTIES as d (d)}
|
||||
<li>
|
||||
<a href={diffHref(d)} aria-current={diff === d ? 'page' : undefined}>
|
||||
<DifficultyBadge difficulty={d} />
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="stack" aria-live="polite">
|
||||
<p class="count">
|
||||
{filtered.length}
|
||||
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
|
||||
</p>
|
||||
|
||||
{#if filtered.length === 0}
|
||||
<p class="empty">{t('directory.empty')}</p>
|
||||
{:else}
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each filtered as tool (tool.slug)}
|
||||
<li><ToolCard {tool} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<EndMarker />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.filters {
|
||||
--gap: var(--space-3);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
nav a[aria-current='page'] {
|
||||
background: var(--oki-surface-2);
|
||||
border-color: var(--oki-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--space-5);
|
||||
border: 1px dashed var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-align: center;
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user