feat: migre le quiz, renomme en Lagé Chat Control, nettoie l'ère Astro
- Quiz de résistance (/quiz) migré d'Astro vers Svelte 5 : 12 questions, contenu fusionné dans content/quiz.json, score calculé localement (aucun envoi), barres par domaine, recommandations liées à l'annuaire réel (/outils?cat=…), persistance locale, partage natif/presse-papier. Audit axe-core : 0 violation. - Renommage complet « Exit Chat Control » → « Lagé Chat Control » : i18n fr/gcf, manifest PWA, package.json, README, design system. - README racine et frontend/README.md réécrits pour le projet Svelte : lancement local (pnpm dev) et déploiement VPS (renvoi vers docs/DEPLOYMENT.md), en français. - CONTRIBUTING.md adapté à la nouvelle structure (content/outils/, forge labola) en conservant la politique éditoriale de confiance. - Nettoyage : suppression de tout l'outillage Astro devenu inutile (src/, public/, scripts/, tests/, migration/, docker/, configs racine eslint/prettier/vitest/playwright/tsconfig, CI GitHub Actions, script de migration one-shot). robots.txt et security.txt recréés sous frontend/static/ avec les bonnes références (labola). - Fix : pin pnpm via frontend/pnpm-workspace.yaml (onlyBuiltDependencies/ allowBuilds pour esbuild) après suppression du package.json racine qui perturbait la résolution de version de corepack. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export interface QuizOption {
|
||||
points: number
|
||||
label: string
|
||||
hint: string
|
||||
}
|
||||
|
||||
export interface QuizQuestion {
|
||||
id: string
|
||||
/** slug de content/categories.json — relie chaque question à l'annuaire */
|
||||
category: string
|
||||
label: string
|
||||
prompt: string
|
||||
rec: string
|
||||
options: QuizOption[]
|
||||
}
|
||||
|
||||
export interface QuizBand {
|
||||
id: string
|
||||
min: number
|
||||
max: number
|
||||
title: string
|
||||
summary: string
|
||||
}
|
||||
|
||||
export interface QuizContent {
|
||||
bands: QuizBand[]
|
||||
questions: QuizQuestion[]
|
||||
}
|
||||
|
||||
const CONTENT = path.resolve(process.cwd(), '..', 'content')
|
||||
|
||||
let cache: QuizContent | null = null
|
||||
|
||||
export function loadQuiz(): QuizContent {
|
||||
if (!cache) {
|
||||
cache = JSON.parse(readFileSync(path.join(CONTENT, 'quiz.json'), 'utf8')) as QuizContent
|
||||
}
|
||||
return cache
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"name": "Lagé Chat Control",
|
||||
"tagline": "Reprends le contrôle de ton internet",
|
||||
"description": "Annuaire d'outils libres pour la souveraineté numérique — messagerie chiffrée, VPN, auto-hébergement, OS libres."
|
||||
},
|
||||
@@ -111,5 +111,27 @@
|
||||
"footer": {
|
||||
"license": "Contenu et code libres (MIT). Zéro tracker, zéro CDN, zéro cookie.",
|
||||
"contribute": "Proposer un outil ou une correction"
|
||||
},
|
||||
"quiz": {
|
||||
"nav": "Quiz",
|
||||
"eyebrow": "Évaluer sa résistance à la censure",
|
||||
"title": "Quiz de résistance",
|
||||
"lede": "Une boussole, pas un classement : réponds à douze questions rapides pour repérer les points faibles de ta pile — sans jugement — puis suis des pistes concrètes vers l'annuaire.",
|
||||
"privacyNote": "Le score est calculé localement dans ton navigateur. Rien n'est envoyé, rien n'est enregistré.",
|
||||
"start": "Commencer le quiz",
|
||||
"readDirectory": "Voir l'annuaire",
|
||||
"progress": "{done} / {total} répondues",
|
||||
"resultEyebrow": "Ton résultat",
|
||||
"outOf": "/ 100",
|
||||
"barsHeading": "Score par domaine",
|
||||
"recHeading": "Tes 3 prochaines étapes",
|
||||
"openDirectory": "Voir les outils →",
|
||||
"restart": "Refaire le quiz",
|
||||
"copy": "Copier le lien du résultat",
|
||||
"copied": "Lien copié ✓",
|
||||
"share": "Partager",
|
||||
"shareText": "Mon score de résistance à la censure : {score}/100 — {band}. Évalue le tien :",
|
||||
"bandsHeading": "Ce que le score signifie",
|
||||
"categoriesHeading": "Les douze domaines, et où progresser"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"name": "Lagé Chat Control",
|
||||
"tagline": "Pran kontwòl entènèt a-w",
|
||||
"description": "Lis a zouti lib pou libèté niméwik — mésajri chifré, VPN, oto-ébèjman, sistèm lib."
|
||||
},
|
||||
@@ -111,5 +111,27 @@
|
||||
"footer": {
|
||||
"license": "Kontni é kòd lib (MIT). Pon trakè, pon CDN, pon cookie.",
|
||||
"contribute": "Pwopozé on zouti oben on koreksyon"
|
||||
},
|
||||
"quiz": {
|
||||
"nav": "Quiz",
|
||||
"eyebrow": "Èvalyé rézistans a-w a sansi-la",
|
||||
"title": "Quiz rézistans",
|
||||
"lede": "On bousòl, pa on klasman : réponn douz kèsyon rapid pou trapé pwen fèb an pil a-w — san jijman — apré swiv chimen konkrè vè lis a zouti-la.",
|
||||
"privacyNote": "Skò-la ka kalkilé lokalman an navigatè a-w. Ayen pa voyé, ayen pa anrèjistré.",
|
||||
"start": "Koumansé quiz-la",
|
||||
"readDirectory": "Gadé lis-la",
|
||||
"progress": "{done} / {total} réponn",
|
||||
"resultEyebrow": "Rézilta a-w",
|
||||
"outOf": "/ 100",
|
||||
"barsHeading": "Skò pa domèn",
|
||||
"recHeading": "Twa pwochen étap a-w",
|
||||
"openDirectory": "Gadé zouti yo →",
|
||||
"restart": "Rifè quiz-la",
|
||||
"copy": "Kopyé lyen rézilta-la",
|
||||
"copied": "Lyen kopyé ✓",
|
||||
"share": "Pataj",
|
||||
"shareText": "Skò rézistans a sansi a mwen : {score}/100 — {band}. Èvalyé pa a-w :",
|
||||
"bandsHeading": "Sa skò-la vlé di",
|
||||
"categoriesHeading": "Douz domèn yo, é kotè pou pwogrésé"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<nav aria-label={t('nav.home')}>
|
||||
<ul class="cluster">
|
||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
||||
<li><a href="/quiz">{t('quiz.nav')}</a></li>
|
||||
<li><a href="/contribuer">{t('nav.contribute')}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { loadQuiz } from '$lib/content/quiz.server'
|
||||
|
||||
export function load() {
|
||||
return loadQuiz()
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
import type { QuizContent } from '$lib/content/quiz.server'
|
||||
|
||||
let { data }: { data: QuizContent } = $props()
|
||||
// le contenu du quiz est figé pour la durée de vie de la page (pas de
|
||||
// navigation interne qui le changerait) — une seule capture au montage
|
||||
const { bands, questions } = $state.snapshot(data)
|
||||
|
||||
const STORAGE_KEY = 'oki-quiz'
|
||||
|
||||
function readSaved(): Record<string, number> {
|
||||
if (typeof localStorage === 'undefined') return {}
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
const out: Record<string, number> = {}
|
||||
for (const [k, v] of Object.entries(parsed)) if (typeof v === 'number') out[k] = v
|
||||
return out
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Un seul état source : { questionId: points }. Persisté en local
|
||||
// uniquement (doctrine §4.3 privacyNote) — jamais envoyé nulle part.
|
||||
let answers = $state<Record<string, number>>(readSaved())
|
||||
|
||||
$effect(() => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(answers))
|
||||
} catch {
|
||||
/* mode privé : le quiz fonctionne quand même pour cette visite */
|
||||
}
|
||||
})
|
||||
|
||||
let answeredCount = $derived(Object.keys(answers).length)
|
||||
let total = questions.length
|
||||
let complete = $derived(answeredCount === total)
|
||||
|
||||
let totalPoints = $derived(Object.values(answers).reduce((s, p) => s + p, 0))
|
||||
let maxPoints = $derived(
|
||||
questions.reduce((s, q) => s + Math.max(...q.options.map((o) => o.points)), 0)
|
||||
)
|
||||
let score = $derived(maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 0)
|
||||
let band = $derived(bands.find((b) => score >= b.min && score <= b.max) ?? bands[0])
|
||||
|
||||
interface Ratio {
|
||||
question: (typeof questions)[number]
|
||||
points: number
|
||||
max: number
|
||||
ratio: number
|
||||
}
|
||||
|
||||
let ratios = $derived<Ratio[]>(
|
||||
questions.map((q) => {
|
||||
const max = Math.max(...q.options.map((o) => o.points))
|
||||
const points = answers[q.id] ?? 0
|
||||
return { question: q, points, max, ratio: max > 0 ? points / max : 0 }
|
||||
})
|
||||
)
|
||||
|
||||
let weakest = $derived([...ratios].sort((a, b) => a.ratio - b.ratio).slice(0, 3))
|
||||
|
||||
let resultPanel: HTMLElement | undefined = $state()
|
||||
let wasComplete = false
|
||||
$effect(() => {
|
||||
if (complete && !wasComplete) resultPanel?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
wasComplete = complete
|
||||
})
|
||||
|
||||
function restart() {
|
||||
answers = {}
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} catch {
|
||||
/* rien à effacer */
|
||||
}
|
||||
document.getElementById('quiz-form')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
let copied = $state(false)
|
||||
function shareUrl() {
|
||||
return location.href.replace(/[?#].*$/, '')
|
||||
}
|
||||
function shareMessage() {
|
||||
return t('quiz.shareText').replace('{score}', String(score)).replace('{band}', band.title)
|
||||
}
|
||||
async function copyLink() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${shareMessage()} ${shareUrl()}`)
|
||||
copied = true
|
||||
setTimeout(() => (copied = false), 2000)
|
||||
} catch {
|
||||
/* presse-papier indisponible */
|
||||
}
|
||||
}
|
||||
let canNativeShare = $derived(typeof navigator !== 'undefined' && 'share' in navigator)
|
||||
async function nativeShare() {
|
||||
try {
|
||||
await navigator.share({ title: t('quiz.title'), text: shareMessage(), url: shareUrl() })
|
||||
} catch {
|
||||
/* annulé ou non supporté */
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('quiz.title')} — {t('site.name')}</title>
|
||||
<meta name="description" content={t('quiz.lede')} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="stack">
|
||||
<p class="eyebrow">{t('quiz.eyebrow')}</p>
|
||||
<h1>{t('quiz.title')}</h1>
|
||||
<p class="prose">{t('quiz.lede')}</p>
|
||||
<p class="privacy">{t('quiz.privacyNote')}</p>
|
||||
<p class="cluster">
|
||||
<a class="cta" href="#quiz-form">{t('quiz.start')}</a>
|
||||
<a class="secondary" href="/outils">{t('quiz.readDirectory')}</a>
|
||||
</p>
|
||||
<noscript>
|
||||
<p class="privacy">{t('quiz.privacyNote')} — JavaScript est requis pour calculer ton score.</p>
|
||||
</noscript>
|
||||
</header>
|
||||
|
||||
{#if answeredCount > 0}
|
||||
<!-- point d'arrêt naturel : la progression est visible, jamais un
|
||||
chargement automatique en arrière-plan (doctrine §1.5) -->
|
||||
<div class="progress-block" role="status">
|
||||
<p class="progress-text">{t('quiz.progress').replace('{done}', String(answeredCount)).replace('{total}', String(total))}</p>
|
||||
<div class="progress-track" aria-hidden="true">
|
||||
<div class="progress-fill" style:--ratio="{(answeredCount / total) * 100}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form id="quiz-form" class="stack" style="--gap: var(--space-4)">
|
||||
<ol class="questions stack" style="--gap: var(--space-4)">
|
||||
{#each questions as question, i (question.id)}
|
||||
<li>
|
||||
<fieldset class="question stack">
|
||||
<legend>
|
||||
<span class="qnum" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
|
||||
{question.prompt}
|
||||
</legend>
|
||||
<div class="options">
|
||||
{#each question.options as option (option.label)}
|
||||
<label class="option" class:checked={answers[question.id] === option.points}>
|
||||
<input
|
||||
type="radio"
|
||||
name={question.id}
|
||||
value={option.points}
|
||||
checked={answers[question.id] === option.points}
|
||||
onchange={() => (answers = { ...answers, [question.id]: option.points })}
|
||||
/>
|
||||
<span class="option-body">
|
||||
<span class="option-label">{option.label}</span>
|
||||
<span class="option-hint">{option.hint}</span>
|
||||
</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</form>
|
||||
|
||||
{#if complete}
|
||||
<section class="result stack" bind:this={resultPanel} tabindex="-1" aria-live="polite">
|
||||
<p class="eyebrow">{t('quiz.resultEyebrow')}</p>
|
||||
<p class="score"><strong>{score}</strong><span>{t('quiz.outOf')}</span></p>
|
||||
<p class="band-title">{band.title}</p>
|
||||
<p class="prose">{band.summary}</p>
|
||||
|
||||
<h2>{t('quiz.barsHeading')}</h2>
|
||||
<div class="bars stack" style="--gap: var(--space-2)">
|
||||
{#each ratios as r (r.question.id)}
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">{r.question.label}</span>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill" style:--ratio="{r.ratio * 100}%"></div>
|
||||
</div>
|
||||
<strong class="bar-value">{r.points}/{r.max}</strong>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<h2>{t('quiz.recHeading')}</h2>
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each weakest as r (r.question.id)}
|
||||
<li class="rec-card stack">
|
||||
<strong>{r.question.label}</strong>
|
||||
<p class="prose">{r.question.rec}</p>
|
||||
<a href="/outils?cat={r.question.category}">{t('quiz.openDirectory')}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="cluster actions">
|
||||
<button type="button" onclick={copyLink}>{copied ? t('quiz.copied') : t('quiz.copy')}</button>
|
||||
{#if canNativeShare}
|
||||
<button type="button" onclick={nativeShare}>{t('quiz.share')}</button>
|
||||
{/if}
|
||||
<button type="button" class="restart" onclick={restart}>{t('quiz.restart')}</button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="key stack">
|
||||
<h2>{t('quiz.bandsHeading')}</h2>
|
||||
<ul class="band-key">
|
||||
{#each bands as b (b.id)}
|
||||
<li>
|
||||
<strong>{b.title}</strong>
|
||||
<span class="range">{b.min}–{b.max}</span>
|
||||
<p class="prose">{b.summary}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<h2>{t('quiz.categoriesHeading')}</h2>
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each questions as q (q.id)}
|
||||
<li class="cat-card stack">
|
||||
<strong>{q.label}</strong>
|
||||
<p class="prose">{q.rec}</p>
|
||||
<a href="/outils?cat={q.category}">{t('quiz.openDirectory')}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.eyebrow {
|
||||
font-family: var(--font-text);
|
||||
font-size: var(--text-0);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.privacy {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
/* ── progression ──────────────────────────────────────────────── */
|
||||
.progress-block {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
background: color-mix(in oklch, var(--oki-surface) 92%, transparent);
|
||||
backdrop-filter: blur(4px);
|
||||
padding-block: var(--space-2);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
margin-block-end: var(--space-1);
|
||||
}
|
||||
|
||||
.progress-track,
|
||||
.bar-track {
|
||||
inline-size: 100%;
|
||||
block-size: 0.5rem;
|
||||
background: var(--oki-surface-2);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill,
|
||||
.bar-fill {
|
||||
inline-size: var(--ratio, 0%);
|
||||
block-size: 100%;
|
||||
background: var(--oki-jaune);
|
||||
border-radius: inherit;
|
||||
transition: inline-size var(--duration-2) var(--ease-out);
|
||||
}
|
||||
|
||||
/* ── questions ────────────────────────────────────────────────── */
|
||||
.question {
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: 700;
|
||||
padding-inline: var(--space-1);
|
||||
}
|
||||
|
||||
.qnum {
|
||||
color: var(--accent-ink);
|
||||
font-family: var(--font-display);
|
||||
margin-inline-end: var(--space-2);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
grid-template-columns: 1fr;
|
||||
margin-block-start: var(--space-3);
|
||||
}
|
||||
|
||||
@container (min-width: 44rem) {
|
||||
.options {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
min-height: var(--tap-target);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option.checked {
|
||||
border-color: var(--oki-jaune);
|
||||
background: var(--oki-surface);
|
||||
outline: 2px solid var(--oki-jaune);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.option input {
|
||||
margin-block-start: 0.2em;
|
||||
inline-size: 1.2rem;
|
||||
block-size: 1.2rem;
|
||||
accent-color: var(--oki-jaune);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 700;
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
.option-hint {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
|
||||
/* ── résultat ──────────────────────────────────────────────────── */
|
||||
.result {
|
||||
border: 1px solid var(--oki-line);
|
||||
border-inline-start: 4px solid var(--oki-jaune);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.score {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-1);
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.score strong {
|
||||
font-size: var(--text-5);
|
||||
}
|
||||
|
||||
.score span {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-2);
|
||||
}
|
||||
|
||||
.band-title {
|
||||
font-family: var(--font-text);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: grid;
|
||||
grid-template-columns: 9rem 1fr 3.5rem;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
font-size: var(--text-0);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.rec-card,
|
||||
.cat-card {
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: 1px solid var(--oki-line);
|
||||
padding-block-start: var(--space-3);
|
||||
}
|
||||
|
||||
.actions button {
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
}
|
||||
|
||||
.actions .restart {
|
||||
margin-inline-start: auto;
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
|
||||
/* ── clé de lecture ────────────────────────────────────────────── */
|
||||
.key {
|
||||
border-top: 1px solid var(--oki-line);
|
||||
padding-block-start: var(--space-5);
|
||||
}
|
||||
|
||||
.band-key {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.band-key li {
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.band-key strong {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.range {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
margin-inline-start: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
import { build, files, version } from '$service-worker'
|
||||
|
||||
const CACHE = `oki-${version}`
|
||||
const SHELL = ['/', '/outils', '/contribuer', ...build, ...files]
|
||||
const SHELL = ['/', '/outils', '/quiz', '/contribuer', ...build, ...files]
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
|
||||
Reference in New Issue
Block a user