Skip to content

Commit 7d1edb8

Browse files
authored
Merge pull request #470 from codegouvfr/feat/upgrade-storybook
feat(storybook): upgrade from v6 to v10
2 parents a85aca0 + 8ef98a5 commit 7d1edb8

80 files changed

Lines changed: 2220 additions & 9452 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ CHANGELOG.md
44
.yarn_home/
55
/test/integration/
66
/storybook-static/
7-
7+
!.storybook
Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,44 @@
1-
2-
import React, { useEffect } from "react";
3-
import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
4-
import { useDarkMode } from "storybook-dark-mode";
1+
import React, { PropsWithChildren, useEffect } from "react";
2+
import {
3+
DocsContainer as BaseContainer,
4+
DocsContainerProps,
5+
Unstyled
6+
} from "@storybook/addon-docs/blocks";
7+
import { useDarkMode } from "@vueless/storybook-dark-mode";
58
import { darkTheme, lightTheme } from "./customTheme";
69
import "../dist/dsfr/utility/icons/icons.min.css";
710
import "../dist/dsfr/dsfr.css";
811
import { useIsDark } from "../dist/useIsDark";
912
import { startReactDsfr } from "../dist/spa";
1013
import { fr } from "../dist/fr";
1114
import { MuiDsfrThemeProvider } from "../dist/mui";
15+
import { TableOfContentsCustom, TocType } from "./TableOfContents";
1216

1317
startReactDsfr({
1418
"defaultColorScheme": "system",
1519
"useLang": () => "fr"
1620
});
1721

