@bjalon/deck-runtime est une librairie React pour compiler, afficher, presenter, editer et exporter des decks YAML/Markdown dans une application metier.
L'objectif n'est pas de remplacer un outil de slides generaliste. La librairie fournit un runtime embarquable pour des supports structures, themables, editables, versionnables et rendus de maniere coherente dans les produits Qastia.
Exemple en ligne : https://bjalon.github.io/qastia-deck/
- Decrire un deck dans un format source lisible, base sur YAML et Markdown.
- Compiler ce format en modele type exploitable par une application React.
- Afficher un deck en preview integree, viewer ou presentation plein ecran.
- Fournir un studio d'edition embarque avec formulaire, YAML et preview de slide.
- Centraliser layouts, themes, transitions et renderers.
- Supporter les diagnostics de compilation pour guider l'edition et le debug.
- Gerer des valeurs globales reutilisees par les slides, avec override local.
- Proposer une sauvegarde locale, des versions, une recuperation de brouillon et un export PDF.
La librairie est pensee pour les applications qui doivent generer, editer ou afficher des supports dans un workflow metier :
- supports d'atelier ou de coaching client ;
- restitutions structurees a partir de donnees applicatives ;
- decks editables dans un espace client ou un back-office ;
- preview immediate pendant l'edition ;
- presentation plein ecran depuis une page applicative ;
- impression ou telechargement PDF controle par l'application.
Pour un projet qui consomme le package publie sur GitHub Packages :
@bjalon:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}npm install @bjalon/deck-runtimePendant le developpement local avec qastia-coaching, il est aussi possible d'utiliser une dependance locale :
{
"dependencies": {
"@bjalon/deck-runtime": "file:../qastia-deck"
}
}La feuille de style doit etre importee par l'application :
import "@bjalon/deck-runtime/styles.css";Un deck est une source YAML. Les contenus textuels des slots sont en Markdown.
version: 1
kind: deck
metadata:
title: Support integre
theme:
id: qastia-coaching
defaults:
slots:
eyebrow:
markdown: Atelier CODIR
footer:
markdown: Sophie Jalon Conseil
slides:
- id: ouverture
layout: cover
slots:
title:
markdown: Aligner les decisions
subtitle:
markdown: Un support editable directement dans l'espace client.
- id: cadrage
layout: title-body
slots:
title:
markdown: Cadrage
body:
markdown: |
- Objectifs
- Decisions attenduesmetadata.title est le titre du slideshow. Dans DeckStudio, il est editable directement par double-clic dans le rail de gauche.
defaults.slots permet de definir des valeurs globales, aujourd'hui utilisees notamment pour eyebrow et footer. Dans le formulaire de slide, ces champs apparaissent en valeur heritee. L'utilisateur peut cocher l'override pour remplacer la valeur globale localement sur une slide.
Studio complet non controle :
import { DeckStudio, type DeckSource } from "@bjalon/deck-runtime";
import "@bjalon/deck-runtime/styles.css";
const source: DeckSource = {
content: `
version: 1
kind: deck
metadata:
title: Demo
slides:
- id: cover
layout: cover
slots:
title:
markdown: Demo
`,
};
export function DeckPage(): React.ReactElement {
return <DeckStudio deckId="demo" initialValue={source} />;
}Studio controle par l'application :
import { useState } from "react";
import { DeckStudio, type DeckSource } from "@bjalon/deck-runtime/editor";
import "@bjalon/deck-runtime/styles.css";
export function ControlledDeckStudio({ initialSource }: { readonly initialSource: DeckSource }) {
const [source, setSource] = useState(initialSource);
return (
<DeckStudio
mode="controlled"
deckId="client-session"
value={source}
onChange={(nextSource) => setSource(nextSource)}
/>
);
}Preview simple avec DeckShow :
import { useEffect, useState } from "react";
import {
DeckShow,
compileDeck,
defaultDeckRuntime,
type CompiledDeck,
type DeckSource,
} from "@bjalon/deck-runtime";
import "@bjalon/deck-runtime/styles.css";
export function DeckPreview({ source }: { readonly source: DeckSource }): React.ReactElement {
const [deck, setDeck] = useState<CompiledDeck | null>(null);
useEffect(() => {
compileDeck(source, {
runtime: defaultDeckRuntime,
mode: "viewer",
locale: "fr-FR",
}).then((result) => {
setDeck(result.status === "valid" || result.status === "degraded" ? result.deck : null);
});
}, [source]);
return deck ? <DeckShow deck={deck} mode="embedded" /> : <p>Deck invalide.</p>;
}DeckStudio: studio d'edition embarque.DeckShow: viewer/preview de deck compile.DeckPresentationOverlay: presentation plein ecran autonome.PrintDeck: rendu print base sur les memes slides compilees.DeckPdfDownloadButton: bouton React pret a l'emploi pour telecharger un PDF.DeckPdfExportHostetuseDeckPdfExport: primitives pour composer une UI PDF custom.DebugDeckFallback: fallback quand la source est invalide.compileDeck: compilation YAML/Markdown vers un modele type.createDeckRuntime: runtime personnalise avec layouts, renderers, themes et transitions.
DeckStudio est la facade d'edition. Il regroupe :
- rail de slides a gauche ;
- edition par formulaire ;
- edition YAML ;
- preview de la slide active ;
- diagnostics ;
- stockage local optionnel ;
- versions locales ;
- edition des valeurs globales ;
- actions de slides ;
- drag and drop de l'ordre des slides.
- Double-clic sur le titre du slideshow dans le rail : edition de
metadata.title. - Blur ou
Entreesur ce titre : sauvegarde. Echapsur ce titre : annulation.Add: ajoute une slide apres la slide selectionnee et selectionne la nouvelle slide.Duplicate: duplique la slide selectionnee juste apres elle.Delete: supprime la slide selectionnee.- Glisser-deposer dans le rail : change l'ordre des slides dans le YAML.
Global: ouvre une popin pour modifier les valeurs globaleseyebrowetfooter.- Checkbox d'override sur un champ herite : remplace la valeur globale pour la slide courante.
Les vues visibles du studio sont configurables :
<DeckStudio
deckId="demo"
initialValue={source}
options={{
editing: {
defaultMode: "form",
viewModes: ["form", "source", "preview"],
allowYamlMode: true,
allowPreviewMode: true,
allowLayoutChange: true,
},
}}
/>Valeurs possibles pour viewModes :
form: formulaire structure ;source: YAML brut ;preview: rendu de la slide active.
viewModes permet de fixer la liste et l'ordre des vues. Les flags plus fins
permettent ensuite d'activer ou non chaque vue optionnelle :
<DeckStudio
deckId="demo"
initialValue={source}
options={{
editing: {
allowYamlMode: false,
allowPreviewMode: true,
},
}}
/>form reste la vue de base du studio. Si allowYamlMode et
allowPreviewMode sont tous les deux a false, la liste deroulante de choix de
vue n'est pas affichee. allowSourceMode reste supporte comme alias historique
de allowYamlMode.
DeckStudio peut afficher un bouton de preview plein ecran dans sa barre
d'actions. Il ouvre le deck compile via le meme moteur que
DeckPresentationOverlay.
<DeckStudio
deckId="demo"
initialValue={source}
features={{
allowFullscreenPreview: true,
}}
options={{
presentation: {
buttonLabel: "Plein écran",
options: {
fullscreen: {
strategy: "browser-fullscreen",
closeOnEscape: true,
},
controls: {
visibility: "auto",
autoHideDelayMs: 1800,
},
},
},
}}
/>Pour masquer ce bouton :
<DeckStudio deckId="demo" initialValue={source} options={{ presentation: false }} />Les panneaux du studio peuvent etre actives, masques ou configures :
<DeckStudio
deckId="demo"
initialValue={source}
options={{
panels: {
slideRail: {
visibleDefault: true,
userToggle: true,
widthPx: 220,
maxVisibleItems: 6,
itemHeightPx: 76,
thumbnailMode: "compact",
allowReorder: true,
allowAddDelete: true,
},
diagnostics: {
visibleDefault: true,
userToggle: true,
placement: "bottom",
},
activeSlidePreview: false,
inspector: {
visibleDefault: true,
widthPx: 340,
},
versionHistory: false,
},
}}
/>Le rail de slides est volontairement compact. maxVisibleItems fixe le nombre
approximatif de slides visibles avant scroll local, et itemHeightPx garantit
une hauteur stable des cartes. S'il contient beaucoup de slides, il scrolle
localement pour garder le focus sur le panneau d'edition principal.
Les renderers declares dans createDeckRuntime sont propages jusqu'aux layouts
par defaut :
const runtime = createDeckRuntime({
renderers: [
{
kind: "markdown",
render: ({ node }) => <CustomMarkdown node={node} />,
},
],
});
const result = await compileDeck(source, {
runtime,
mode: "viewer",
locale: "fr-FR",
});Le deck compile conserve le registry utilise pendant la compilation, puis
DeckShow, DeckStudio, DeckPresentationOverlay et PrintDeck le transmettent
au rendu.
<DeckStudio
deckId="demo"
initialValue={source}
features={{
allowAddSlide: true,
allowDuplicateSlide: true,
allowDeleteSlide: true,
allowReorderSlides: true,
allowLayoutChange: true,
allowRawSourceEdit: true,
allowVersionRestore: true,
}}
/>allowReorderSlides est active par defaut dans la configuration courante.
DeckShow affiche un deck compile sans dependre du studio.
<DeckShow
deck={deck}
mode="embedded"
controls={{
placement: "bottom",
showPreviousNext: true,
showCounter: true,
showPresentationButton: true,
showPresentationControlsModeSelect: true,
presentationControlsMode: "auto",
onPresentationControlsModeChange: setPresentationControlsMode,
}}
onRequestPresentation={(event) => {
openPresentation(event.slideId);
}}
/>DeckShow ne monte pas lui-meme DeckPresentationOverlay. Il emet seulement onRequestPresentation, ce qui laisse l'application hote gerer l'ouverture.
Navigation clavier :
mode="viewer": navigation globale ;mode="embedded": navigation limitee au focus dans le viewer ;keyboardNavigation={false}: navigation clavier desactivee.
Les champs input, textarea, select, contenteditable et zones similaires ne sont pas interceptes par la navigation du viewer.
import { DeckPresentationOverlay } from "@bjalon/deck-runtime/presentation";
<DeckPresentationOverlay
deck={deck}
open={presentationOpen}
initialSlideId={selectedSlideId}
options={{
fullscreen: {
strategy: "browser-fullscreen",
closeOnEscape: true,
},
controls: {
visibility: "auto",
autoHideDelayMs: 1800,
},
hint: {
showWhenControlsHidden: true,
text: "Fleches gauche/droite : naviguer - Escape : quitter",
position: "bottom-right",
},
}}
onOpenChange={(event) => setPresentationOpen(event.open)}
/>Modes de controles :
visible: boutons de navigation toujours visibles ;hidden: pas de boutons, seulement le hint si configure ;auto: controles visibles puis masques automatiquement.
Strategies fullscreen :
browser-fullscreen: tente l'API navigateur ;overlay: plein ecran CSS fiable sans API navigateur.
Bouton pret a l'emploi :
import { DeckPdfDownloadButton } from "@bjalon/deck-runtime/pdf";
import "@bjalon/deck-runtime/styles.css";
export function PdfAction({ deck }: { readonly deck: CompiledDeck }): React.ReactElement {
return (
<DeckPdfDownloadButton deck={deck}>
Telecharger PDF
</DeckPdfDownloadButton>
);
}UI custom avec hook :
import { DeckPdfExportHost, useDeckPdfExport } from "@bjalon/deck-runtime/pdf";
function CustomPdfAction({ deck }: { readonly deck: CompiledDeck }) {
const { exportHostRef, exportPdf, exporting } = useDeckPdfExport({ deck });
return (
<>
<button type="button" onClick={() => void exportPdf()} disabled={exporting}>
{exporting ? "Export..." : "PDF"}
</button>
<DeckPdfExportHost ref={exportHostRef} deck={deck} />
</>
);
}Le PDF client-side est genere en rasterisant les pages issues de PrintDeck. jspdf et html2canvas sont charges dynamiquement au moment de l'export.
La librairie suit la stack cible de la specification :
- schema et parsing :
zod,yaml; - rendu contenu :
react-markdown, Mermaid via renderer charge dynamiquement ; - edition source : CodeMirror 6 avec support YAML ;
- tests : Vitest, Jest DOM et
fast-checkpour les proprietes compiler ; - tests visuels futurs : Playwright configure via
npm run test:playwright.
Shiki est disponible pour un futur renderer code enrichi. Le renderer code
actuel reste volontairement leger pour ne pas alourdir la preview simple.
Le runtime par defaut contient les layouts, renderers, themes et transitions fournis par la librairie. Une application peut ajouter ou remplacer ces registres :
import { createDeckRuntime } from "@bjalon/deck-runtime/runtime";
const runtime = createDeckRuntime({
layouts: [...customLayouts],
renderers: [...customRenderers],
themes: [...customThemes],
transitions: [...customTransitions],
registryCollisionStrategy: "override",
});Strategies disponibles en cas de collision de registre :
override: le dernier element remplace le precedent ;keep-first: le premier element est conserve ;throw: une erreur est levee des qu'un doublon est detecte.
Les themes impactent uniquement le rendu des slides, pas l'interface du studio ou de l'application hote.
La resolution du theme se fait au runtime via le deck compile et deckThemeStyle. Les presets actuels sont declares dans le runtime par defaut et appliques par classe CSS + variables CSS.
L'exemple integre montre un selecteur de theme persiste dans localStorage cote application exemple.
Le package expose des entrypoints separes pour limiter ce qu'une application importe :
import { DeckShow } from "@bjalon/deck-runtime/viewer";
import { DeckStudio } from "@bjalon/deck-runtime/editor";
import { DeckPresentationOverlay } from "@bjalon/deck-runtime/presentation";
import { compileDeck } from "@bjalon/deck-runtime/compiler";
import { createDeckRuntime } from "@bjalon/deck-runtime/runtime";
import { DeckPdfDownloadButton, PrintDeck } from "@bjalon/deck-runtime/pdf";
import "@bjalon/deck-runtime/styles.css";L'entree racine reste disponible pour un usage simple :
import { DeckShow, DeckStudio, compileDeck } from "@bjalon/deck-runtime";L'exemple montre une integration typique dans une page applicative :
- authentification Google OAuth via Firebase Auth ;
- liste Firestore des decks avec creation, edition, suppression, import et export JSON ;
- modele Firestore stable
schemaVersion: 1; - releases de deck stockees dans une sous-collection immuable ;
- preview integree avec
DeckShow; DeckStudiocontrole sur la meme source YAML ;- selecteur de theme persiste dans
localStorage; - menu de panels preview/diagnostics persiste dans
localStorage; - presentation plein ecran avec choix
auto,visible,hidden; - export PDF direct ;
- statut de compilation et diagnostics ;
- route publique de test sans authentification.
Version en ligne : https://bjalon.github.io/qastia-deck/
Designer de test sans authentification : https://bjalon.github.io/qastia-deck/test/
Lancement local :
npm install
npm run dev:exampleLa page /test/ ouvre directement le designer avec un jeu de slides d'exemple,
sans Firebase. La racine de l'exemple utilise Firebase si la configuration est
presente.
Configuration Firebase locale :
cp examples/integrated/.env.example .env.localRenseigner ensuite .env.local à la racine du repo :
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_STORAGE_BUCKET=...
VITE_FIREBASE_MESSAGING_SENDER_ID=...
VITE_FIREBASE_APP_ID=...Pour tester exactement le build production en local, le même format peut être
placé dans .env.production à la racine du repo. Ce fichier est ignoré par git.
En CI et pour GitHub Pages, le workflow ne lit pas de fichier .env.production :
il injecte les mêmes variables depuis les secrets GitHub Actions.
Modele Firestore utilise par l'exemple :
deckRuntimeExampleDecks/{deckId}
schemaVersion: 1
title: string
slug: string
description: string
source: string
ownerUid: string
createdAtIso: string
updatedAtIso: string
createdByEmail: string | null
updatedByEmail: string | null
releaseCount: number
latestReleaseId: string | null
latestReleaseNumber: number
deletedAtIso: string | null
deckRuntimeExampleDecks/{deckId}/releases/{releaseId}
schemaVersion: 1
deckId: string
releaseNumber: number
label: string
notes: string
source: string
sourceHash: string
createdAtIso: string
createdByUid: string
createdByEmail: string | nullLes fichiers deployables sont fournis a la racine du repo :
firebase.json
firestore.rules
firestore.indexes.jsonRegles Firestore utilisees par l'exemple :
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function allowedUser() {
return signedIn()
&& request.auth.token.email in [
'sophie.jalon@gmail.com',
'bjalon@qastia.com'
];
}
function ownsDeck(deckId) {
return allowedUser()
&& get(/databases/$(database)/documents/deckRuntimeExampleDecks/$(deckId)).data.ownerUid == request.auth.uid;
}
match /deckRuntimeExampleDecks/{deckId} {
allow create: if allowedUser()
&& request.resource.data.schemaVersion == 1
&& request.resource.data.ownerUid == request.auth.uid;
allow read, update: if allowedUser()
&& resource.data.ownerUid == request.auth.uid;
allow delete: if false;
match /releases/{releaseId} {
allow read: if ownsDeck(deckId);
allow create: if ownsDeck(deckId)
&& request.resource.data.schemaVersion == 1
&& request.resource.data.createdByUid == request.auth.uid;
allow update, delete: if false;
}
}
}
}Checklist Firebase :
- creer un projet Firebase ;
- activer Authentication > Google ;
- ajouter les domaines autorises :
localhost,127.0.0.1et le domaine GitHub Pages ; pour cette page :bjalon.github.io; - creer une base Firestore ;
- publier les regles ci-dessus ;
- ajouter les variables
VITE_FIREBASE_*en local ; - ajouter les variables ou secrets GitHub Actions correspondants. Les
Repository variablessuffisent car la configuration Firebase web n'est pas un secret applicatif :DECK_EXAMPLE_FIREBASE_API_KEY,DECK_EXAMPLE_FIREBASE_AUTH_DOMAIN,DECK_EXAMPLE_FIREBASE_PROJECT_ID,DECK_EXAMPLE_FIREBASE_STORAGE_BUCKET,DECK_EXAMPLE_FIREBASE_MESSAGING_SENDER_ID,DECK_EXAMPLE_FIREBASE_APP_ID.
npm install
npm run buildCommandes utiles :
npm run typecheck
npm test
npm run test:jest
npm run build:example
npm pack --dry-runnpm run test:jest reconstruit le package puis teste les fichiers dist, afin de valider ce qui sera reellement consomme par les applications.
Le package est publie sous le scope npm @bjalon, car le repository GitHub est bjalon/qastia-deck.
GitHub Packages npm impose que le scope du package corresponde au proprietaire GitHub autorise. Le workflow publie donc @bjalon/deck-runtime avec secrets.GITHUB_TOKEN et la permission Actions packages: write.
Configuration cote consommateur :
@bjalon:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}Dependance publiee :
{
"dependencies": {
"@bjalon/deck-runtime": "^1.2.0"
}
}Les documents de reference sont dans documentations/ :
spec-deck-runtime-editor.md: specification fonctionnelle et API cible ;spec-deck-runtime-architecture.md: regles d'architecture et choix structurants.