@oremis/prisme
| Install | |
|---|---|
npm install @oremis/prisme |
|
| Latest Version: | 0.23.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 21, 2026 |
| Links: | GitHub · npm |
Prisme
Prisme est la bibliotheque UI Vue 3 officielle des applications web OREMIS.
Elle fournit des composants, des styles et des tokens de design pour construire des interfaces coherentes dans l'ecosysteme OREMIS.
Requirements
- Node 24 ou plus recent.
- Vue 3.5 ou plus recent.
- Un navigateur moderne compatible avec Tailwind CSS v4.
Typographie
Prisme utilise deux polices, reprises du design system Atmosphere :
- Nunito Sans pour le texte (
--pr-font-sans, applique a tous les composants viareset.css), en police variable : un seul fichier couvre toutes les graisses ; - Poppins pour les grands titres (
--pr-font-heading) :PrTypographydeh1ah4, en-tete de page, titres de carte, de fenetre, de volet et de navbar. Les petits titres (alertes, toasts, listes) restent en Nunito Sans, plus lisible a petite taille.
Importez @oremis/prisme/fonts.css pour les charger : sans cela, les navigateurs retombent silencieusement sur la police systeme (ui-sans-serif/system-ui).
Les polices sont auto-hebergees plutot que chargees depuis Google Fonts : avec un <link> vers fonts.googleapis.com, le navigateur de chaque visiteur transmet son adresse IP a Google, ce qui a deja ete sanctionne en Europe au titre du RGPD (sans consentement prealable). Pour une association dont le public peut etre vulnerable, ce n'est pas acceptable. @oremis/prisme/fonts.css s'appuie sur @fontsource-variable/nunito-sans et @fontsource/poppins (installes avec Prisme) : les fichiers de police sont servis par votre application.
// Avant styles.css. Prisme fournit les polices (@fontsource) : le bundler de l'app sert les fichiers.
import '@oremis/prisme/fonts.css'
import '@oremis/prisme/styles.css'
Avec mountPrismeIsolated(), chargez la police dans le document hote (import ci-dessus dans l'entree JS de la page), pas dans l'option styles : les navigateurs Chromium ignorent les @font-face declares a l'interieur d'un shadow root. Une fois declaree dans la page, la police est utilisable dans le shadow DOM.
Usage
- Installez le package.
npm install @oremis/prisme
- Importez les styles.
import '@oremis/prisme/styles.css'
- Utilisez les composants. L'entree principale ne depend pas de tiptap : seul
PrRichTextEditoren a besoin, et il est expose par une entree dediee (voir plus bas).
<script setup lang="ts">
import { PrButton } from '@oremis/prisme'
</script>
<template>
<PrButton>Continuer</PrButton>
</template>
- Consultez la documentation Storybook.
https://associationoremis.github.io/prisme/
Composants controles (v-model)
La valeur d'un champ passe toujours par un v-model nu (modelValue). Un etat qui n'est pas une valeur de formulaire (ouvert, page, replie) garde son nom, comme dans la primitive reka-ui enveloppee :
| Composant(s) | Prop | v-model |
|---|---|---|
Champs (PrInput, PrSelect, PrCheckbox, PrSwitch, PrToggle...), PrAccordion, PrTabs, PrSortableList (les elements, dans l'ordre) |
modelValue |
v-model="valeur" |
PrCollapsible, PrDialog, PrSheet, PrPopover, PrToast, PrAlertDialog, PrDropdownMenu |
open |
v-model:open="valeur" |
PrPagination, PrDataTable |
page |
v-model:page="valeur" |
PrSidebar, PrAppShell |
collapsed |
v-model:collapsed="valeur" |
Pour un usage non controle (un formulaire Blade, sans etat cote consommateur), donnez la valeur de depart avec default-value, default-checked, default-pressed ou default-open selon le composant. Un model-value sans ecouteur fait la meme chose sur les champs texte (PrInput, PrTextarea) : le champ garde ce qui est tape, meme quand une erreur s'affiche. Dans une vue Blade : <pr-input name="nom" default-value="{{ old('nom', $benevole->nom) }}"> ou <pr-checkbox name="active" default-checked>.
Avant la 0.20, PrCheckbox et PrSwitch utilisaient v-model:checked, PrToggle v-model:pressed : ces noms n'existent plus depuis la 1.0 (voir « Passer de 0.x a 1.0 »).
Textes et langue
Les textes que Prisme ecrit lui-meme (boutons Fermer/Annuler, noms lus par les lecteurs d'ecran, messages vides, erreurs, messages de usePrForm) sont en francais par defaut. L'option messages les remplace, en entier ou en partie :
import { Prisme, prMessagesEn } from '@oremis/prisme'
app.use(Prisme, { messages: prMessagesEn }) // tout en anglais
app.use(Prisme, { messages: { dataTable: { empty: 'Aucun benevole' } } }) // un seul texte
La meme option existe sur registerPrisme(app, { messages }) (Blade) et mountPrismeIsolated(..., { messages }). locale (dans les messages) regle les noms de mois et de jours et le format des nombres. Un prop sur le composant (close-label, placeholder, empty-text...) reste prioritaire pour cette instance. usePrMessages() donne ces textes a un composant de l'app, et le type PrMessages liste toutes les cles.
Formulaires (usePrForm)
usePrForm garde les valeurs et les erreurs d'un formulaire, valide avec des regles (required(), email(), minLength(2)...) et reprend les erreurs d'une reponse Laravel 422.
<script setup lang="ts">
import { email, PrErrorSummary, PrInput, required, usePrForm } from '@oremis/prisme'
const { fields, errors, errorList, handleSubmit } = usePrForm({
name: { initialValue: '', rules: [required()], label: 'Nom' },
email: { initialValue: '', rules: [required(), email()], label: 'Email' },
})
const save = () => handleSubmit(() => axios.post('/benevoles', { name: fields.name.value, email: fields.email.value }))
</script>
<template>
<form @submit.prevent="save">
<PrErrorSummary :errors="errorList" />
<PrInput v-model="fields.name.value" :error="errors.name.value ?? undefined" label="Nom" />
<PrInput v-model="fields.email.value" :error="errors.email.value ?? undefined" label="Email" type="email" />
</form>
</template>
Quand handleSubmit refuse l'envoi (regles ou 422), le focus va au premier champ en erreur : au clavier ou avec un lecteur d'ecran, on arrive directement sur ce qu'il faut corriger. Le champ est cherche dans le formulaire qui a le focus ; avec plusieurs formulaires ou un envoi declenche ailleurs, passez-le : usePrForm(schema, { form: formRef }). focusOnError: false desactive ce comportement.
PrErrorSummary liste les erreurs au-dessus du formulaire, annoncees des leur apparition (role="alert") : « Email : Adresse email invalide » grace au label de chaque champ. En Blade, il prend aussi des messages simples : <pr-error-summary :errors='@json($errors->all())'></pr-error-summary>.
Toasts depuis le code (usePrToast)
Placez un PrToastProvider dans le layout de l'app, puis affichez un toast depuis n'importe quel code, composant ou non (un store, un intercepteur HTTP) :
import { usePrToast } from '@oremis/prisme'
const { toast, dismiss } = usePrToast()
const id = toast({ title: 'Benevole ajoute', description: 'Camille MARTIN a rejoint l\'equipe.', variant: 'success' })
toast({ title: 'Connexion perdue', variant: 'danger', actionLabel: 'Reessayer', onAction: reconnect })
Un toast danger reste affiche jusqu'a ce qu'on le ferme, les autres disparaissent apres 5 s (duration). Au-dela de quatre toasts, les plus anciens se ferment. Avec plusieurs PrToastProvider sur la page, seul le premier affiche ces toasts.
PrRichTextEditor
Editeur de texte riche (Vue 3 + tiptap), avec titres, listes, tableaux, images, video YouTube, blocs de code colores, sections repliables et encadres Callout (info/succes/avertissement/danger).
Il est expose par l'entree dediee @oremis/prisme/editor, pas par l'entree principale ni par app.use(Prisme) : il importe tiptap et lowlight, et les garder hors de l'entree principale permet aux apps qui n'utilisent pas l'editeur d'importer @oremis/prisme sans installer ces paquets. Ses styles, eux, restent dans @oremis/prisme/styles.css.
<script setup lang="ts">
import { ref } from 'vue'
import { PrRichTextEditor } from '@oremis/prisme/editor'
const content = ref('<p>Contenu initial</p>')
async function uploadImage(file: File, onProgress?: (percent: number) => void) {
const body = new FormData()
body.append('file', file)
const response = await fetch('/upload', { method: 'POST', body })
// Adaptez selon votre backend : pas de progression reelle possible avec
// `fetch`, un client HTTP capable de suivre l'upload (axios, XHR) permet
// d'appeler `onProgress(percent)` pendant l'envoi.
onProgress?.(100)
const { url } = await response.json()
return url
}
</script>
<template>
<PrRichTextEditor v-model="content" label="Contenu" :upload-image="uploadImage" />
</template>
Ses dependances tiptap (@tiptap/*, lowlight, tiptap-extension-resize-image) sont des peerDependencies optionnelles : installez-les explicitement dans l'application consommatrice (memes versions que celles listees dans package.json#peerDependencies de Prisme) :
npm install @tiptap/vue-3@3.31.3 @tiptap/starter-kit@3.31.3 @tiptap/extension-link@3.31.3 # ... voir peerDependencies
Le node tiptap Callout utilise par l'editeur est aussi exporte separement, pour une app qui monterait son propre editeur tiptap sans passer par PrRichTextEditor :
import { Callout } from '@oremis/prisme/editor' // ou '@oremis/prisme/tiptap/callout'
Le rendu du contenu (tableaux, Callout, blocs de code, sections repliables) est stylise par @oremis/prisme/styles/editor-content.css, importable seul par une vue de lecture seule qui n'a pas besoin du reste de Prisme :
import '@oremis/prisme/styles/editor-content.css'
<div class="pr-editor-content" v-html="content" />
Comme PrCombobox/PrTagInput, le contenu de l'editeur n'est pas un <textarea> natif : pour une soumission de formulaire native (<form method="POST"> sans JS de soumission), passez un name — un <input type="hidden"> synchronise avec modelValue est rendu automatiquement.
<PrRichTextEditor v-model="content" name="body" />
Boutons d'action (action)
Pour les actions courantes, action regle l'icone, le libelle et le ton en une prop : un bouton discret (ghost), carre avec seulement l'icone, dont le libelle est le nom accessible et l'infobulle.
<pr-button action="view" size="sm" href="{{ route('users.show', $user) }}"></pr-button>
<pr-button action="edit" size="sm" href="{{ route('users.edit', $user) }}"></pr-button>
<pr-button action="delete" size="sm" @click="confirmDelete"></pr-button>
<pr-button action="add" variant="primary">Ajouter un benevole</pr-button>
Actions disponibles (prButtonActions) : view, edit, delete, remove, reject, approve, add, duplicate, copy, download, upload, search, history, back, more. delete, remove et reject sont rouges (tone="danger"). label, icon, tone et variant remplacent ceux de l'action ; un texte dans le bouton s'affiche apres l'icone. Sans action, icon ajoute une icone Lucide et label nomme un bouton qui n'a qu'une icone.
En-tetes de page et de carte, fiches
<pr-page-header title="{{ $user->full_name }}" description="Benevole depuis {{ $since }}" back-href="{{ route('users.index') }}" back-label="Retour aux benevoles">
<template #actions>
<pr-button action="edit" variant="secondary" href="{{ route('users.edit', $user) }}">Modifier</pr-button>
</template>
</pr-page-header>
<pr-card title="Sanctions en cours" description="Suspensions et avertissements actifs.">
<template #actions><pr-button action="add" size="sm" variant="secondary">Ajouter</pr-button></template>
...
</pr-card>
<pr-description-list :items="@js([['label' => 'Type', 'value' => $type], ['label' => 'Fin prevue', 'value' => $endsAt]])">
<pr-description-item label="Statut"><pr-badge variant="warning">En cours</pr-badge></pr-description-item>
</pr-description-list>
PrPageHeader: lien retour,h1, description, actions a droite (dessous quand la place manque).PrCard:title,description(props ou slots) et slotactions;heading-level(2 par defaut). Sanspadded, l'en-tete garde sa marge et un trait le separe du contenu.PrDescriptionList: paires libelle / valeur d'une fiche, sur deux colonnes, une seule quand la liste est etroite (requete de conteneur). Une valeur vide affiche « Non renseigné » (empty-text).
Recherche cote serveur (PrCombobox)
<pr-combobox label="Ville" name="city_id" search-url="{{ route('cities.search') }}"
:options="@js($city ? [['value' => (string) $city->id, 'label' => $city->name]] : [])"
default-value="{{ old('city_id', $city?->id) }}"></pr-combobox>
La route recoit ?q=... (search-param) et renvoie un tableau de { label, value } ou une collection de ressources Laravel ({ data: [...] }). La recherche part apres debounce ms sans frappe (250) et a partir de min-chars caracteres (2) ; la requete precedente est annulee. options ne sert plus qu'a donner le libelle des valeurs deja choisies. En Vue, :search="(query, signal) => ..." remplace search-url.
Listes a reordonner (PrSortableList)
Une liste dont on change l'ordre a la souris, au doigt ou au clavier. Chaque element a une poignee (un bouton nomme « Deplacer ... ») : on la saisit avec Espace ou Entree, on deplace l'element avec les fleches haut et bas, on le depose avec Espace ou Entree, Echap annule. Chaque etape est lue par les lecteurs d'ecran. Les listes qui partagent un group s'echangent leurs elements : a la souris en glissant de l'une a l'autre, au clavier en depassant le bout d'une liste.
<PrSortableList v-for="module in modules" :key="module.id" v-model="module.lessons" group="lessons"
:list-id="module.id" :aria-label="module.title" @change="save">
<template #default="{ item }"><PrListItem :title="item.title" /></template>
</PrSortableList>
item-key(idpar defaut) identifie chaque element ;item-labelle nomme pour les lecteurs d'ecran (label,titleounamepar defaut).changepart une fois l'element depose, depuis la liste ou il arrive :{ item, from, to, oldIndex, newIndex }, oufromettosont leslist-id. C'est le moment d'enregistrer l'ordre.- Dans un formulaire Blade, sans
v-model:default-valuedonne les elements,nameenvoie leurs cles dans l'ordre (name[]). - Une liste vide montre ou deposer (
empty-textou slotempty). Pres du bord de la fenetre, ou d'un conteneur qui defile, la page defile pendant le glisser. - Les listes sont verticales ; deux listes d'un meme groupe ne s'imbriquent pas l'une dans l'autre (des modules qui se reordonnent et contiennent des lecons : deux groupes differents).
Tableaux sans cadre (PrTable variant="borderless")
Pour les listes d'un tableau de bord, posees directement sur une carte ou sur la page : ni cadre ni bandeau d'en-tete, des en-tetes en gris, un trait leger entre les lignes (pas sous la derniere), la ligne survolee mise en avant. variant="default" garde le tableau encadre.
<PrTable variant="borderless">...</PrTable>
En Blade : <table is="vue:pr-table" variant="borderless">.
Groupes d'entrees dans la sidebar (PrSidebarSection)
Pour ranger les entrees d'une sidebar sous des titres (« Chat », « Administration »...). Quand la sidebar est etroite (repliee sur ordinateur, ou menu ferme sur telephone), le titre laisse la place a un trait entre les groupes, sans trait au-dessus du premier. Les lecteurs d'ecran lisent toujours le titre : chaque section est un groupe nomme (role="group").
<PrSidebar>
<PrSidebarSection label="Chat">
<PrSidebarItem :icon="MessagesSquare" label="Conversations" href="/conversations" />
</PrSidebarSection>
<PrSidebarSection label="Administration">
<PrSidebarItem :icon="ScrollText" label="Journal des acces" href="/journal" />
</PrSidebarSection>
</PrSidebar>
A preferer aux sous-items (PrSidebarSubItem) pour regrouper des pages qu'on ouvre souvent : une sidebar etroite cache les sous-items, alors que les entrees d'une section restent a un clic.
Largeur des fenetres et des volets (size)
PrDialog et PrSheet prennent size="sm" | "md" | "lg" | "xl" (md par defaut, leur largeur d'avant). Une fenetre : 24, 34, 48 ou 64rem, par exemple lg pour un formulaire avec un exemple ou un tableau, xl pour agrandir une image. Un volet lateral : 20, 26, 36 ou 48rem, par exemple lg pour un formulaire avec un editeur de texte riche ; en haut ou en bas, size regle sa hauteur. Les deux restent dans l'ecran sur mobile.
Empiler plusieurs composants Prisme verticalement
PrDataTable et les autres composants larges (formulaires avec beaucoup de champs) utilisent flex flex-col en interne, pas display: grid. Un wrapper consommateur en display: grid sans grid-template-columns explicite autour d'un tel composant produit un debordement (CSS Grid blowout) : la piste implicite se dimensionne sur le contenu le plus large, meme si le conteneur a min-width: 0 (qui ne protege que sa propre boite, pas sa piste interne).
Pour empiler plusieurs composants Prisme verticalement (DataTable, formulaire, etc.), utilisez flex flex-col (ou display: block) plutot qu'une grille nue :
<!-- A eviter : grille sans colonnes explicites -->
<div class="grid">
<PrDataTable ... />
</div>
<!-- Recommande -->
<div class="flex flex-col">
<PrDataTable ... />
</div>
Laravel Blade
Dans une application Laravel avec Vite, Prisme peut etre monte dans une vue Blade via une petite application Vue.
- Installez le package dans l'application Laravel.
npm install @oremis/prisme
- Importez les styles et montez Vue dans
resources/js/app.ts.
import { createApp } from 'vue'
import { PrButton } from '@oremis/prisme'
import '@oremis/prisme/styles.css'
const element = document.getElementById('prisme-app')
if (element) {
createApp({
components: { PrButton },
template: '<PrButton>{{ label }}</PrButton>',
data: () => ({
label: element.dataset.label ?? 'Continuer',
}),
}).mount(element)
}
- Ajoutez le point de montage dans une vue Blade.
@vite('resources/js/app.ts')
<div id="prisme-app" data-label="Enregistrer"></div>
Pour plusieurs composants ou des ecrans plus complets, creez un composant Vue dedie puis montez-le depuis app.ts.
Enregistrement global avec app.use(Prisme)
Vous pouvez aussi ecrire les composants Prisme directement dans le Blade, tant qu'ils sont dans le noeud monte par Vue. Dans ce cas, installez le plugin Prisme avec app.use(Prisme) : il enregistre globalement tous les composants publics de la bibliotheque, utilisables ensuite en HTML/Blade sous leur forme kebab-case.
import { createApp } from 'vue'
import Prisme from '@oremis/prisme'
import '@oremis/prisme/styles.css'
const app = createApp({})
app.use(Prisme)
app.mount('#prisme-app')
@vite('resources/js/app.ts')
<div id="prisme-app">
<pr-button>Enregistrer</pr-button>
<pr-badge>Actif</pr-badge>
<pr-input placeholder="Nom"></pr-input>
<pr-data-table></pr-data-table>
</div>
app.use(Prisme) n'enregistre pas PrRichTextEditor (voir plus haut). Pour utiliser <pr-rich-text-editor> en Blade, ajoutez le plugin PrismeEditor de l'entree dediee (tiptap doit alors etre installe) :
import Prisme from '@oremis/prisme'
import { PrismeEditor } from '@oremis/prisme/editor'
app.use(Prisme).use(PrismeEditor)
Chargement a la demande avec registerPrisme (recommande pour Blade)
app.use(Prisme) met toute la bibliotheque dans le bundle de chaque page. registerPrisme (entree @oremis/prisme/blade) enregistre les composants presents sur toutes les pages tels quels, et tous les autres a la demande : chaque composant devient un petit fichier charge seulement quand une page l'utilise. Plus besoin de script qui genere la liste des composants dans l'application.
import { createApp } from 'vue'
import { PrButton, PrNavbar, PrSidebar } from '@oremis/prisme'
import { prSkeletonFor, registerPrisme } from '@oremis/prisme/blade'
import '@oremis/prisme/styles.css'
const app = createApp({})
registerPrisme(app, {
// Sur toutes les pages : affiches des le premier rendu, sans apparaitre apres coup.
eager: { PrButton, PrNavbar, PrSidebar },
// Optionnel : un squelette pendant le chargement d'un composant (au-dela de `delay`, 150 ms par defaut).
// prSkeletonFor dessine les champs, interrupteurs et declencheurs de dialogue (voir « Squelettes de chargement »).
loading: prSkeletonFor,
})
app.mount('#app')
Montez Vue sur un noeud qui porte v-cloak et cachez-le avec [v-cloak] { display: none } pour eviter de voir les balises brutes avant le montage. Tout texte saisi par quelqu'un doit etre place dans un element v-pre : Vue compile toute la page, et {{ ... }} dans une note serait sinon evalue. En filet de securite pour un texte oublie hors de v-pre, registerPrisme(app, { delimiters: ['[[%', '%]]'] }) change les delimiteurs de Vue : un {{ ... }} saisi reste du texte, et les interpolations Vue de vos vues s'ecrivent [[% x %]] au lieu de @{{ x }}.
Squelettes de chargement
Pendant que Vue demarre, la page cachee par v-cloak peut etre remplacee par sa silhouette grise : barre de navigation, menu et contenu. Ses styles doivent s'afficher avant tout le reste, donc dans le <head>, en ligne. getPrSkeletonStyles() les fournit avec les couleurs des tokens Prisme, en clair et en sombre ; comme le script de theme, ecrivez-les dans une vue Blade a l'installation :
// scripts/generate-prisme.mjs, lance par "postinstall"
import { writeFileSync } from 'node:fs'
import { getPrSkeletonStyles } from '@oremis/prisme'
writeFileSync('resources/views/partials/prisme-skeleton-styles.blade.php', `<style>${getPrSkeletonStyles()}</style>\n`)
<head>
<style>[v-cloak] { display: none; }</style>
@include('partials.prisme-skeleton-styles')
...
</head>
<body>
<div id="app" v-cloak>...</div>
<div class="pr-sk-page">
<p class="pr-sk-sr-only" role="status">Chargement de la page...</p>
<div class="pr-sk-page__navbar" aria-hidden="true"></div>
<div class="pr-sk-page__sidebar" aria-hidden="true">...</div>
<div class="pr-sk-page__content" aria-hidden="true">
<span class="pr-sk pr-sk--title"></span>
<div class="pr-sk-page__card"><span class="pr-sk pr-sk--text"></span></div>
</div>
</div>
</body>
Formes disponibles : pr-sk--text, title, label, field, textarea, button, switch, icon, block, circle, alignees par pr-sk-stack (en colonne) et pr-sk-row (en ligne). .pr-sk-page ne s'affiche que juste apres un #app[v-cloak], et disparait au montage de Vue. Sans sidebar, pr-sk-page__content--centered centre le contenu. L'animation s'arrete pour les personnes qui reduisent les animations. prSkeletonFor reprend les memes classes pour les composants charges a la demande.
Navigation plus rapide entre les pages
En Blade, chaque clic recharge toute la page et Vue la recompile avant de l'afficher. Une fois monte, registerPrisme prepare donc la page suivante, sans rien a ajouter dans l'application :
prefetch('prerender'par defaut) : quand le pointeur reste sur un lien, le navigateur charge deja la page, et avec'prerender'monte meme Vue en arriere-plan : au clic, elle s'affiche tout de suite.'prefetch'telecharge seulement le HTML,falsedesactive. Chrome et Edge uniquement (Speculation Rules), les autres navigateurs l'ignorent. Avec la navigation sans rechargement (par defaut, voir plus bas), le survol precharge la page dans tous les navigateurs, sans Speculation Rules.preload('idle'par defaut) : quand la page n'a plus rien a faire, les composants charges a la demande sont telecharges un par un, pour que les pages suivantes les trouvent en cache, sans squelette. Desactive avec Save-Data.transitions(truepar defaut) : la navbar et la sidebar restent immobiles, seul le contenu fait un fondu (View Transitions entre documents). Uniquement quand la nouvelle page est deja montee, et jamais avec « reduire les animations ».measure(truepar defaut) : la mesureprisme:mount(du debut de la navigation au montage de Vue) apparait dans l'onglet Performance des devtools et enconsole.debug(niveau « Verbose »).
Ne sont jamais precharges : les liens vers un autre site, ceux qui ont download ou une target, ceux dont l'adresse contient logout, et ceux qui portent data-no-prefetch. Le prechargement envoie une vraie requete GET : un lien qui modifie quelque chose en GET (a eviter) doit porter data-no-prefetch.
Navigation sans rechargement (navigation: 'swap', par defaut)
Par defaut, un clic sur un lien ou l'envoi d'un formulaire ne recharge plus la page, dans tous les navigateurs. Prisme recupere la page suivante (le serveur renvoie toujours sa page Blade complete) et remplace seulement le contenu de #app : la navbar et la sidebar restent en place avec leur etat (sidebar repliee, defilement), leurs props sont mises a jour (element actif, badges), et le contenu (.pr-shell-grid__content, sinon tout #app) est reconstruit. Le titre, l'adresse, le token CSRF, les boutons Precedent/Suivant et le defilement suivent ; le focus passe au contenu et les lecteurs d'ecran entendent le titre de la nouvelle page. Le survol d'un lien precharge la page (sans Speculation Rules, inutiles ici), et sur un ecran tactile, le simple contact du doigt sur le lien, juste avant le clic.
Rien a changer dans les controleurs ni dans les vues. La page se recharge normalement :
- si la page suivante apporte un script ou une feuille de style que la page actuelle n'a pas (
@push('scripts'), carte, editeur...) ; - si la reponse n'est pas du HTML (un export), vient d'un autre site (session expiree renvoyee au SSO) ou n'arrive pas ;
- pour les liens
download,target,logout, ou marquesdata-prisme-reload(sur le lien ou un parent).
Les formulaires sont envoyes de la meme facon, sans rechargement : Prisme envoie les memes champs que le navigateur (bouton clique, token CSRF, fichiers, @method('DELETE') compris) et affiche la page vers laquelle le serveur redirige. Apres un succes, la page suivante avec son message flash ; apres une validation ratee, la meme page avec les erreurs et les anciennes valeurs, sans nouvelle entree dans l'historique, le defilement garde et le focus sur le premier champ en erreur (aria-invalid). Un formulaire GET (recherche, filtres) se comporte comme un lien vers son adresse. Un double clic n'envoie le formulaire qu'une fois. Pendant l'envoi, le formulaire porte aria-busy et le bouton clique aria-disabled et data-prisme-submitting : au-dela de 150 ms, un PrButton affiche le meme indicateur qu'avec loading (un autre bouton, seulement le curseur d'attente).
Un formulaire n'est jamais envoye deux fois. Quand sa reponse ne peut pas remplacer le contenu :
- un fichier (un export) est telecharge, la page reste en place ;
- une redirection vers un autre site (SSO) ou vers une page d'une autre mise en page est suivie normalement ;
- une page d'erreur renvoyee par le formulaire lui-meme (session expiree 419, erreur 500) est affichee telle quelle.
Seule exception : si la reponse n'arrive pas (connexion coupee, ou redirection vers un autre site que le navigateur refuse de lire), le navigateur renvoie lui-meme le formulaire. Un formulaire qui redirige vers un autre site (connexion OAuth, paiement) doit donc porter data-prisme-reload, sinon son action s'execute deux fois.
Le navigateur garde l'envoi des formulaires deja geres par l'app (@submit.prevent, un onsubmit="return confirm(...)" refuse), de ceux qui ont une target, qui vont vers un autre site ou vers une adresse contenant logout, et de ceux marques data-prisme-reload (sur le formulaire, un parent ou le bouton).
Un script de l'app qui agit sur la page a son chargement (DOMContentLoaded, querySelector apres le montage) ne se relance pas quand la page change : chargez-le avec @push('scripts') (la page se recharge alors normalement) ou marquez ses liens data-prisme-reload.
navigation: false coupe la navigation sans rechargement pour une app. Pour comparer ou ecarter une cause dans son propre navigateur, dans la console : localStorage.setItem('prisme:navigation', 'off') puis recharger (localStorage.removeItem('prisme:navigation') pour revenir).
Pagination d'une liste Laravel
Avec page-param, PrPagination affiche de vrais liens au lieu de boutons : la navigation sans rechargement les suit, la page ne se recharge pas. Chaque lien reprend l'adresse actuelle en changeant seulement ce parametre, donc les filtres et la page d'une autre liste sont gardes. fragment ajoute une ancre aux liens : la page suivante s'ouvre sur la liste, pas en haut de la page.
Pour l'utiliser partout a la place de la pagination de Laravel (en anglais, et avec un theme sombre qui suit le systeme plutot que Prisme), une vue suffit :
{{-- resources/views/pagination/prisme.blade.php --}}
@if ($paginator->hasPages())
<pr-pagination
:page="{{ $paginator->currentPage() }}"
:page-count="{{ $paginator->lastPage() }}"
page-param="{{ $paginator->getPageName() }}"
aria-label="{{ $label ?? 'Pagination' }}"
@if ($paginator->fragment()) fragment="{{ $paginator->fragment() }}" @endif
></pr-pagination>
@endif
// AppServiceProvider::boot()
Paginator::defaultView('pagination.prisme');
{{ $users->fragment('users')->links() }} ramene alors sur l'element id="users". Plusieurs paginations sur une meme page ont chacune besoin de leur nom : $users->links(data: ['label' => 'Pages des bénévoles']).
app.use(Prisme) ou imports nommes ?
app.use(Prisme)enregistre en une seule fois tous les composants Prisme comme composants globaux de l'application. C'est le plus adapte quand les composants sont utilises directement dans du HTML/Blade (pas de<script setup>pour les declarer), au prix d'inclure l'integralite de la bibliotheque dans le bundle.import { PrButton } from '@oremis/prisme'importe uniquement les composants reellement utilises et beneficie du tree-shaking. C'est le choix recommande dans des composants Vue (SFC) ou seule une partie de Prisme est necessaire.
Les deux approches sont interchangeables et peuvent cohabiter dans la meme application.
Eviter le flash de theme (FOUC) en Blade
Dans une SPA, usePrTheme() applique le theme des le montage de Vue. En Blade, le HTML est deja affiche par le navigateur avant que le bundle Vue ne s'execute : sans intervention, la page s'affiche brievement dans le theme par defaut avant de basculer vers le theme reellement choisi (sombre par ex.).
Pour l'eviter, il faut qu'un <script> synchrone, non-module, s'execute dans le <head> du layout Blade avant le premier paint — donc avant meme @vite(...), dont les scripts sont charges en type="module" (differe par le navigateur). getPrThemeInitScript() fournit justement ce script : une chaine de JS vanilla, sans dependance, qui lit la preference stockee et pose data-pr-theme/color-scheme sur <html> immediatement.
Le plus simple est de l'ecrire une fois dans un fichier statique servi tel quel (pas de build Vite dessus), a partir d'un petit script Node execute a la racine du projet :
// scripts/write-theme-init-script.mjs
import { writeFileSync } from 'node:fs'
import { getPrThemeInitScript } from '@oremis/prisme'
writeFileSync('public/prisme-theme-init.js', getPrThemeInitScript())
<head>
<script src="{{ asset('prisme-theme-init.js') }}"></script>
@vite(['resources/css/app.css', 'resources/js/app.ts'])
</head>
Sans cette etape, getPrThemeInitScript() — bien que fournie a cet effet — n'a aucun effet : le flash de theme qu'elle est censee eviter se produira quand meme, puisque usePrTheme()/setPrTheme() ne s'executent qu'apres l'hydratation du bundle Vue, donc apres le premier paint.
Isoler Prisme dans une page qui utilise un autre framework CSS (Bootstrap, etc.)
@oremis/prisme/styles.css inclut un reset Tailwind non prefixe (.flex, .p-4, .hidden, .grid, etc. sans espace de nom). Importe normalement (import global ou @vite(...)) dans une page Blade qui charge deja un autre framework CSS non-Tailwind (Bootstrap par exemple), ce reset s'applique a toute la page, pas seulement au composant monte : il peut silencieusement casser l'apparence ou le comportement d'elements totalement sans rapport ailleurs sur la meme page. Incident reel : la sidebar d'une app consommatrice s'est retrouvee bloquee en position repliee des qu'un composant Prisme etait monte sur la meme page.
Deux solutions, selon le besoin.
1. @oremis/prisme/styles-scoped.css (le plus simple). La meme feuille, mais qui ne touche jamais la page hote : aucune regle sur html, body ou les elements nus, et chaque classe utilitaire ne s'applique qu'aux elements Prisme et a leur contenu (.collapse, .container, .table... de Bootstrap restent intacts). Les composants s'ecrivent ensuite normalement dans la page, sans shadow DOM.
import '@oremis/prisme/styles-scoped.css' // au lieu de styles.css
Limite : les styles de la page hote (par exemple le label ou le button de Bootstrap) peuvent encore atteindre l'interieur des composants. Pour un composant complexe (editeur de texte riche), preferez la solution 2.
2. Shadow DOM. Montez le composant dans un shadow DOM plutot que dans le document courant, avec mountPrismeIsolated() : le style de Prisme reste alors entierement confine a l'interieur, sans jamais pouvoir affecter le reste de la page (et inversement, le CSS de l'hote ne peut pas polluer l'interieur du composant).
import { mountPrismeIsolated } from '@oremis/prisme'
import prismeStyles from '@oremis/prisme/styles.css?inline' // Vite: recupere le CSS en chaine, sans l'injecter dans <head>
import MyPrismeComponent from './MyPrismeComponent.vue'
const host = document.getElementById('prisme-app')
if (host) {
mountPrismeIsolated(MyPrismeComponent, host, { styles: prismeStyles })
}
mountPrismeIsolated() renvoie { app, shadowRoot, unmount } si vous avez besoin d'aller plus loin (demonter le composant, inspecter le shadow root, etc.).
Theme
Dans le shadow root, les tokens (--pr-color-*, --pr-space-*...) sont declares sur :host, c'est-a-dire l'element host passe a mountPrismeIsolated(). Le data-pr-theme que usePrTheme() pose sur <html> n'y est pas visible : c'est l'attribut data-pr-theme de l'element hote qui choisit le theme. L'option theme le pose pour vous :
theme |
Effet |
|---|---|
'light' / 'dark' |
Theme fixe, quel que soit celui de la page hote. |
'document' |
Recopie <html data-pr-theme> et suit ses changements (theme de la page Prisme hote, PrThemeToggle). Sans cet attribut sur <html>, suit la preference du systeme. |
| omise | L'attribut de l'element hote n'est pas touche. Sans attribut, le composant suit la preference du systeme (prefers-color-scheme). |
// Page Bootstrap sans mode sombre : forcer le theme clair.
mountPrismeIsolated(MyPrismeComponent, host, { styles: prismeStyles, theme: 'light' })
Pour surcharger un token dans le shadow root, ajoutez la regle dans styles, apres le CSS de Prisme, sur :host pour le theme clair et :host([data-pr-theme="dark"]) pour le sombre (plus specifique, il l'emporte sur une simple regle :host).
Styles de vos propres composants
Seul le CSS passe dans styles est injecte dans le shadow root. En build librairie/embed (Vite build.lib), les blocs <style> de vos propres SFC sont extraits dans une feuille separee, injectee (ou non) dans <head>, jamais dans le shadow root : ils n'y ont aucun effet. Mettez ce CSS dans un fichier importe en ?inline et concatenez-le a celui de Prisme, comme le fait OREMIS Chat :
import prismeStyles from '@oremis/prisme/styles.css?inline'
import launcherStyles from './launcher.css?inline'
mountPrismeIsolated(ChatLauncher, host, {
styles: `${prismeStyles}\n${launcherStyles}`,
theme: 'light',
})
La police se charge, elle, dans le document hote (voir Typographie).
Attention avec PrRichTextEditor (et tout autre composant qui accepte un prop name pour rendre un <input> cache destine a une soumission de formulaire native) : un element place dans un shadow DOM n'est pas inclus dans la soumission native du <form> ancetre. Ne passez pas name dans ce cas : gardez un vrai <input type="hidden"> dans le DOM normal (hors du shadow root), et synchronisez-le vous-meme via v-model/un callback plutot que de compter sur l'auto-rendu du composant.
Versions et compatibilite
Prisme suit le versionnage semantique depuis la 1.0 :
- Correctif (1.0.x) : un bug corrige, sans rien changer a l'API.
- Mineure (1.x.0) : un ajout (composant, prop, option) qui ne casse rien. Un nom qui change y garde l'ancien en alias deprecie : il marche toujours, avec un avertissement unique dans la console qui donne le nouveau nom.
- Majeure (x.0.0) : le retrait des alias deprecies et les changements cassants, listes dans un guide de migration.
Une montee de version mineure ne demande donc rien aux apps ; il suffit de lire les avertissements de la console pour preparer la majeure suivante. Les versions de test (1.0.0-rc.1) sont publiees sous le tag npm next : npm install @oremis/prisme@next.
L'API publique, couverte par cette promesse :
- ce qu'exportent
@oremis/prisme,@oremis/prisme/blade,@oremis/prisme/editoret@oremis/prisme/registry; - les chemins
@oremis/prisme/components/<categorie>/<Nom>(atoms/Button,molecules/Toast...), stables meme si un composant change de categorie ; - les props, evenements, slots et balises Blade (
<pr-xxx>) documentes, les variables CSS--pr-*et les classespr-xxxdes composants.
Ne sont pas publics : les fichiers sous dist/internal/, les classes utilitaires pr:* et la structure HTML interne d'un composant.
Passer de 0.x a 1.0
La 1.0 retire les alias deprecies entre la 0.20 et la 0.22. Une app en 0.23 qui n'affiche aucun avertissement [prisme] dans la console n'a rien a changer. Sinon :
| Avant | Apres |
|---|---|
v-model:checked sur PrCheckbox et PrSwitch |
v-model (ou default-checked en Blade) |
v-model:pressed sur PrToggle |
v-model (ou default-pressed) |
variant="default" sur PrToast |
variant="neutral" |
label sur PrSpinner, PrSidebar, PrNavigationMenu, PrToastProvider, PrThemeToggle |
aria-label |
:data sur PrDataTable |
:rows |
is-loading sur PrDataTable |
loading |
:display-pagination="false" |
hide-pagination |
:display-view-options="false" |
hide-view-options |
:hide-selected-rows-count="false" |
show-selected-rows-count |
{ label: 'Supprimer', danger: true } dans row-actions |
{ label: 'Supprimer', tone: 'danger' } |
Changements de comportement a connaitre en venant d'une version plus ancienne que la 0.23 :
- 0.22 : un
model-valuesans ecouteur surPrInput/PrTextareaest une valeur de depart (le champ garde ce qui est tape) ;no-results-messagedePrDataTablene s'affiche plus que lorsque la recherche ne trouve rien. - 0.23 : un champ lie par
v-modelaffiche exactement sa valeur (undefined= vide) ;PrNumberInputenvoienullquand il est vide ; un@confirmasynchrone dePrAlertDialoggarde la fenetre ouverte jusqu'a la fin de la requete.
Pour reperer les anciens noms dans une app : grep -rnE "v-model:(checked|pressed)|is-loading|display-pagination|display-view-options|hide-selected-rows-count|danger: true|variant=\"default\"" resources/ src/, puis verifier les label des cinq composants ci-dessus.
Development
npm install
npm run storybook
Build de la librairie :
npm run build
Build du Storybook statique :
npm run build-storybook
Classes Tailwind des composants : prefixe pr:
Depuis la 0.18, les classes Tailwind des composants portent le prefixe pr: (pr:flex, pr:sm:top-auto, pr:data-[state=open]:bg-[...]), configure par prefix(pr) dans src/styles/prisme.css. Prisme ne genere donc aucune classe du meme nom que celles d'une app (.hidden, .top-0...) : aucune ne peut ecraser l'autre. Les classes BEM (pr-button, pr-data-table__filter) restent sans prefixe. Une classe sans pr: dans un composant n'est pas generee : node scripts/prefix-classes.mjs liste celles qui auraient ete oubliees (--write les corrige).
Publier une version
On travaille sur develop. Pour publier :
- Sur
develop, un commitchore(release): X.Y.Zqui changeversiondanspackage.json(etpackage-lock.json, vianpm version X.Y.Z --no-git-tag-version) et renomme la section## Non publiedeCHANGELOG.mden## X.Y.Z (AAAA-MM-JJ). Sans cette section, la publication echoue. - PR de
developversmain, puis merge. - Le workflow
npm-publish.ymlteste, construit et publieX.Y.Zsur npm, puis cree le tagvX.Y.Zet une release GitHub avec la section du changelog.
Un merge qui ne change pas la version ne publie rien. Ne creez pas le tag a la main : le workflow s'en charge. Une version de test (1.0.0-beta.1) est publiee sous le tag npm next, pas latest. Le workflow ci.yml lance les memes verifications sur chaque PR et sur develop.
License
Prisme est distribue sous licence MIT. Voir LICENSE.
Version History
| Version | Released | License |
|---|---|---|
| 0.23.0 | MIT | |
| 0.22.0 | MIT | |
| 0.21.0 | MIT | |
| 0.20.0 | MIT | |
| 0.19.3 | MIT | |
| 0.19.2 | MIT | |
| 0.19.1 | MIT | |
| 0.19.0 | MIT | |
| 0.18.1 | MIT | |
| 0.18.0 | MIT | |
| 0.17.0 | MIT | |
| 0.16.1 | MIT | |
| 0.16.0 | MIT | |
| 0.15.0 | MIT | |
| 0.14.0 | MIT | |
| 0.13.1 | MIT | |
| 0.13.0 | MIT | |
| 0.12.0 | MIT | |
| 0.11.0 | MIT | |
| 0.10.1 | MIT | |
| 0.9.0 | MIT | |
| 0.8.0 | MIT | |
| 0.7.0 | MIT | |
| 0.6.0 | MIT | |
| 0.5.0 | MIT | |
| 0.3.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.3 | MIT | |
| 0.1.0 | MIT |
Pre-releases (7)
| Version | Released | License |
|---|---|---|
| 1.0.0-rc.7 | MIT | |
| 1.0.0-rc.6 | MIT | |
| 1.0.0-rc.5 | MIT | |
| 1.0.0-rc.4 | MIT | |
| 1.0.0-rc.3 | MIT | |
| 1.0.0-rc.2 | MIT | |
| 1.0.0-rc.1 | MIT |