iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Intermédiaire

OpenPatch

Créer et publier une librairie Vue 3 — composant de patch notes avec mode Vue, composable, mode standalone CDN, personnalisation CSS et publication npm.

Vue 3TypeScriptViteTailwind CSS v4
GitHub
01Setup Vite + Vue 3 + TypeScript02Types TypeScript et structure de données03Le composant OpenPatch.vue04Versioning avec localStorage05Le composable useOpenPatch06Mode standalone CDN07Build dual-mode et publication npm
Chapitre 3·30 min

Le composant OpenPatch.vue

Le composant principal. Il gère l'affichage de la modal, les transitions, la personnalisation CSS via custom properties, et expose une API impérative via defineExpose.

Props et émissions

src/components/OpenPatch.vue — <script setup>
import { ref, computed, onMounted, watch } from 'vue'
import type { PatchNotesConfig, CSSCustomization } from '../types/settings'
 
interface Props {
  projectId: string
  version: string
  patchnotes: PatchNotesConfig
  title?: string
  closeButtonText?: string
  forceShow?: boolean
  manual?: boolean
  cssCustomization?: CSSCustomization
}
 
const props = withDefaults(defineProps<Props>(), {
  title: '🎉 Nouveautés',
  closeButtonText: 'Compris !',
  forceShow: false,
  manual: false
})
 
const emit = defineEmits<{
  (e: 'close'): void
  (e: 'shown'): void
}>()

manual permet de désactiver l'affichage automatique au montage — utile quand on veut contrôler le moment d'affichage depuis le parent.

Template : modal avec Teleport

src/components/OpenPatch.vue — <template>
<template>
  <Teleport to="body">
    <Transition name="modal">
      <div
        v-if="isVisible"
        class="fixed inset-0 z-50 flex items-center justify-center p-4"
        @click.self="handleClose"
      >
        <div class="absolute inset-0 bg-slate-900/30 backdrop-blur-[2px]"></div>
 
        <div
          class="relative w-full max-w-xl max-h-[80vh] rounded-lg shadow-xl overflow-hidden border op-modal"
          :style="customStyles"
        >
          <!-- Header sticky -->
          <div class="sticky top-0 z-10 border-b px-6 py-4 op-header">
            <h2 class="text-lg font-semibold op-title">{{ title }}</h2>
            <p class="text-xs mt-0.5 op-version">Version {{ version }}</p>
          </div>
 
          <!-- Contenu scrollable -->
          <div class="overflow-y-auto max-h-[calc(80vh-140px)] px-6 py-5">
            <div v-for="(section, i) in patchnotes.sections" :key="i">
              <span class="... op-section-tag">{{ section.title }}</span>
              <ul>
                <li
                  v-for="(item, j) in section.items"
                  :key="j"
                  class="op-item"
                >
                  {{ item }}
                </li>
              </ul>
            </div>
          </div>
 
          <!-- Footer sticky -->
          <div class="sticky bottom-0 border-t px-6 py-4 op-footer">
            <button @click="handleClose" class="w-full op-button">
              {{ closeButtonText }}
            </button>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<Teleport to="body"> déplace la modal en dehors du DOM de l'application — elle s'affiche au-dessus de tout sans dépendre du z-index du parent. @click.self sur l'overlay ferme la modal sans propager vers le contenu.

CSS custom properties

La personnalisation ne passe pas par des classes Tailwind dynamiques — trop complexe à purger. On utilise des CSS custom properties injectées en inline style :

const customStyles = computed(() => {
  if (!props.cssCustomization) return {}
  const styles: Record<string, string> = {}
  if (props.cssCustomization.primaryColor)
    styles['--op-primary-color'] = props.cssCustomization.primaryColor
  if (props.cssCustomization.backgroundColor)
    styles['--op-bg-color'] = props.cssCustomization.backgroundColor
  // ... autres propriétés
  return styles
})

Les valeurs par défaut sont définies dans le CSS scoped :

<style scoped>
.op-modal {
  --op-bg-color: #ffffff;
  --op-text-color: #334155;
  --op-primary-color: #0f172a;
  --op-border-color: #e2e8f0;
  --op-button-bg-color: #0f172a;
  --op-button-text-color: #ffffff;
}

Transition d'entrée/sortie

<style scoped>
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}
 
.modal-enter-active > div:last-child,
.modal-leave-active > div:last-child {
  transition: transform 0.3s ease;
}
 
.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}
 
.modal-enter-from > div:last-child,
.modal-leave-to > div:last-child {
  transform: scale(0.95) translateY(-20px);
}

L'overlay fade in/out, la card scale + slide simultanément. Deux éléments, deux animations indépendantes dans la même transition.

API impérative exposée

const show = () => { isVisible.value = true; emit('shown') }
const hide = () => { isVisible.value = false }
const reset = () => { localStorage.removeItem(getStorageKey()) }
 
defineExpose({ show, hide, reset, isVisible })

defineExpose rend ces méthodes accessibles via templateRef.value.show() depuis le parent, et via instance.show() depuis le mode standalone.

Précédent
Types TypeScript et structure de données
Suivant
Versioning avec localStorage

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main