From a9ec5c49e491b26a515fdac26842838c8f749ef6 Mon Sep 17 00:00:00 2001 From: Yvv Date: Sat, 18 Jul 2026 05:28:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20socle=20initial=20du=20package=20mutual?= =?UTF-8?q?is=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - primitives de design partagées / mécanisme moods commun - isolé, opt-in, sans impact sur les projets existants Co-Authored-By: Claude Opus 4.8 --- .gitignore | 5 ++ README.md | 48 ++++++++++++++++ app/composables/useMood.ts | 113 +++++++++++++++++++++++++++++++++++++ nuxt.config.ts | 5 ++ package.json | 8 +++ 5 files changed, 179 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/composables/useMood.ts create mode 100644 nuxt.config.ts create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f79edc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.output/ +.nuxt/ +dist/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8e4ae4 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# @yvv/nuxt-base + +Nuxt layer partagé pour les projets Nuxt Yvv (librodrome, GrateWizard, libreDecision, SejeteralO). +Fournit le **mécanisme** commun, jamais les couleurs (chaque projet garde son identité). + +## Contenu + +- `app/composables/useMood.ts` — mécanisme d'ambiances/palettes (extrait de libreDecision, généralisé). + Supporte les 2 patterns existants sans réécrire le CSS d'un projet : + - **classes `.mood-`** + variables `--mood-*` définies en CSS (style libreDecision) ; + - **variables CSS injectées en JS** via `MoodDef.vars` (style librodrome `--color-*`) ; + - option `lightDarkClasses` pour basculer aussi `.palette-light/.palette-dark`. + +## Adoption (à faire AVEC vérif visuelle mobile + desktop — zéro régression) + +Dans `nuxt.config.ts` du projet : + +```ts +export default defineNuxtConfig({ + extends: ['../yvv-nuxt-base'], // chemin relatif depuis la racine frontend du projet +}) +``` + +`useMood` devient auto-importé. Puis, par projet : + +### libreDecision (risque quasi nul — c'est le mécanisme canonique) +Remplacer le corps de `frontend/app/composables/useMood.ts` par un appel au layer : +```ts +// frontend/app/composables/useMood.ts +const moods = [ + { id: 'peps', label: 'Peps', description: 'Chaud et tonique', icon: 'i-lucide-sun', color: '#d44a10', isDark: false }, + { id: 'zen', label: 'Zen', description: 'Nature vivante', icon: 'i-lucide-leaf', color: '#2e8b48', isDark: false }, + { id: 'chagrine', label: 'Chagrine', description: 'Nuit profonde', icon: 'i-lucide-moon', color: '#6488d8', isDark: true }, + { id: 'grave', label: 'Grave', description: 'Ambre mineral', icon: 'i-lucide-shield', color: '#d8a030', isDark: true }, +] +export const useLibreMood = () => useMood(moods, { storageKey: 'libredecision_mood', defaultId: 'peps' }) +``` +`moods.css` (les couleurs) reste inchangé. Vérifier le switch des 4 moods + persistance. + +### librodrome (via l'option vars — le CSS `--color-*` reste inchangé) +Convertir `stores/palette.ts` pour déléguer l'application DOM à `useMood`, en passant les triplets HSL dans `vars` (`--color-primary`, etc.) et `lightDarkClasses: true`. Le store peut rester pour l'API publique, mais l'application au DOM passe par le mécanisme partagé. Vérifier les 4 saisons + light/dark. + +### GrateWizard / SejeteralO +Adopter après les deux premiers, même principe. SejeteralO (pas d'UnoCSS, vue-router 5) en dernier, prudemment. + +## Principe + +Ce layer unifie la **logique** (application DOM, persistance, SSR-guard, color-mode), pas le **contrat CSS**. Migrer le contrat CSS (renommer `--color-*` → `--mood-*`) est une étape séparée, optionnelle, à faire sous contrôle visuel. diff --git a/app/composables/useMood.ts b/app/composables/useMood.ts new file mode 100644 index 0000000..a65c579 --- /dev/null +++ b/app/composables/useMood.ts @@ -0,0 +1,113 @@ +import { ref, readonly, type Ref } from 'vue' + +/** + * Définition d'une ambiance ("mood") / palette. + * Chaque projet fournit SA propre liste (ses couleurs = son identité). + */ +export interface MoodDef { + id: string + label?: string + description?: string + icon?: string + color?: string + isDark: boolean + /** + * Variables CSS à poser sur quand ce mood est actif. + * Optionnel — pour les projets qui injectent les variables en JS + * (style librodrome) plutôt que via des classes .mood- en CSS + * (style libreDecision). Les deux approches sont supportées. + */ + vars?: Record +} + +export interface UseMoodOptions { + /** clé localStorage (défaut: 'yvv_mood') */ + storageKey?: string + /** id du mood par défaut (défaut: premier de la liste) */ + defaultId?: string + /** synchronise useColorMode().preference depuis isDark (défaut: true) */ + syncColorMode?: boolean + /** bascule aussi les classes .palette-light / .palette-dark (style librodrome) (défaut: false) */ + lightDarkClasses?: boolean +} + +const currentMood: Ref = ref('') + +/** + * Mécanisme moods partagé (extrait de libreDecision, généralisé). + * Applique sur : classe .mood-, variables CSS éventuelles, + * synchro color-mode, persistance localStorage. SSR-safe. + * + * @example + * const moods: MoodDef[] = [ + * { id: 'peps', label: 'Peps', icon: 'i-lucide-sun', color: '#d44a10', isDark: false }, + * { id: 'grave', label: 'Grave', icon: 'i-lucide-shield', color: '#d8a030', isDark: true }, + * ] + * const { moods: list, currentMood, setMood, initMood } = useMood(moods) + */ +export function useMood(moods: MoodDef[], options: UseMoodOptions = {}) { + const { + storageKey = 'yvv_mood', + defaultId = moods[0]?.id ?? '', + syncColorMode = true, + lightDarkClasses = false, + } = options + + function applyMood(moodId: string) { + if (import.meta.server) return + const mood = moods.find(m => m.id === moodId) + if (!mood) return + + const html = document.documentElement + + // classe .mood- + moods.forEach(m => html.classList.remove(`mood-${m.id}`)) + html.classList.add(`mood-${moodId}`) + + // variables CSS injectées (si le projet en fournit) + if (mood.vars) { + for (const [k, v] of Object.entries(mood.vars)) { + html.style.setProperty(k, v) + } + } + + // classes palette-light / palette-dark (compat style librodrome) + if (lightDarkClasses) { + html.classList.toggle('palette-light', !mood.isDark) + html.classList.toggle('palette-dark', mood.isDark) + html.style.setProperty('color-scheme', mood.isDark ? 'dark' : 'light') + } + + // synchro color-mode Nuxt (si dispo et demandé) + if (syncColorMode) { + try { + // @ts-expect-error auto-import Nuxt (résolu chez le consommateur) + const colorMode = useColorMode() + colorMode.preference = mood.isDark ? 'dark' : 'light' + } catch { + /* @nuxtjs/color-mode absent : on ignore */ + } + } + + localStorage.setItem(storageKey, moodId) + currentMood.value = moodId + } + + function setMood(moodId: string) { + applyMood(moodId) + } + + function initMood() { + if (import.meta.server) return + const saved = localStorage.getItem(storageKey) + const moodId = saved && moods.some(m => m.id === saved) ? saved : defaultId + applyMood(moodId) + } + + return { + moods, + currentMood: readonly(currentMood), + setMood, + initMood, + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts new file mode 100644 index 0000000..70f7f83 --- /dev/null +++ b/nuxt.config.ts @@ -0,0 +1,5 @@ +// @yvv/nuxt-base — Nuxt layer partagé (mécanismes communs aux projets Nuxt Yvv) +// Consommé via: export default defineNuxtConfig({ extends: ['../yvv-nuxt-base'] }) +// Fournit (auto-importés chez le consommateur) : composables/useMood.ts +// N'impose AUCUN module ni CSS — les projets gardent leur stack et leurs couleurs. +export default defineNuxtConfig({}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..2dd14c4 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@yvv/nuxt-base", + "version": "0.1.0", + "private": true, + "description": "Nuxt layer partagé (composables communs : useMood) pour les projets Nuxt Yvv", + "type": "module", + "main": "./nuxt.config.ts" +}