18-
export const DocsContainer = ({ children, context }) => {
22+
export const DocsContainer = ({ children, context }: PropsWithChildren<DocsContainerProps>) => {
1923
const isStorybookUiDark = useDarkMode();
2024
const { setIsDark } = useIsDark();
2125

22-
useEffect(
23-
()=> {
24-
setIsDark(isStorybookUiDark);
25-
},
26-
[isStorybookUiDark]
27-
);
26+
useEffect(() => {
27+
setIsDark(isStorybookUiDark);
28+
}, [isStorybookUiDark]);
2829

2930
const backgroundColor = fr.colors.decisions.background.default.grey.default;
3031

32+
// took from addon-docs/src/blocks/DocsContainer.tsx
33+
let toc: TocType | undefined;
34+
try {
35+
const meta = context.resolveOf("meta", ["meta"]);
36+
toc = meta.preparedMeta.parameters?.docs?.toc;
37+
} catch (err) {
38+
// No meta, falling back to project annotations
39+
toc = context?.projectAnnotations?.parameters?.docs?.toc;
40+
}
41+
3142
return (
3243
<>
3344
<style>{`
@@ -52,26 +63,12 @@ export const DocsContainer = ({ children, context }) => {
5263
}
5364
5465
`}</style>
55-
<BaseContainer
56-
context={{
57-
...context,
58-
"storyById": id => {
59-
const storyContext = context.storyById(id);
60-
return {
61-
...storyContext,
62-
"parameters": {
63-
...storyContext?.parameters,
64-
"docs": {
65-
...storyContext?.parameters?.docs,
66-
"theme": isStorybookUiDark ? darkTheme : lightTheme
67-
}
68-
}
69-
};
70-
}
71-
}}
72-
>
66+
<BaseContainer context={context} theme={isStorybookUiDark ? darkTheme : lightTheme}>
7367
<MuiDsfrThemeProvider>
74-
{children}
68+
<Unstyled>
69+
{toc && <TableOfContentsCustom channel={context.channel} />}
70+
{children}
71+
</Unstyled>
7572
</MuiDsfrThemeProvider>
7673
</BaseContainer>
7774
</>

.storybook/Stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react";
2+
import { Stories as BaseStories } from "@storybook/addon-docs/blocks";
3+
import { type PropsOf } from "@emotion/react";
4+
5+
export const Stories = (props: PropsOf<typeof BaseStories>) => {
6+
return <BaseStories {...props} />;
7+
};

.storybook/TableOfContents.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { styled } from "storybook/theming";
2+
import SideMenu, { SideMenuProps } from "../dist/SideMenu";
3+
import Channel from "storybook/internal/channels";
4+
import { NAVIGATE_URL } from "storybook/internal/core-events";
5+
import React, { useEffect, useState } from "react";
6+
import { DocsTypes } from "@storybook/addon-docs";
7+
8+
export type TocType = Exclude<Required<DocsTypes["parameters"]>["docs"]["toc"], undefined>;
9+
10+
const Aside = styled.div`
11+
position: fixed;
12+
right: 4rem;
13+
top: 0;
14+
bottom: 0;
15+
width: 16rem;
16+
z-index: 1;
17+
overflow: auto;
18+
padding-top: 4rem;
19+
padding-bottom: 2rem;
20+
padding-left: 1rem;
21+
padding-right: 1rem;
22+
23+
-webkit-font-smoothing: antialiased;
24+
-moz-osx-font-smoothing: grayscale;
25+
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
26+
-webkit-overflow-scrolling: touch;
27+
28+
@media (max-width: 768px) {
29+
display: none;
30+
}
31+
32+
& .fr-sidemenu__inner {
33+
padding: 0;
34+
}
35+
36+
& .fr-sidemenu__link {
37+
padding: 0.5rem 0.75rem;
38+
}
39+
`;
40+
41+
/**
42+
* Hook pour détecter le heading actuellement visible avec IntersectionObserver
43+
*/
44+
function useActiveHeading(headings: HTMLHeadingElement[]) {
45+
const [activeId, setActiveId] = useState<string>("");
46+
47+
useEffect(() => {
48+
if (headings.length === 0) return;
49+
50+
// Map pour stocker les ratios d'intersection de chaque heading
51+
const headingObservers = new Map<string, number>();
52+
53+
const observer = new IntersectionObserver(
54+
entries => {
55+
// Mettre à jour le ratio d'intersection pour chaque heading observé
56+
entries.forEach(entry => {
57+
const id = entry.target.id;
58+
if (entry.isIntersecting) {
59+
headingObservers.set(id, entry.intersectionRatio);
60+
} else {
61+
headingObservers.set(id, 0);
62+
}
63+
});
64+
65+
// Trouver le heading avec le plus grand ratio d'intersection
66+
let maxRatio = 0;
67+
let activeHeadingId = "";
68+
69+
headingObservers.forEach((ratio, id) => {
70+
if (ratio > maxRatio) {
71+
maxRatio = ratio;
72+
activeHeadingId = id;
73+
}
74+
});
75+
76+
// Ne mettre à jour que si on a trouvé un heading visible
77+
// Sinon on garde l'état précédent
78+
if (activeHeadingId && activeHeadingId !== activeId) {
79+
setActiveId(activeHeadingId);
80+
} else if (!activeId && headings.length > 0) {
81+
// Cas initial : si aucun heading n'est actif encore, prendre le premier
82+
setActiveId(headings[0].id);
83+
}
84+
},
85+
{
86+
// rootMargin négatif = créer une zone "active" au centre du viewport
87+
// "-20% 0px -35% 0px" = zone active entre 20% du haut et 65% du bas
88+
rootMargin: "-20% 0px -35% 0px",
89+
threshold: [0, 0.25, 0.5, 0.75, 1] // Observer à différents niveaux de visibilité
90+
}
91+
);
92+
93+
// Observer tous les headings
94+
headings.forEach(heading => {
95+
if (heading.id) {
96+
observer.observe(heading);
97+
headingObservers.set(heading.id, 0);
98+
}
99+
});
100+
101+
return () => {
102+
observer.disconnect();
103+
};
104+
}, [headings, activeId]);
105+
106+
return activeId;
107+
}
108+
109+
interface TableOfContentsCustomProps {
110+
channel: Channel;
111+
}
112+
113+
export const TableOfContentsCustom = ({ channel }: TableOfContentsCustomProps) => {
114+
const [headingElements, setHeadingElements] = useState<HTMLHeadingElement[]>([]);
115+
116+
// Initialiser les headings une seule fois
117+
useEffect(() => {
118+
const contentElement = document.querySelector(".sbdocs-content");
119+
const elements = Array.from(
120+
contentElement?.querySelectorAll<HTMLHeadingElement>(
121+
"h3:not(.docs-story *, .skip-toc)"
122+
) ?? []
123+
);
124+
setHeadingElements(elements);
125+
}, []);
126+
127+
// Utiliser le hook pour tracker l'ID actif
128+
const activeId = useActiveHeading(headingElements);
129+
130+
// Créer les items avec isActive
131+
const headings = headingElements.map<SideMenuProps.Item>(heading => ({
132+
text: (heading.innerText || heading.textContent).trim(),
133+
isActive: heading.id === activeId,
134+
linkProps: {
135+
href: `#${heading.id}`,
136+
onClick(e) {
137+
e.preventDefault();
138+
if (e.currentTarget instanceof HTMLAnchorElement) {
139+
const [, headerId] = e.currentTarget.href.split("#");
140+
if (headerId) {
141+
channel.emit(NAVIGATE_URL, { url: `#${headerId}` });
142+
document.querySelector(`#${heading.id}`)?.scrollIntoView({
143+
behavior: "smooth"
144+
});
145+
}
146+
}
147+
}
148+
}
149+
}));
150+
151+
return (
152+
<Aside>
153+
<SideMenu align="right" burgerMenuButtonText="Table des matières" items={headings} />
154+
</Aside>
155+
);
156+
};

.storybook/customTheme.js

Lines changed: 0 additions & 35 deletions
This file was deleted.

.storybook/customTheme.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { create } from "storybook/theming";
2+
3+
const brandImage = "logo.png";
4+
const brandTitle = "@codegouvfr/react-dsfr";
5+
const brandUrl = "https://github.com/codegouvfr/react-dsfr";
6+
const fontBase = '"Marianne", arial, sans-serif';
7+
const fontCode = "monospace";
8+
9+
export const darkTheme = create({
10+
base: "dark",
11+
appBg: "#1E1E1E",
12+
appContentBg: "#161616",
13+
barBg: "#161616",
14+
colorSecondary: "#8585F6",
15+
textColor: "#FFFFFF",
16+
brandImage,
17+
brandTitle,
18+
brandUrl,
19+
fontBase,
20+
fontCode
21+
});
22+
23+
export const lightTheme = create({
24+
base: "light",
25+
appBg: "#F6F6F6",
26+
appContentBg: "#FFFFFF",
27+
barBg: "#FFFFFF",
28+
colorSecondary: "#000091",
29+
textColor: "#212121",
30+
brandImage,
31+
brandTitle,
32+
brandUrl,
33+
fontBase,
34+
fontCode
35+
});

.storybook/main.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

.storybook/main.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineMain } from "@storybook/react-vite/node";
2+
3+
export default defineMain({
4+
framework: "@storybook/react-vite",
5+
features: {
6+
backgrounds: false
7+
},
8+
stories: [
9+
"../stories/*.mdx",
10+
"../stories/*.stories.@(ts|tsx)",
11+
"../stories/blocks/*.stories.@(ts|tsx)",
12+
"../stories/charts/*.stories.@(ts|tsx)"
13+
],
14+
addons: [
15+
"@vueless/storybook-dark-mode",
16+
"@storybook/addon-links",
17+
"@storybook/addon-a11y",
18+
"@storybook/addon-docs"
19+
],
20+
staticDirs: ["../dist", "./static"]
21+
});

.storybook/manager-head.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,24 @@
3333
[data-parent-id^="hidden"] {
3434
display: none !important;
3535
}
36+
37+
/* full manager loader (circle) */
38+
body.dark div[aria-label^="Content is loading..."] {
39+
border-color: rgb(133, 133, 246) rgba(130, 130, 243, 0.29) rgba(130, 130, 243, 0.29) !important;
40+
mix-blend-mode: normal !important;
41+
}
42+
43+
body:not(.dark) div[aria-label^="Content is loading..."] {
44+
border-color: rgb(0, 0, 145) rgba(0, 0, 142, 0.29) rgba(0, 0, 142, 0.29) !important;
45+
mix-blend-mode: normal !important;
46+
}
47+
48+
/* full manager page loader (dsfr vars not available) */
49+
body.dark section[aria-labelledby="main-preview-heading"] div:has(+ #storybook-preview-wrapper) {
50+
background-color: #161616 !important;
51+
}
52+
53+
body:not(.dark) section[aria-labelledby="main-preview-heading"] div:has(+ #storybook-preview-wrapper) {
54+
background-color: #ffffff !important;
55+
}
3656
</style>

0 commit comments

Comments
 (0)