Système de thèmes
Layers
- The engine, package
@vignettes/theme-kit(packages/theme-kit/), holds the CSS every theme needs to work with the Vue components: the semantic token contract, Reveal patches, the MDCdisplay: contentsbridge, the media-layout grid, theslide-h1/h2/h3flex skeleton, quicklinks, and thelayoutscale preset. Every theme builds on this layer. Its Reveal patches display inlinecodeas aninline-blockbox, and the renderer puts the code and the punctuation next to it in acode-groupspan that the kit keeps on one line: the segment moves to the next line whole, with its punctuation, and breaks inside its box only when it is longer than the line. - The DSFR base, package
@vignettes/theme-dsfr(packages/theme-dsfr/), is the French state design system as a brand-neutral theme: palette, Marianne typography,layoutscale, light surfaces. A brand extends it, and so does the application's owndsfrtheme. The build copies the Marianne font files from the@gouvfr/dsfrnpm package.@vignettes/theme-dsfrleaves the surface of each slide role to the theme that extends it.themes/dsfr/paints flat surfaces from the DSFR palette on Reveal's background layer (see "Slides by role"): the title slide onblue-france-975, section dividers onblue-france-sun-113with light text, content slides on white. - The application's themes (
dsfr,minimal) are inthemes/<name>/, and a tenant's brands inthemes/<name>/of the content space (NUXT_CONTENT_DIR,presentations/by default). A brand extends the DSFR base or the engine alone.
Manifest
{
"name": "brand",
"extends": "dsfr",
"aliases": [],
"lang": "fr",
"parser": "separator",
"reveal": { "slideNumber": true, "width": 1200, "height": 800 },
"backgrounds": {
"h1": "assets/slide-contrast.png",
"h2": "assets/slide-contrast.png",
"h3": "assets/slide-subtle.png"
}
}
nameequals the directory name. A deck names the theme with it intheme:, and the stylesheet is/themes/<name>.css.extendsnames the theme this one builds on; the build checks it.aliaseslists other frontmatter names that resolve to this theme. A tenant uses it to point a name already present in its decks to its brand. The build reports an alias that shadows another theme's name.lang,parserandrevealare the deck defaults the theme carries: the language for orthotypography, the slide parser and the Reveal config.backgroundsgives the background image of the title slide (h1), the sections (h2), the content slides (h3) and the others (default), as a path relative to the theme directory or an absolute URL.entryis the SCSS entry,<name>.scssby default.assetslists extra directories copied underpublic/themes/<name>/, as{ "from": "node_modules/...", "to": "fonts", "match": "regex" }, resolved from the project root.codeis the default code theme of the deck blocks: a name ofCODE_THEMES(nuit,sombre,clair), or its properties.previewdescribes how the catalog draws the card of a deck on this theme, without loading the theme's stylesheet.
preview takes fontFamily, titleColor, titleGradient (a gradient applied to the text of the title), textColor and surface. Its fonts field names an SCSS entry declaring the font faces, compiled on its own to public/themes/<name>/preview-fonts.css, which the catalog loads; pkg:@vignettes/theme-dsfr/fonts is the value the DSFR theme uses. levels overrides titleColor, titleGradient and textColor for a first slide of another role, and titleGradient: 'none' removes the gradient there.
{
"preview": {
"fontFamily": "Marianne, Arial, sans-serif",
"fonts": "pkg:@vignettes/theme-dsfr/fonts",
"titleColor": "#000091",
"textColor": "#3a3a3a",
"surface": "#ffffff"
}
}
packages/core/src/theme/tokens.generated.ts is generated from the manifests of the application's themes by scripts/build-theme-tokens.ts and committed; it seeds the runtime registry in packages/core/src/theme/tokens.ts (knownThemes, resolveThemeName, isKnownTheme, getThemeTokens). Frontmatter accepts canonical names and aliases; resolveThemeName maps a name to the theme whose CSS is loaded.
Tenant themes at runtime
Themes of the content space reach a deployment through the bucket. mise run theme:push -- <name>[,<name>] --apply compiles them locally and uploads themes/<name>.css, themes/<name>/assets/, and themes/<name>/tokens.json to the bucket, then rebuilds themes/index.json from every published theme. An instance that shares the bucket with another one reads its themes under a folder of its own: NUXT_FROZEN_S3_THEMES_PREFIX=staging gives staging/themes/…, and theme:push --staging publishes there with the secrets of the Infisical environment staging. On a deployment, server/utils/themes.ts reads that index (cached for a minute; POST /api/admin/themes/reload forces it), merges it with the themes found on disk in the content space (development), and applies the result with setThemeRegistry. The theme-registry server middleware hands the registry to page requests and the universal plugin src/plugins/theme-registry.ts seeds the client with it, so both sides resolve names, aliases and backgrounds identically. /themes/** (server/routes/themes/[...path].ts) streams the files from the bucket for names that have no static file under public/themes/.
The theme packages a theme was compiled with
A published theme holds the CSS of @vignettes/theme-kit and @vignettes/theme-dsfr that the machine running theme:push had installed. theme:push writes their versions in tokens.json and in the index, under compiledWith: the version of each package, then the start of a hash of its stylesheets (0.1.0+6a73b97e14), which changes with any change to a stylesheet. The build of an instance computes the same versions (themePackageVersions of packages/nuxt/src/build/theme-build.ts). When the two differ, or when a theme has no compiledWith, the server writes the theme and the two versions in its log as it loads the themes, and POST /api/admin/themes/reload returns them under gaps. Publishing the theme again from a checkout of the branch the instance runs clears the gap.
Testing a change of the theme packages
A pull request that changes packages/theme-kit or packages/theme-dsfr changes the CSS of the published themes once they are compiled again. To test it on staging, push the branch to staging, then publish the themes the test decks use from that branch:
mise run theme:push -- <name>[,<name>] --staging --apply
Staging serves the themes published under its prefix: a deck whose theme was published without --staging renders there without its stylesheet.
Production keeps serving the themes published without --staging. After the merge into main and the deployment of production, publish the same themes from main without --staging:
mise run theme:push -- <name>[,<name>] --apply
Renaming a theme in a deployed instance
A deck's theme is written in the frontmatter of each of its published versions, of its draft and of its snapshots, and in the theme column the catalog reads. mise run theme:rename -- <from> <to> rewrites all of them in one instance, through the app container, listing the decks it would touch before --apply writes anything. scripts/lib/theme-rename.ts defines what a theme declaration looks like.
The semantic token contract
Code that works under any theme (the engine layer, MDC components such as Highlight.vue) reads these custom properties only, and each theme sets them:
--accentis the primary color, for links, dividers and accents.--accent-softis a lighter accent, for hover tints.--accent-bgis a very light tint, for callout backgrounds.--accent-strongis the accent mixed with the title color, for text on--accent-softor--accent-bg, derived from--accentby default.--accent-lineis the accent mixed with the surface, for the outlines of a diagram: the cards, the subject regions and the edge labels, derived from--accentby default.--surfaceis the page background.--surface-contrastis the inverse surface, for dark blocks and code panels.--on-contrastis the text color on--surface-contrast.--text-defaultcolors the body text,--text-titlethe headings and strong text,--text-mentionthe muted text (small print, captions).--border,--radiusand--shadowgive the outline, the corners and the elevation of a box drawn by a component.--text-step-1to--text-step-4are the type scale above the body text, inemof the text around: the headings inside the body and the sizes ofHighlight.vueread them.--flow-space,--block-spaceand--heading-space-beforeare the vertical rhythm of the body of a content slide: the space inside a group of text (between a heading and its paragraph, two paragraphs, a paragraph and its list, two list items), above and below a block other than text, and above a heading of the body.--measureis the maximum line length of a paragraph or a list of that body.
packages/theme-kit/_semantic-tokens.scss gives each token a default value, which applies when a theme omits it. packages/theme-kit/_palette.scss defines the tints of the diagram cards, --palette-<name>-bg, -main and -text, under eight generic names a theme redefines.
The kit derives three more tokens from the text intensities, at the root of the page: --on-surface-title, --on-surface-default and --on-surface-mention. A component that paints its own surface, such as a diagram card or a story frame, sets the text of that surface from them. A slide that changes its text colors, such as a dark section divider, leaves that surface with its dark text.
The code theme of a deck
A deck sets the colors of its code blocks in its frontmatter (code: sombre, or code: { theme: sombre, string: '#FFD700' }). RevealPresentation turns that value into --code-* custom properties on the deck root and adds the class reveal--code-theme. packages/nuxt/src/runtime/assets/code-theme.css, which ships with the renderer, colors each token from these properties under any theme. A role the code theme omits keeps the color of the highlight theme (mdc.highlight.theme).
A deck without a code theme takes the one its theme names in its manifest ("code": "clair"). The three code themes read the tokens of the theme kit. nuit sets the colors of github-dark on --code-surface-dark. sombre keeps its own dark palette. clair sets a block of --code-surface with the text intensities, --accent-strong for the keys and the -text tones of the card palette for the strings, the literals and the functions. clair suits a light theme: under the kit defaults and under dsfr, each of its tones reaches a contrast of 4.5:1 or more against the block, and the author of a theme that changes these tokens checks that contrast. The author of a dark theme names nuit or sombre in its manifest. The current zone of a JsonTour is marked with --accent-on-contrast on a dark block, --accent-strong on clair. When the theme names none, the deck carries neither the class nor the properties and its tokens keep the colors of the highlight theme.
RevealPresentation adds reveal--code-block when the frontmatter of the deck names a background, and the renderer then draws the block. With a code theme named in a manifest, the theme's stylesheet draws the block.
A theme reads --code-size as a minimum for its own size, and --code-min-size, in slide units, as the smallest size of a code block (0px in the kit; 15px in dsfr and minimal, where a block in a narrow column stays readable): font-size: max(var(--code-size, 0px), 0.7em, var(--code-min-size)).
Compilation
pnpm build:themes # tokens, then every theme
pnpm build:theme brand # one theme
node scripts/build-themes.ts --watch # recompile on change
packages/nuxt/src/build/theme-registry.ts discovers the themes; packages/nuxt/src/build/theme-build.ts compiles one theme with sass and its NodePackageImporter, so @use 'pkg:@vignettes/theme-kit' resolves from the application's node_modules wherever the theme directory is. packages/nuxt/src/build/theme-watcher.js watches theme directories and packages/ during nuxt dev and reloads the page after a recompile. server/plugins/theme-consistency.ts warns at boot when a theme directory, its compiled CSS and the generated tokens disagree.
Writing a theme
A brand on the DSFR base:
// presentations/themes/mybrand/mybrand.scss
@use 'pkg:@vignettes/theme-dsfr/variables' as *;
@use 'pkg:@vignettes/theme-dsfr';
.reveal h1 { color: #0000cc; }
.reveal section.slide-h2 h2 { color: var(--dsfr-grey-950) !important; }
A theme made of the tokens, with the base skin of the kit and the DSFR sizes (examples/themes/exemple-sombre and exemple-marque are complete examples; exemple-marque also paints its section slides):
@use 'pkg:@vignettes/theme-kit';
@use 'pkg:@vignettes/theme-kit/base';
@use 'pkg:@vignettes/theme-kit/presets/layout';
:root {
--accent: #c2410c;
--surface: #fffdf9;
// the ten other tokens of the contract
}
A theme on the engine alone, with its own scale:
// themes/mytheme/mytheme.scss
@use 'pkg:@vignettes/theme-kit';
:root {
--accent: #e63946;
--surface: #ffffff;
// the rest of the contract
}
.reveal h1 { font-size: 3em; }
Add theme.json, run pnpm build:themes, and use theme: mytheme in a deck. pkg:@vignettes/theme-kit/presets/layout gives a theme the DSFR sizes without its colors.
The fallback theme
A deck whose frontmatter declares no theme: renders in the fallback theme of the instance. The create form preselects that theme and a catalog card previews such a deck with it. runtimeConfig.public.defaultTheme names it (dsfr in this application) and NUXT_PUBLIC_DEFAULT_THEME overrides it at run time. Without a host setting, @vignettes/nuxt falls back to minimal. When that name matches no registered theme, the server ignores it and writes a [themes] warning at boot that names the theme applied.
Slides by role
The engine tags each <section> with the class of its role, which surfaceLevel of packages/core/src/deck/model.ts gives: slide-h1 for the title slide, slide-h2 for a section divider, slide-h3 for a content slide. A section divider is a ## slide made of its heading and at most one paragraph of 140 characters or fewer, besides its speaker notes and its annotations other than :layout (isSectionSlide of packages/core/src/deck/split.ts). A ## slide with more content is a content slide and carries slide-h3; the manifest's h2 and h3 backgrounds follow the same roles. The flex skeleton comes from packages/theme-kit/_slide-structure.scss; themes set sizes and colors, and backgrounds come from the manifest.
A section that holds vertical slides is wrapped in Reveal's stack. That wrapper carries stack-h1 or stack-h2 and no slide- class, and the section's own content is the first of its vertical slides, with its own slide- class. So a rule written as .reveal section.slide-h2 h3 styles the divider alone and cannot reach the slides nested under it. Target stack-h2 to style the wrapper itself.
A flat color surface goes on Reveal's background layer: Reveal creates one .slide-background per slide under .reveal .backgrounds, copies the slide's classes onto it, and fills the screen with it whatever the aspect ratio. A rule such as .reveal .slide-background.slide-h2 { background-color: … } colors the divider edge to edge, and Reveal reads that computed color to mark the slide and the deck has-dark-background, which switches its controls to light on its own. Painting the section itself leaves the viewport color visible around the slide on screens of another ratio. The rule leaves out the parent of the layer: the print view moves the layer into the .pdf-page of its slide, and the scroll view, under 435 pixels wide, into the .scroll-page-content of its slide.
Checking a theme
examples/controle-du-theme.md is a deck that holds each slide role and each element a theme colors: the title slide, a section, a short and a long content slide, inline code, a list, a table, a quote, a code block, columns, a media on the right, a media taking two thirds of the width on the left, a media at the bottom, an image behind the text on a card, an image behind the text under a veil, a Mermaid diagram, a Diagram with a colored region and colored nodes, and a ## slide with content. Write the name of the theme under check in its theme:, import it in a local studio, and read every slide, the vertical ones included:
- Sizes: 1280 × 720 and 1920 × 1080, then 390 pixels wide. Under 435 pixels, reveal.js switches to its scroll view: the slides follow one another, each scaled to the width of the screen, so the body text measures a few pixels whatever the theme. Each section keeps its surface edge to edge.
- Contrast: 4.5:1 or more between a text and its surface, 3:1 for a text of 24 pixels or more, or of 18.5 pixels or more in bold (RGAA, criterion 3.2). The pairs are
--text-title,--text-defaultand--text-mentionon--surface;--accenton--surfacefor the links and on--accent-bgfor the inline code of the base skin;--accent-strongon--accent-softfor an emphasized diagram node; each--palette-<name>-texton--surfaceand on its--palette-<name>-bg;--on-contraston--surface-contrast; the title, the text and the links of a section on its surface. - Inline code: an inline box with a padding and a background. At the end of a line, the browser can break a segment at a space it holds: the background stops at the end of the line and starts again on the next one without its padding. Read the three long segments of the long content slide at each width.
- Diagrams: Mermaid fills its boxes with
--accent-bg, outlines them with--accentand writes their labels in--on-surface-title; its lines take the--text-mentionof the slide, its free text the--text-defaultof the slide, and it measures its labels with the font of the slide. The last slide has a##heading and a diagram: it takes the surface of a content slide. The renderer scales a diagram down to the width of its column, and its height follows: a wide font scales a wide diagram down further, and a tall diagram can reach the navigation arrows. - The PDF export prints the section surfaces.
Troubleshooting
- Stylesheet 404:
public/themes/<name>.cssis missing; runpnpm build:themes. - A deck reports an unknown theme: the name is neither a directory with a manifest nor an alias; check
packages/core/src/theme/tokens.generated.ts. - Changes in a package do not show: the watcher recompiles every theme on a
packages/change; a hard refresh may be needed. assets "node_modules/..." not found: install dependencies, the DSFR fonts come from@gouvfr/dsfr.
Composants Storybook
Embeds a live Storybook story in a slide. The slide loads the story from the published Storybook, and shows the component as that Storybook renders it when the deck opens.
Kit de thèmes
The engine CSS layer that every Vignettes theme builds on. It is a set of SCSS files, and Sass is the one tool it needs.