diff --git a/apps/docs/astro.config.ts b/apps/docs/astro.config.ts
index 3027788..d118ade 100644
--- a/apps/docs/astro.config.ts
+++ b/apps/docs/astro.config.ts
@@ -29,7 +29,14 @@ export default defineConfig({
description:
'Run, self-host and observe a Lumyx SFU: quickstart, deployment, the six media-path metrics and the REST API.',
social: [{ icon: 'github', label: 'GitHub', href: REPO }],
- sidebar: sidebarGroups().map((group) => ({ label: group.label, items: group.items })),
+ // Les squelettes portent le badge « Soon » de la maquette Docs v3.
+ sidebar: sidebarGroups().map((group) => ({
+ label: group.label,
+ items: group.items.map((slug) =>
+ stubs.includes(slug) ? { slug, badge: { text: 'Soon', class: 'lumyx-soon' } } : slug,
+ ),
+ })),
+ editLink: { baseUrl: 'https://github.com/lumyx-dev/website/edit/main/apps/docs/' },
customCss: ['./src/styles/global.css'],
expressiveCode: false,
routeMiddleware: './src/route-middleware.ts',
@@ -38,6 +45,9 @@ export default defineConfig({
ThemeSelect: './src/components/starlight/ThemeSelect.astro',
PageTitle: './src/components/starlight/PageTitle.astro',
SiteTitle: './src/components/starlight/SiteTitle.astro',
+ SocialIcons: './src/components/starlight/SocialIcons.astro',
+ PageSidebar: './src/components/starlight/PageSidebar.astro',
+ Footer: './src/components/starlight/Footer.astro',
},
}),
// Voir apps/landing/astro.config.ts : les enfants passes depuis le MDX deviennent des
diff --git a/apps/docs/package.json b/apps/docs/package.json
index c317f89..e4b637f 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -14,6 +14,7 @@
"verify:ds": "node scripts/verify-ds.mjs"
},
"dependencies": {
+ "@astrojs/markdown-satteri": "0.4.6",
"@astrojs/react": "^7.1.2",
"@astrojs/sitemap": "^3.7.4",
"@astrojs/starlight": "^0.42.7",
@@ -25,6 +26,7 @@
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.4.4",
"react": "19.2.8",
+ "satteri": "0.10.5",
"react-dom": "19.2.8",
"tailwindcss": "^4.3.3"
},
diff --git a/apps/docs/src/components/code-panel.astro b/apps/docs/src/components/code-panel.astro
new file mode 100644
index 0000000..b786d27
--- /dev/null
+++ b/apps/docs/src/components/code-panel.astro
@@ -0,0 +1,25 @@
+---
+// Panneau de code de la maquette Docs v3 : toujours sombre, nom de fichier, bouton Copy.
+import { CopyButton } from './copy-button';
+import type { Token } from '@/lib/json-tokens';
+
+interface Props {
+ label: string;
+ lines: Token[][];
+ class?: string;
+}
+const { label, lines, class: className } = Astro.props;
+const text = lines.map((l) => l.map((t) => t.v).join('')).join('\n');
+---
+
+
+
+ {label}
+
+
+
{
+ lines.map((line) => (
+
{line.map((t) => {t.v})}
+ ))
+ }
+
diff --git a/apps/docs/src/components/copy-button.tsx b/apps/docs/src/components/copy-button.tsx
new file mode 100644
index 0000000..9b120ae
--- /dev/null
+++ b/apps/docs/src/components/copy-button.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+/** Bouton « Copy » des panneaux de code. Ilot React : seul morceau interactif de la page. */
+export function CopyButton({ text }: { text: string }) {
+ const [copied, setCopied] = React.useState(false);
+ const timer = React.useRef>(undefined);
+ React.useEffect(() => () => clearTimeout(timer.current), []);
+
+ return (
+
+ );
+}
diff --git a/apps/docs/src/components/metric-detail.astro b/apps/docs/src/components/metric-detail.astro
new file mode 100644
index 0000000..3f3b8d2
--- /dev/null
+++ b/apps/docs/src/components/metric-detail.astro
@@ -0,0 +1,36 @@
+---
+// Le corps d'UNE metrique (maquette Docs v3) : texte et encarts a gauche, exemple JSON a droite.
+// Le titre reste un vrai `##` dans le MDX, avec son ancre epinglee (`## Jitter [#jitter_ms]`) :
+// c'est lui qui alimente le sommaire. Ce composant ne rend donc ni titre ni `id`.
+import { METRICS } from '@/lib/metrics-data';
+import { tokenizeJson } from '@/lib/json-tokens';
+import CodePanel from './code-panel.astro';
+
+interface Props {
+ field: string;
+}
+const m = METRICS.find((metric) => metric.field === Astro.props.field);
+if (!m) throw new Error(`MetricDetail: metrique inconnue "${Astro.props.field}"`);
+---
+
+
+
+ {m.field} · {m.unit}
+
{m.body}
+
+
Default
+
{m.threshold}
+
Scope
+
{m.scope}
+
Breaks
+
{m.breaks}
+
+
+
+
What to do. {m.action}
+
+
+
+
diff --git a/apps/docs/src/components/metrics-reference.tsx b/apps/docs/src/components/metrics-reference.tsx
index 84a9351..2952ae0 100644
--- a/apps/docs/src/components/metrics-reference.tsx
+++ b/apps/docs/src/components/metrics-reference.tsx
@@ -1,104 +1,28 @@
-import {
- Card,
- CardContent,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@lumyx/ui';
import { METRICS } from '@/lib/metrics-data';
/**
- * Tableau recapitulatif des six seuils par defaut, repris de l'ancienne page. Le brief de la
- * tache 5 ne le mentionnait pas ; sans lui, la vue d'ensemble (six metriques sur une grille de
- * six colonnes) disparaissait de la doc migree.
+ * « At a glance » (maquette Docs v3) : une carte par metrique, qui renvoie a sa section.
+ * Seuil par defaut en pastille, ce qu'elle casse, et le nom du champ.
*/
-export function MetricsThresholds() {
+export function MetricsGlance() {
return (
-
-
-
-
- Metric
- Field
- Unit
- Threshold
- Scope
- What it breaks
-
-
-
- {METRICS.map((m) => (
-
- {m.name}
- {m.field}
- {m.unit}
- {/* `sl-num` : c'est la colonne dont le travail est la comparaison verticale de
- nombres (> 2% / > 200ms / < 100kbps). Sans chiffres tabulaires, ils ne
- s'alignent pas. */}
- {m.threshold}
- {m.scope}
- {m.breaks}
-
- ))}
-
-
-
- );
-}
-
-/**
- * Le corps d'UNE metrique : sept champs se tiennent mieux dans un objet que dans des
- * paragraphes paralleles.
- *
- * Ce composant ne rend deliberement ni titre ni `id`. Le titre est un vrai `##` dans le MDX,
- * avec son ancre epinglee (`## Packet loss [#packet_loss_ratio]`), pour deux raisons : Fumadocs
- * derive la table des matieres des titres du document au moment de la compilation, donc un titre
- * rendu a l'interieur d'un composant React n'y figure jamais ; et l'ancre explicite conserve
- * `#packet_loss_ratio` la ou rehype-slug aurait produit `#packet-loss`. Poser ici un `id` en plus
- * le dupliquerait.
- */
-export function MetricDetail({ field }: { field: string }) {
- const m = METRICS.find((metric) => metric.field === field);
- if (!m) throw new Error(`MetricDetail: metrique inconnue "${field}"`);
-
- return (
-
);
}
diff --git a/apps/docs/src/components/starlight/Footer.astro b/apps/docs/src/components/starlight/Footer.astro
new file mode 100644
index 0000000..129ba1f
--- /dev/null
+++ b/apps/docs/src/components/starlight/Footer.astro
@@ -0,0 +1,9 @@
+---
+// Pied de page (maquette Docs v3) : seulement la page precedente et la suivante. Le lien
+// d'edition est monte dans l'en-tete de page (PageTitle.astro).
+import Pagination from '@astrojs/starlight/components/Pagination.astro';
+---
+
+
diff --git a/apps/docs/src/components/starlight/PageSidebar.astro b/apps/docs/src/components/starlight/PageSidebar.astro
new file mode 100644
index 0000000..7637e91
--- /dev/null
+++ b/apps/docs/src/components/starlight/PageSidebar.astro
@@ -0,0 +1,22 @@
+---
+// Le sommaire de Starlight, suivi de l'encart « New to Lumyx? » de la maquette Docs v3.
+// L'encart vit dans la colonne de droite : il n'apparait que la ou elle existe (page avec
+// sommaire, ecran large), et pas sur le quickstart lui-meme.
+import Default from '@astrojs/starlight/components/PageSidebar.astro';
+
+const { toc, id } = Astro.locals.starlightRoute;
+const showNewcomer = Boolean(toc) && id !== 'quickstart';
+---
+
+
+{
+ showNewcomer && (
+
+ )
+}
diff --git a/apps/docs/src/components/starlight/PageTitle.astro b/apps/docs/src/components/starlight/PageTitle.astro
index db6eee3..d72ba0f 100644
--- a/apps/docs/src/components/starlight/PageTitle.astro
+++ b/apps/docs/src/components/starlight/PageTitle.astro
@@ -1,15 +1,41 @@
---
-// Remplace le PageTitle de Starlight : fil d'Ariane (nom de la section), titre au palier 44 et
-// chapo au palier 16, comme sous Fumadocs. L'id `_top` est celui que Starlight attend.
-const { entry, sidebar } = Astro.locals.starlightRoute;
+// Remplace le PageTitle de Starlight (maquette Docs v3) : fil d'Ariane Docs › section › page,
+// titre en degrade, chapo, puis la version et le lien d'edition. L'id `_top` est celui que
+// Starlight attend.
+import { Icon } from '@astrojs/starlight/components';
+import { VERSION } from '@/lib/site-data';
+
+const { entry, sidebar, editUrl } = Astro.locals.starlightRoute;
type SidebarEntry = (typeof sidebar)[number];
function hasCurrent(e: SidebarEntry): boolean {
return e.type === 'link' ? e.isCurrent : e.entries.some(hasCurrent);
}
const section = sidebar.find((e) => e.type === 'group' && hasCurrent(e))?.label;
+const crumbs = ['Docs', section].filter(Boolean) as string[];
---
-{section &&
diff --git a/apps/docs/src/components/starlight/SiteTitle.astro b/apps/docs/src/components/starlight/SiteTitle.astro
index 372100e..3783d44 100644
--- a/apps/docs/src/components/starlight/SiteTitle.astro
+++ b/apps/docs/src/components/starlight/SiteTitle.astro
@@ -1,5 +1,5 @@
---
-// Le wordmark Lumyx, qui renvoie vers lumyx.dev comme sous Fumadocs.
+// Le wordmark Lumyx, qui renvoie vers lumyx.dev, suivi de « Docs » en retrait (maquette Docs v3).
import { Wordmark } from '@lumyx/ui';
import { SITE_URL } from '@/lib/env';
---
@@ -7,6 +7,7 @@ import { SITE_URL } from '@/lib/env';
+Docs
diff --git a/apps/docs/src/components/starlight/SocialIcons.astro b/apps/docs/src/components/starlight/SocialIcons.astro
new file mode 100644
index 0000000..aeb9c33
--- /dev/null
+++ b/apps/docs/src/components/starlight/SocialIcons.astro
@@ -0,0 +1,38 @@
+---
+// Remplace les icones sociales de Starlight par la barre de liens de la maquette Docs v3 :
+// retour au site, GitHub, version en pastille. Le bouton de theme suit, rendu par Starlight.
+import { SITE_URL } from '@/lib/env';
+import { REPO, VERSION } from '@/lib/site-data';
+---
+
+
+
+
diff --git a/apps/docs/src/content/docs/metrics-reference.mdx b/apps/docs/src/content/docs/metrics-reference.mdx
index 5a6ebf9..155ffbd 100644
--- a/apps/docs/src/content/docs/metrics-reference.mdx
+++ b/apps/docs/src/content/docs/metrics-reference.mdx
@@ -1,16 +1,26 @@
---
title: Metrics reference
-description: Six metrics, read from RTCP as the media passes through the SFU. Each one is armed by default with the threshold below and a 30s debounce, per peer and — where the scope says so — per room.
+description: Six numbers, measured in the media path for every peer and sampled every 500 ms. Each has a default threshold you can change per environment.
---
-import { MetricDetail, MetricsThresholds } from '@/components/metrics-reference';
+import { Aside } from '@astrojs/starlight/components';
+import { MetricsGlance } from '@/components/metrics-reference';
+import MetricDetail from '@/components/metric-detail.astro';
+import CodePanel from '@/components/code-panel.astro';
+import { tokenizeToml } from '@/lib/json-tokens';
{/* Les ancres sont epinglees en `[#champ]` plutot que laissees a rehype-slug, qui aurait
renomme `#packet_loss_ratio` en `#packet-loss` et casse les liens profonds existants. */}
-## Default thresholds
+
-
+## At a glance [#overview]
+
+
## Packet loss [#packet_loss_ratio]
@@ -36,14 +46,29 @@ import { MetricDetail, MetricsThresholds } from '@/components/metrics-reference'
-## Overriding a threshold
-
-Thresholds live per project and per environment since v0.4.0. Set one and it applies to every room in that
-environment; alerts still debounce for 30s before firing.
-
-```http
-PATCH /v1/projects/live-classroom/thresholds
-
-{ "packet_loss_ratio": 0.03,
- "rtt_ms": 250 }
-```
+## Change a threshold [#overrides]
+
+
+
+
+ Thresholds will live in lumyx.toml and be
+ read at startup. Keep a looser file for staging: a room on a bad office link should not look like an outage.
+
+
A metric counts as crossed only after it stays past the threshold for the full debounce window.
+
Draft format. Today the SFU reads environment variables.
+
+
+
diff --git a/apps/docs/src/lib/heading-ids.test.ts b/apps/docs/src/lib/heading-ids.test.ts
new file mode 100644
index 0000000..e40fcf8
--- /dev/null
+++ b/apps/docs/src/lib/heading-ids.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "bun:test";
+import { splitPinnedId } from "./heading-ids";
+
+describe("splitPinnedId", () => {
+ test("separe le titre et l'ancre", () => {
+ expect(splitPinnedId("Jitter [#jitter_ms]")).toEqual({ text: "Jitter", id: "jitter_ms" });
+ });
+
+ test("ignore un titre ordinaire", () => {
+ expect(splitPinnedId("At a glance")).toBeNull();
+ expect(splitPinnedId("Arrays [like this] in a title")).toBeNull();
+ });
+});
diff --git a/apps/docs/src/lib/heading-ids.ts b/apps/docs/src/lib/heading-ids.ts
new file mode 100644
index 0000000..a7427f1
--- /dev/null
+++ b/apps/docs/src/lib/heading-ids.ts
@@ -0,0 +1,32 @@
+import type { MdastPluginDefinition } from 'satteri';
+
+/**
+ * Ancres epinglees : `## Jitter [#jitter_ms]` rend `
Jitter
`.
+ * La syntaxe vient de Fumadocs ; Starlight ne la connait pas et laissait `[#jitter_ms]` dans le
+ * titre et le sommaire, avec une ancre `#jitter-jitter_ms`. Le plugin retire le suffixe et pose
+ * l'`id` ; le plugin d'ancres d'Astro garde un `id` existant, donc le sommaire suit.
+ *
+ * Plugin Satteri (le moteur Markdown par defaut d'Astro 7), pas remark : `markdown.remarkPlugins`
+ * n'est lu que par l'ancien moteur unified.
+ */
+const PINNED = /\s*\[#([\w-]+)\]\s*$/;
+
+/** `"Jitter [#jitter_ms]"` → `{ text: "Jitter", id: "jitter_ms" }` ; `null` sans ancre epinglee. */
+export function splitPinnedId(value: string): { text: string; id: string } | null {
+ const m = PINNED.exec(value);
+ return m ? { text: value.slice(0, m.index), id: m[1]! } : null;
+}
+
+export const pinnedHeadingIds: MdastPluginDefinition = {
+ name: 'lumyx-pinned-heading-ids',
+ heading(node, ctx) {
+ const last = node.children.at(-1);
+ if (last?.type !== 'text') return;
+ const pinned = splitPinnedId(last.value);
+ if (!pinned) return;
+ ctx.setProperty(last, 'value', pinned.text);
+ // `hProperties` est lu au passage en hast mais absent du type `HeadingData`.
+ const data = node.data as { hProperties?: Record } | undefined;
+ ctx.setProperty(node, 'data', { ...data, hProperties: { ...data?.hProperties, id: pinned.id } });
+ },
+};
diff --git a/apps/docs/src/lib/json-tokens.ts b/apps/docs/src/lib/json-tokens.ts
new file mode 100644
index 0000000..3a9bb88
--- /dev/null
+++ b/apps/docs/src/lib/json-tokens.ts
@@ -0,0 +1,33 @@
+/**
+ * Coloration minimale des exemples JSON de la page Metrics reference (maquette Docs v3).
+ * Rend des classes de tokens, pas des couleurs : les panneaux de code sont toujours sombres
+ * (classe `.dark`), et les tokens y prennent leur valeur sombre.
+ */
+export type Token = { v: string; cls: string };
+
+const CLS = { key: "text-accent-text", str: "text-accent2", num: "text-ok", dim: "text-faint", txt: "text-body" };
+const RE = /("(?:[^"\\]|\\.)*")(\s*:)?|\b(\d+(?:\.\d+)?)\b|([{}[\],:])/g;
+
+export function tokenizeJson(line: string): Token[] {
+ const out: Token[] = [];
+ let i = 0;
+ let m: RegExpExecArray | null;
+ RE.lastIndex = 0;
+ while ((m = RE.exec(line))) {
+ if (m.index > i) out.push({ v: line.slice(i, m.index), cls: CLS.txt });
+ if (m[1]) {
+ out.push({ v: m[1], cls: m[2] ? CLS.key : CLS.str });
+ if (m[2]) out.push({ v: m[2], cls: CLS.dim });
+ } else if (m[3]) out.push({ v: m[3], cls: CLS.num });
+ else out.push({ v: m[4]!, cls: CLS.dim });
+ i = RE.lastIndex;
+ }
+ if (i < line.length) out.push({ v: line.slice(i), cls: CLS.txt });
+ return out.length ? out : [{ v: " ", cls: CLS.txt }];
+}
+
+/** TOML : commentaires estompes, sections en accent, le reste en texte. */
+export function tokenizeToml(line: string): Token[] {
+ const cls = line.startsWith("#") ? CLS.dim : line.startsWith("[") ? CLS.key : CLS.txt;
+ return [{ v: line || " ", cls }];
+}
diff --git a/apps/docs/src/lib/markdown.ts b/apps/docs/src/lib/markdown.ts
index 17b241c..7d56515 100644
--- a/apps/docs/src/lib/markdown.ts
+++ b/apps/docs/src/lib/markdown.ts
@@ -1,4 +1,6 @@
import { lumyxCodeTheme } from '@lumyx/ui/code-theme';
+import { satteri } from '@astrojs/markdown-satteri';
+import { pinnedHeadingIds } from './heading-ids';
/**
* Coloration du code. Expressive Code (le defaut de Starlight) est coupe : il calcule ses propres
@@ -8,4 +10,5 @@ import { lumyxCodeTheme } from '@lumyx/ui/code-theme';
*/
export const markdown = {
shikiConfig: { theme: lumyxCodeTheme },
+ processor: satteri({ mdastPlugins: [pinnedHeadingIds] }),
};
diff --git a/apps/docs/src/lib/metrics-data.test.ts b/apps/docs/src/lib/metrics-data.test.ts
index 5d94bca..a3a8c73 100644
--- a/apps/docs/src/lib/metrics-data.test.ts
+++ b/apps/docs/src/lib/metrics-data.test.ts
@@ -2,7 +2,8 @@ import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { createElement } from "react";
import { METRICS } from "./metrics-data";
-import { MetricsThresholds } from "../components/metrics-reference";
+import { MetricsGlance } from "../components/metrics-reference";
+import { tokenizeJson } from "./json-tokens";
describe("donnees des metriques", () => {
test("porte les six metriques du collecteur", () => {
@@ -33,32 +34,39 @@ describe("donnees des metriques", () => {
});
});
-describe("MetricsThresholds", () => {
- // Le tableau recapitulatif est la seule vue d'ensemble des six metriques : une ligne perdue
- // ne casse rien et ne se voit pas. On compte donc les cellules et on verifie que chaque valeur
- // des donnees arrive bien dans le rendu.
- const html = renderToStaticMarkup(createElement(MetricsThresholds));
+describe("MetricsGlance", () => {
+ // La grille « At a glance » est la seule vue d'ensemble des six metriques : une carte perdue
+ // ne casse rien et ne se voit pas. On compte les cartes et on verifie leurs valeurs.
+ const html = renderToStaticMarkup(createElement(MetricsGlance));
- test("rend les six lignes sur les six colonnes", () => {
- expect(html.match(/