diff --git a/.eslintignore b/.eslintignore index c783be4f4..5169dece6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,4 @@ CHANGELOG.md .yarn_home/ /test/integration/ /storybook-static/ - +!.storybook diff --git a/.storybook/DocsContainer.js b/.storybook/DocsContainer.js deleted file mode 100644 index 868a401fb..000000000 --- a/.storybook/DocsContainer.js +++ /dev/null @@ -1,79 +0,0 @@ - -import React, { useEffect } from "react"; -import { DocsContainer as BaseContainer } from "@storybook/addon-docs"; -import { useDarkMode } from "storybook-dark-mode"; -import { darkTheme, lightTheme } from "./customTheme"; -import "../dist/dsfr/utility/icons/icons.min.css"; -import "../dist/dsfr/dsfr.css"; -import { useIsDark } from "../dist/useIsDark"; -import { startReactDsfr } from "../dist/spa"; -import { fr } from "../dist/fr"; -import { MuiDsfrThemeProvider } from "../dist/mui"; - -startReactDsfr({ - "defaultColorScheme": "system", - "useLang": () => "fr" -}); - -export const DocsContainer = ({ children, context }) => { - const isStorybookUiDark = useDarkMode(); - const { setIsDark } = useIsDark(); - - useEffect( - ()=> { - setIsDark(isStorybookUiDark); - }, - [isStorybookUiDark] - ); - - const backgroundColor = fr.colors.decisions.background.default.grey.default; - - return ( - <> - - { - const storyContext = context.storyById(id); - return { - ...storyContext, - "parameters": { - ...storyContext?.parameters, - "docs": { - ...storyContext?.parameters?.docs, - "theme": isStorybookUiDark ? darkTheme : lightTheme - } - } - }; - } - }} - > - - {children} - - - - ); -}; diff --git a/.storybook/DocsContainer/BaseContainer.tsx b/.storybook/DocsContainer/BaseContainer.tsx new file mode 100644 index 000000000..ee492160d --- /dev/null +++ b/.storybook/DocsContainer/BaseContainer.tsx @@ -0,0 +1,70 @@ +import { TableOfContentsCustom, TocType } from "../TableOfContents"; +import { ThemeProvider, ThemeVars, ensure as ensureTheme } from "storybook/theming"; +import { DocsPageWrapper } from "./DocsPageWrapper"; +import { + DocsContainerProps, + DocsContext, + SourceContainer, + Unstyled +} from "@storybook/addon-docs/blocks"; +import React, { PropsWithChildren, useEffect } from "react"; + +// From @storybook/addon-docs/blocks/DocsContainer.tsx + +const { document, window: globalWindow } = globalThis; +export const BaseContainer = ({ + context, + theme, + children +}: PropsWithChildren) => { + let toc: TocType | undefined; + try { + const meta = context.resolveOf("meta", ["meta"]); + toc = meta.preparedMeta.parameters?.docs?.toc; + } catch (err) { + // No meta, falling back to project annotations + toc = context?.projectAnnotations?.parameters?.docs?.toc; + } + + useEffect(() => { + let url: URL; + try { + url = new URL(globalWindow.parent.location.toString()); + if (url.hash) { + const element = document.getElementById(decodeURIComponent(url.hash.substring(1))); + if (element) { + // Introducing a delay to ensure scrolling works when it's a full refresh. + setTimeout(() => { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); + }, 200); + } + } + } catch { + // pass + } + }); + + return ( + + + + + {toc && } + + ) : null + } + > + {children} + + + + + ); +}; diff --git a/.storybook/DocsContainer/DocsPageWrapper.tsx b/.storybook/DocsContainer/DocsPageWrapper.tsx new file mode 100644 index 000000000..e3723b72d --- /dev/null +++ b/.storybook/DocsContainer/DocsPageWrapper.tsx @@ -0,0 +1,435 @@ +import type { FC } from "react"; +import React from "react"; + +import { withReset } from "storybook/internal/components"; + +import { transparentize } from "polished"; +import type { CSSObject } from "storybook/theming"; +import { styled } from "storybook/theming"; + +/** + * This selector styles all raw elements inside the DocsPage like this example with a `
`: + * :where(div:not(.sb-unstyled, .sb-anchor, .sb-unstyled div, .sb-unstyled div)) + * + * 1. ':where': ensures this has a specificity of 0, making it easier to override. + * 2. 'div:not(...)': selects all div elements that are not... + * 3. '.sb-anchor': Ensures anchors are not styled, which would have led to inheritable styles bleeding + * all the way down to stories + * 4. '.sb-unstyled, .sb-unstyled div': any element with sb-unstyled class, or descendants thereof + * 5. .sb-unstyled is an escape hatch that allows the user to opt-out of the default styles by wrapping + * their content in an element with the 'sb-unstyled' class or the block. + * + * Most Storybook doc blocks has the sb-unstyled class to opt-out of the default styles. + */ +const toGlobalSelector = (element: string): string => + `& :where(${element}:not(.sb-anchor, .sb-unstyled, .sb-unstyled ${element}))`; + +const breakpoint = 600; + +export const Title = styled.h1(withReset, ({ theme }) => ({ + color: theme.color.defaultText, + fontSize: theme.typography.size.m3, + fontWeight: theme.typography.weight.bold, + lineHeight: "32px", + + [`@media (min-width: ${breakpoint}px)`]: { + fontSize: theme.typography.size.l1, + lineHeight: "36px", + marginBottom: "16px" + } +})); + +export const DocsContent = styled.div(({ theme }) => { + const reset = { + fontFamily: theme.typography.fonts.base, + fontSize: theme.typography.size.s3, + margin: 0, + + WebkitFontSmoothing: "antialiased", + MozOsxFontSmoothing: "grayscale", + WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", + WebkitOverflowScrolling: "touch" + }; + const headers = { + margin: "20px 0 8px", + padding: 0, + cursor: "text", + position: "relative", + color: theme.color.defaultText, + "&:first-of-type": { + marginTop: 0, + paddingTop: 0 + }, + "&:hover a.anchor": { + textDecoration: "none" + }, + "& code": { + fontSize: "inherit" + } + }; + const code = { + lineHeight: 1, + margin: "0 2px", + padding: "3px 5px", + whiteSpace: "nowrap", + + borderRadius: 3, + fontSize: theme.typography.size.s2 - 1, + + border: + theme.base === "light" + ? `1px solid ${theme.color.mediumlight}` + : `1px solid ${theme.color.darker}`, + color: + theme.base === "light" + ? transparentize(0.1, theme.color.defaultText) + : transparentize(0.3, theme.color.defaultText), + backgroundColor: theme.base === "light" ? theme.color.lighter : theme.color.border + }; + + return { + maxWidth: 1000, + width: "100%", + minWidth: 0, + [toGlobalSelector("a")]: { + ...reset, + fontSize: "inherit", + lineHeight: "24px", + + color: theme.color.secondary, + textDecoration: "none", + "&.absent": { + color: "#cc0000" + }, + "&.anchor": { + display: "block", + paddingLeft: 30, + marginLeft: -30, + cursor: "pointer", + position: "absolute", + top: 0, + left: 0, + bottom: 0 + } + }, + [toGlobalSelector("blockquote")]: { + ...reset, + margin: "16px 0", + borderLeft: `4px solid ${theme.color.medium}`, + padding: "0 15px", + color: theme.color.dark, + "& > :first-of-type": { + marginTop: 0 + }, + "& > :last-child": { + marginBottom: 0 + } + }, + [toGlobalSelector("div")]: reset, + [toGlobalSelector("dl")]: { + ...reset, + margin: "16px 0", + padding: 0, + "& dt": { + fontSize: "14px", + fontWeight: "bold", + fontStyle: "italic", + padding: 0, + margin: "16px 0 4px" + }, + "& dt:first-of-type": { + padding: 0 + }, + "& dt > :first-of-type": { + marginTop: 0 + }, + + "& dt > :last-child": { + marginBottom: 0 + }, + + "& dd": { + margin: "0 0 16px", + padding: "0 15px" + }, + + "& dd > :first-of-type": { + marginTop: 0 + }, + + "& dd > :last-child": { + marginBottom: 0 + } + }, + [toGlobalSelector("h1")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.l1}px`, + fontWeight: theme.typography.weight.bold + }, + [toGlobalSelector("h2")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.m2}px`, + paddingBottom: 4, + borderBottom: `1px solid ${theme.appBorderColor}` + }, + [toGlobalSelector("h3")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.m1}px`, + fontWeight: theme.typography.weight.bold + }, + [toGlobalSelector("h4")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.s3}px` + }, + [toGlobalSelector("h5")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.s2}px` + }, + [toGlobalSelector("h6")]: { + ...reset, + ...headers, + fontSize: `${theme.typography.size.s2}px`, + color: theme.color.dark + }, + [toGlobalSelector("hr")]: { + border: "0 none", + borderTop: `1px solid ${theme.appBorderColor}`, + height: 4, + padding: 0 + }, + [toGlobalSelector("img")]: { + maxWidth: "100%" + }, + [toGlobalSelector("li")]: { + ...reset, + fontSize: theme.typography.size.s2, + color: theme.color.defaultText, + lineHeight: "24px", + "& + li": { + marginTop: ".25em" + }, + "& ul, & ol": { + marginTop: ".25em", + marginBottom: 0 + }, + "& code": code + }, + [toGlobalSelector("ol")]: { + ...reset, + margin: "16px 0", + paddingLeft: 30, + "& :first-of-type": { + marginTop: 0 + }, + "& :last-child": { + marginBottom: 0 + } + }, + [toGlobalSelector("p")]: { + ...reset, + margin: "16px 0", + fontSize: theme.typography.size.s2, + lineHeight: "24px", + color: theme.color.defaultText, + "& code": code + }, + [toGlobalSelector("pre")]: { + ...reset, + // reset + fontFamily: theme.typography.fonts.mono, + WebkitFontSmoothing: "antialiased", + MozOsxFontSmoothing: "grayscale", + lineHeight: "18px", + padding: "11px 1rem", + whiteSpace: "pre-wrap", + color: "inherit", + borderRadius: 3, + margin: "1rem 0", + + "&:not(.prismjs)": { + background: "transparent", + border: "none", + borderRadius: 0, + padding: 0, + margin: 0 + }, + "& pre, &.prismjs": { + padding: 15, + margin: 0, + whiteSpace: "pre-wrap", + color: "inherit", + fontSize: "13px", + lineHeight: "19px", + code: { + color: "inherit", + fontSize: "inherit" + } + }, + "& code": { + whiteSpace: "pre" + }, + "& code, & tt": { + border: "none" + } + }, + [toGlobalSelector("span")]: { + ...reset, + "&.frame": { + display: "block", + overflow: "hidden", + + "& > span": { + border: `1px solid ${theme.color.medium}`, + display: "block", + float: "left", + overflow: "hidden", + margin: "13px 0 0", + padding: 7, + width: "auto" + }, + "& span img": { + display: "block", + float: "left" + }, + "& span span": { + clear: "both", + color: theme.color.darkest, + display: "block", + padding: "5px 0 0" + } + }, + "&.align-center": { + display: "block", + overflow: "hidden", + clear: "both", + + "& > span": { + display: "block", + overflow: "hidden", + margin: "13px auto 0", + textAlign: "center" + }, + "& span img": { + margin: "0 auto", + textAlign: "center" + } + }, + "&.align-right": { + display: "block", + overflow: "hidden", + clear: "both", + + "& > span": { + display: "block", + overflow: "hidden", + margin: "13px 0 0", + textAlign: "right" + }, + "& span img": { + margin: 0, + textAlign: "right" + } + }, + "&.float-left": { + display: "block", + marginRight: 13, + overflow: "hidden", + float: "left", + "& span": { + margin: "13px 0 0" + } + }, + "&.float-right": { + display: "block", + marginLeft: 13, + overflow: "hidden", + float: "right", + + "& > span": { + display: "block", + overflow: "hidden", + margin: "13px auto 0", + textAlign: "right" + } + } + }, + [toGlobalSelector("table")]: { + ...reset, + margin: "16px 0", + fontSize: theme.typography.size.s2, + lineHeight: "24px", + padding: 0, + borderCollapse: "collapse", + "& tr": { + borderTop: `1px solid ${theme.appBorderColor}`, + backgroundColor: theme.appContentBg, + margin: 0, + padding: 0 + }, + "& tr:nth-of-type(2n)": { + backgroundColor: theme.base === "dark" ? theme.color.darker : theme.color.lighter + }, + "& tr th": { + fontWeight: "bold", + color: theme.color.defaultText, + border: `1px solid ${theme.appBorderColor}`, + margin: 0, + padding: "6px 13px" + }, + "& tr td": { + border: `1px solid ${theme.appBorderColor}`, + color: theme.color.defaultText, + margin: 0, + padding: "6px 13px" + }, + "& tr th :first-of-type, & tr td :first-of-type": { + marginTop: 0 + }, + "& tr th :last-child, & tr td :last-child": { + marginBottom: 0 + } + }, + [toGlobalSelector("ul")]: { + ...reset, + margin: "16px 0", + paddingLeft: 30, + "& :first-of-type": { + marginTop: 0 + }, + "& :last-child": { + marginBottom: 0 + }, + listStyle: "disc" + } + } as CSSObject; +}); + +export const DocsWrapper = styled.div(({ theme }) => ({ + background: theme.background.content, + display: "flex", + flexDirection: "row-reverse", + justifyContent: "center", + padding: "4rem 20px", + minHeight: "100vh", + boxSizing: "border-box", + gap: "3rem", + + [`@media (min-width: ${breakpoint}px)`]: {} +})); + +interface DocsPageWrapperProps { + children?: React.ReactNode; + toc?: React.ReactNode; +} + +export const DocsPageWrapper: FC = ({ children, toc }) => ( + + {toc} + {children} + +); diff --git a/.storybook/DocsContainer/index.tsx b/.storybook/DocsContainer/index.tsx new file mode 100644 index 000000000..3943e2775 --- /dev/null +++ b/.storybook/DocsContainer/index.tsx @@ -0,0 +1,59 @@ +import React, { PropsWithChildren, useEffect } from "react"; +import { DocsContainerProps, Unstyled } from "@storybook/addon-docs/blocks"; +import { useDarkMode } from "@vueless/storybook-dark-mode"; +import { darkTheme, lightTheme } from "../customTheme"; +import "../../dist/dsfr/utility/icons/icons.min.css"; +import "../../dist/dsfr/dsfr.css"; +import { useIsDark } from "../../dist/useIsDark"; +import { startReactDsfr } from "../../dist/spa"; +import { fr } from "../../dist/fr"; +import { MuiDsfrThemeProvider } from "../../dist/mui"; +import { BaseContainer } from "./BaseContainer"; + +startReactDsfr({ + "defaultColorScheme": "system", + "useLang": () => "fr" +}); + +export const DocsContainer = ({ children, context }: PropsWithChildren) => { + const isStorybookUiDark = useDarkMode(); + const { setIsDark } = useIsDark(); + + useEffect(() => { + setIsDark(isStorybookUiDark); + }, [isStorybookUiDark]); + + const backgroundColor = fr.colors.decisions.background.default.grey.default; + return ( + <> + + + + + {children} + + + + ); +}; diff --git a/.storybook/Stories.tsx b/.storybook/Stories.tsx new file mode 100644 index 000000000..77bde5d3c --- /dev/null +++ b/.storybook/Stories.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Stories as BaseStories } from "@storybook/addon-docs/blocks"; +import { type PropsOf } from "@emotion/react"; + +export const Stories = (props: PropsOf) => { + return ; +}; diff --git a/.storybook/TableOfContents.tsx b/.storybook/TableOfContents.tsx new file mode 100644 index 000000000..277467eb1 --- /dev/null +++ b/.storybook/TableOfContents.tsx @@ -0,0 +1,160 @@ +import { styled } from "storybook/theming"; +import SideMenu, { SideMenuProps } from "../dist/SideMenu"; +import Channel from "storybook/internal/channels"; +import { NAVIGATE_URL } from "storybook/internal/core-events"; +import React, { useEffect, useState } from "react"; +import { DocsTypes } from "@storybook/addon-docs"; + +export type TocType = Exclude["docs"]["toc"], undefined>; + +const Aside = styled.div({ + width: "10rem", + + "@media (max-width: 768px)": { + display: "none" + } +}); + +const SideMenuStyled = styled(SideMenu)({ + position: "fixed", + bottom: 0, + top: 0, + width: "16rem", + paddingTop: "4rem", + paddingBottom: "2rem", + overflowY: "auto", + + "-webkit-font-smoothing": "antialiased", + "-moz-osx-font-smoothing": "grayscale", + "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)", + "-webkit-overflow-scrolling": "touch", + + "& .fr-sidemenu__inner": { + padding: 0 + }, + + "& .fr-sidemenu__link": { + padding: "0.5rem 0.75rem" + } +}); + +/** + * Hook pour détecter le heading actuellement visible avec IntersectionObserver + */ +function useActiveHeading(headings: HTMLHeadingElement[]) { + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + if (headings.length === 0) return; + + // Map pour stocker les ratios d'intersection de chaque heading + const headingObservers = new Map(); + + const observer = new IntersectionObserver( + entries => { + // Mettre à jour le ratio d'intersection pour chaque heading observé + entries.forEach(entry => { + const id = entry.target.id; + if (entry.isIntersecting) { + headingObservers.set(id, entry.intersectionRatio); + } else { + headingObservers.set(id, 0); + } + }); + + // Trouver le heading avec le plus grand ratio d'intersection + let maxRatio = 0; + let activeHeadingId = ""; + + headingObservers.forEach((ratio, id) => { + if (ratio > maxRatio) { + maxRatio = ratio; + activeHeadingId = id; + } + }); + + // Ne mettre à jour que si on a trouvé un heading visible + // Sinon on garde l'état précédent + if (activeHeadingId && activeHeadingId !== activeId) { + setActiveId(activeHeadingId); + } else if (!activeId && headings.length > 0) { + // Cas initial : si aucun heading n'est actif encore, prendre le premier + setActiveId(headings[0].id); + } + }, + { + // rootMargin négatif = créer une zone "active" au centre du viewport + // "-20% 0px -35% 0px" = zone active entre 20% du haut et 65% du bas + rootMargin: "-20% 0px -35% 0px", + threshold: [0, 0.25, 0.5, 0.75, 1] // Observer à différents niveaux de visibilité + } + ); + + // Observer tous les headings + headings.forEach(heading => { + if (heading.id) { + observer.observe(heading); + headingObservers.set(heading.id, 0); + } + }); + + return () => { + observer.disconnect(); + }; + }, [headings, activeId]); + + return activeId; +} + +interface TableOfContentsCustomProps { + channel: Channel; +} + +export const TableOfContentsCustom = ({ channel }: TableOfContentsCustomProps) => { + const [headingElements, setHeadingElements] = useState([]); + + // Initialiser les headings une seule fois + useEffect(() => { + const contentElement = document.querySelector(".sbdocs-content"); + const elements = Array.from( + contentElement?.querySelectorAll( + "h3:not(.docs-story *, .skip-toc)" + ) ?? [] + ); + setHeadingElements(elements); + }, []); + + // Utiliser le hook pour tracker l'ID actif + const activeId = useActiveHeading(headingElements); + + // Créer les items avec isActive + const headings = headingElements.map(heading => ({ + text: (heading.innerText || heading.textContent).trim(), + isActive: heading.id === activeId, + linkProps: { + href: `#${heading.id}`, + onClick(e) { + e.preventDefault(); + if (e.currentTarget instanceof HTMLAnchorElement) { + const [, headerId] = e.currentTarget.href.split("#"); + if (headerId) { + channel.emit(NAVIGATE_URL, { url: `#${headerId}` }); + document.querySelector(`#${heading.id}`)?.scrollIntoView({ + behavior: "smooth" + }); + } + } + } + } + })); + + return ( + + ); +}; diff --git a/.storybook/customTheme.js b/.storybook/customTheme.js deleted file mode 100644 index fac2e29a0..000000000 --- a/.storybook/customTheme.js +++ /dev/null @@ -1,35 +0,0 @@ -import { create } from "@storybook/theming"; - -const brandImage= "logo.png"; -const brandTitle= "@codegouvfr/react-dsfr"; -const brandUrl= "https://github.com/codegouvfr/react-dsfr"; -const fontBase= '"Marianne", arial, sans-serif'; -const fontCode= "monospace"; - -export const darkTheme = create({ - "base": "dark", - "appBg": "#1E1E1E", - "appContentBg": "#161616", - "barBg": "#161616", - "colorSecondary": "#8585F6", - "textColor": "#FFFFFF", - brandImage, - brandTitle, - brandUrl, - fontBase, - fontCode -}); - -export const lightTheme = create({ - "base": "light", - "appBg": "#F6F6F6", - "appContentBg": "#FFFFFF", - "barBg": "#FFFFFF", - "colorSecondary": "#000091", - "textColor": "#212121", - brandImage, - brandTitle, - brandUrl, - fontBase, - fontCode -}); diff --git a/.storybook/customTheme.ts b/.storybook/customTheme.ts new file mode 100644 index 000000000..54e08e63b --- /dev/null +++ b/.storybook/customTheme.ts @@ -0,0 +1,35 @@ +import { create } from "storybook/theming"; + +const brandImage = "logo.png"; +const brandTitle = "@codegouvfr/react-dsfr"; +const brandUrl = "https://github.com/codegouvfr/react-dsfr"; +const fontBase = '"Marianne", arial, sans-serif'; +const fontCode = "monospace"; + +export const darkTheme = create({ + base: "dark", + appBg: "#1E1E1E", + appContentBg: "#161616", + barBg: "#161616", + colorSecondary: "#8585F6", + textColor: "#FFFFFF", + brandImage, + brandTitle, + brandUrl, + fontBase, + fontCode +}); + +export const lightTheme = create({ + base: "light", + appBg: "#F6F6F6", + appContentBg: "#FFFFFF", + barBg: "#FFFFFF", + colorSecondary: "#000091", + textColor: "#212121", + brandImage, + brandTitle, + brandUrl, + fontBase, + fontCode +}); diff --git a/.storybook/main.js b/.storybook/main.js deleted file mode 100644 index 8ae8ed940..000000000 --- a/.storybook/main.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - "stories": [ - "../stories/*.stories.mdx", - "../stories/*.stories.@(ts|tsx)", - "../stories/blocks/*.stories.@(ts|tsx)", - "../stories/charts/*.stories.@(ts|tsx)", - "../stories/picto/*.stories.@(ts|tsx)", - ], - "addons": [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "storybook-dark-mode", - "@storybook/addon-a11y" - ], - "core": { - "builder": "webpack5" - }, - "staticDirs": ["../dist", "./static"] -}; diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 000000000..08c6ab4b0 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,24 @@ +import { defineMain } from "@storybook/react-vite/node"; + +export default defineMain({ + framework: "@storybook/react-vite", + features: { + backgrounds: false + }, + stories: [ + "../stories/*.mdx", + "../stories/*.stories.@(ts|tsx)", + "../stories/blocks/*.stories.@(ts|tsx)", + "../stories/charts/*.stories.@(ts|tsx)" + ], + addons: [ + "@vueless/storybook-dark-mode", + "@storybook/addon-links", + "@storybook/addon-a11y", + "@storybook/addon-docs" + ], + staticDirs: ["../dist", "./static"], + docs: { + docsMode: true + } +}); diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html index 69d9dc1da..4c56c0f76 100644 --- a/.storybook/manager-head.html +++ b/.storybook/manager-head.html @@ -33,4 +33,24 @@ [data-parent-id^="hidden"] { display: none !important; } + + /* full manager loader (circle) */ + body.dark div[aria-label^="Content is loading..."] { + border-color: rgb(133, 133, 246) rgba(130, 130, 243, 0.29) rgba(130, 130, 243, 0.29) !important; + mix-blend-mode: normal !important; + } + + body:not(.dark) div[aria-label^="Content is loading..."] { + border-color: rgb(0, 0, 145) rgba(0, 0, 142, 0.29) rgba(0, 0, 142, 0.29) !important; + mix-blend-mode: normal !important; + } + + /* full manager page loader (dsfr vars not available) */ + body.dark section[aria-labelledby="main-preview-heading"] div:has(+ #storybook-preview-wrapper) { + background-color: #161616 !important; + } + + body:not(.dark) section[aria-labelledby="main-preview-heading"] div:has(+ #storybook-preview-wrapper) { + background-color: #ffffff !important; + } \ No newline at end of file diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..62389d135 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,53 @@ + \ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js deleted file mode 100644 index e1cbc3033..000000000 --- a/.storybook/preview.js +++ /dev/null @@ -1,128 +0,0 @@ -import { darkTheme, lightTheme } from "./customTheme"; -import { DocsContainer } from "./DocsContainer"; - -export const parameters = { - "actions": { "argTypesRegex": "^on[A-Z].*" }, - "controls": { - "matchers": { - "color": /(background|color)$/i, - "date": /Date$/, - }, - }, - "backgrounds": { "disable": true }, - "darkMode": { - "light": lightTheme, - "dark": darkTheme, - }, - "docs": { - "container": DocsContainer - }, - "viewport": { - "viewports": { - "1440p": { - "name": "1440p", - "styles": { - "width": "2560px", - "height": "1440px", - }, - }, - "fullHD": { - "name": "Full HD", - "styles": { - "width": "1920px", - "height": "1080px", - }, - }, - "macBookProBig": { - "name": "MacBook Pro Big", - "styles": { - "width": "1024px", - "height": "640px", - }, - }, - "macBookProMedium": { - "name": "MacBook Pro Medium", - "styles": { - "width": "1440px", - "height": "900px", - }, - }, - "macBookProSmall": { - "name": "MacBook Pro Small", - "styles": { - "width": "1680px", - "height": "1050px", - }, - }, - "pcAgent": { - "name": "PC Agent", - "styles": { - "width": "960px", - "height": "540px", - }, - }, - "iphone12Pro": { - "name": "Iphone 12 pro", - "styles": { - "width": "390px", - "height": "844px", - }, - }, - "iphone5se":{ - "name": "Iphone 5/SE", - "styles": { - "width": "320px", - "height": "568px", - }, - }, - "ipadPro": { - "name": "Ipad pro", - "styles": { - "width": "1240px", - "height": "1366px", - }, - }, - "Galaxy s9+": { - "name": "Galaxy S9+", - "styles": { - "width": "320px", - "height": "658px", - }, - } - }, - }, - "options": { - "storySort": (a, b) => - getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind), - }, -}; - -const { getHardCodedWeight } = (() => { - - const orderedPagesPrefix = [ - "🇫🇷 Introduction", - //"components", - "components/Header", - "components/Footer", - "components/consentManagement", - "components/Alert", - "components/Tabs", - "components/Stepper", - "components/Button", - "components/FranceConnectButton", - "components/ProConnectButton" - ]; - - function getHardCodedWeight(kind) { - - for (let i = 0; i < orderedPagesPrefix.length; i++) { - if (kind.toLowerCase().startsWith(orderedPagesPrefix[i].toLowerCase())) { - return orderedPagesPrefix.length - i; - } - } - - return 0; - } - - return { getHardCodedWeight }; -})(); diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 000000000..cd7d8382c --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,162 @@ +import { darkTheme, lightTheme } from "./customTheme"; +import { DocsContainer } from "./DocsContainer"; +import { definePreview } from "@storybook/react-vite"; +import addonDocs from "@storybook/addon-docs"; +import addonLinks from "@storybook/addon-links"; +import addonA11y from "@storybook/addon-a11y"; +import { AddonTypes, PreviewAddon } from "storybook/internal/csf"; +import { ThemeVars } from "storybook/theming"; +import { ViewMode } from "storybook/internal/types"; + +interface DarkModeTypes extends AddonTypes { + parameters: { + darkMode?: { + light: ThemeVars; + dark: ThemeVars; + }; + }; +} + +interface ViewModeTypes extends AddonTypes { + parameters: { + viewMode?: ViewMode; + }; +} + +interface PreviewTabsTypes extends AddonTypes { + parameters: { + previewTabs?: { + [key: string]: { + hidden: boolean; + }; + }; + }; +} + +const noop = () => ({} as PreviewAddon); + +export default definePreview({ + // CSF Next syntax with custom additions for autocompletion + addons: [ + addonDocs(), + addonLinks(), + addonA11y(), + noop(), + noop(), + noop() + ], + tags: ["autodocs"], + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } + }, + viewMode: "canvas", + darkMode: { + light: lightTheme, + dark: darkTheme + }, + docs: { + container: DocsContainer, + toc: true + }, + viewport: { + options: { + "1440p": { + "name": "1440p", + "styles": { + "width": "2560px", + "height": "1440px" + } + }, + "fullHD": { + "name": "Full HD", + "styles": { + "width": "1920px", + "height": "1080px" + } + }, + "macBookProBig": { + "name": "MacBook Pro Big", + "styles": { + "width": "1024px", + "height": "640px" + } + }, + "macBookProMedium": { + "name": "MacBook Pro Medium", + "styles": { + "width": "1440px", + "height": "900px" + } + }, + "macBookProSmall": { + "name": "MacBook Pro Small", + "styles": { + "width": "1680px", + "height": "1050px" + } + }, + "pcAgent": { + "name": "PC Agent", + "styles": { + "width": "960px", + "height": "540px" + } + }, + "iphone12Pro": { + "name": "Iphone 12 pro", + "styles": { + "width": "390px", + "height": "844px" + } + }, + "iphone5se": { + "name": "Iphone 5/SE", + "styles": { + "width": "320px", + "height": "568px" + } + }, + "ipadPro": { + "name": "Ipad pro", + "styles": { + "width": "1240px", + "height": "1366px" + } + }, + "Galaxy s9+": { + "name": "Galaxy S9+", + "styles": { + "width": "320px", + "height": "658px" + } + } + } + }, + options: { + storySort: { + method: "alphabetical", + order: [ + "🇫🇷 Introduction", + "components", + [ + "Header", + "Footer", + "consentManagement", + "Alert", + "Tabs", + "Stepper", + "Button", + "FranceConnectButton", + "ProConnectButton", + "*" + ] + ] + } + } + } +}); diff --git a/package.json b/package.json index 9551763ce..92473e3e1 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "_format": "prettier '**/*.{ts,tsx,json,md}'", "format": "yarn _format --write", "format:check": "yarn _format --list-different", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook", + "storybook": "storybook dev -p 6006", + "storybook:dev": "storybook dev -p 6006", + "build-storybook": "storybook build", "prestorybook": "yarn build && node dist/bin/react-dsfr update-icons", "prebuild-storybook": "yarn prestorybook" }, @@ -83,15 +84,10 @@ "@gouvfr/dsfr-chart": "^1.0.0", "@mui/icons-material": "^5.14.18", "@mui/material": "^5.14.18", - "@storybook/addon-a11y": "^6.5.16", - "@storybook/addon-actions": "^6.5.13", - "@storybook/addon-essentials": "^6.5.13", - "@storybook/addon-interactions": "^6.5.13", - "@storybook/addon-links": "^6.5.13", - "@storybook/builder-webpack5": "^6.5.13", - "@storybook/manager-webpack5": "^6.5.13", - "@storybook/react": "^6.5.13", - "@storybook/testing-library": "^0.0.13", + "@storybook/addon-a11y": "10.2.1", + "@storybook/addon-docs": "10.2.1", + "@storybook/addon-links": "10.2.1", + "@storybook/react-vite": "^10.2.1", "@tanstack/react-virtual": "^3.0.0-beta.39", "@types/css": "^0.0.33", "@types/jsdom": "^21.1.7", @@ -100,14 +96,16 @@ "@types/node": "^18.7.18", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", + "@types/yargs-parser": "^21.0.3", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", + "@vueless/storybook-dark-mode": "^10.0.7", "babel-loader": "^8.3.0", "chromatic": "^6.17.2", "css": "^3.0.0", - "eslint": "^7.26.0", + "eslint": "^8", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-storybook": "^0.6.7", + "eslint-plugin-storybook": "10.2.1", "evt": "^2.4.2", "fzf": "^0.5.1", "husky": "^4.3.8", @@ -118,17 +116,19 @@ "next": "13.5.1", "parse-numeric-range": "^1.3.0", "patch-package": "^8.0.0", + "polished": "^4.3.1", "powerhooks": "^0.22.0", "prettier": "^2.3.0", "react": "18.2.0", "react-dom": "18.2.0", "remixicon": "^4.2.0", - "storybook-dark-mode": "^1.1.2", + "storybook": "10.2.1", "svgo": "^3.3.2", "ts-node": "^10.9.1", "tss-react": "^4.9.1", "type-route": "^1.0.1", "typescript": "^4.9.1", + "vite": "^7.0.0", "vitest": "^0.24.3" }, "main": "dist/fr/index.js", diff --git a/src/Table.tsx b/src/Table.tsx index 7a67b7a84..898bafa57 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -31,7 +31,17 @@ export namespace TableProps { type ExtractColorVariant = FrClassName extends `fr-table--${infer AccentColor}` ? Exclude< AccentColor, - "no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered" + | "no-scroll" + | "no-caption" + | "caption-bottom" + | "layout-fixed" + | "bordered" + | "sm" + | "md" + | "lg" + | "xl" + | "xs" + | "multiline" > : never; diff --git a/stories/Accordion.stories.tsx b/stories/Accordion.stories.tsx index 3f867037b..4bc31786f 100644 --- a/stories/Accordion.stories.tsx +++ b/stories/Accordion.stories.tsx @@ -1,9 +1,7 @@ import { Accordion } from "../dist/Accordion"; import { getStoryFactory, logCallbacks } from "./getStory"; -import { sectionName } from "./sectionName"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { Accordion }, argTypes: { "label": { @@ -60,7 +58,7 @@ function ControlledAccordion() { "disabledProps": ["lang"] }); -export default meta; +export default { ...meta, title: "components/Accordion" }; export const Default = getStory({ "label": "Name of the Accordion", diff --git a/stories/AgentConnectButton.stories.tsx b/stories/AgentConnectButton.stories.tsx index 73ca74e27..49e23bb32 100644 --- a/stories/AgentConnectButton.stories.tsx +++ b/stories/AgentConnectButton.stories.tsx @@ -1,16 +1,14 @@ import { AgentConnectButton } from "../dist/AgentConnectButton"; -import { sectionName } from "./sectionName"; import { getStoryFactory, logCallbacks } from "./getStory"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { AgentConnectButton }, "description": ` - [See AgentConnect documentation](https://github.com/france-connect/Documentation-AgentConnect/blob/main/doc_fs/implementation_fca/bouton_fca.md) - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/AgentConnectButton.tsx)` }); -export default meta; +export default { ...meta, title: "components/AgentConnectButton" }; export const Default = getStory({ "url": "https://example.com" diff --git a/stories/Alert.stories.tsx b/stories/Alert.stories.tsx index 0eb80fd58..a3bb5ea46 100644 --- a/stories/Alert.stories.tsx +++ b/stories/Alert.stories.tsx @@ -1,11 +1,10 @@ import { Alert, type AlertProps } from "../dist/Alert"; -import { sectionName } from "./sectionName"; + import { getStoryFactory, logCallbacks } from "./getStory"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { Alert }, "description": ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/alerte) @@ -78,13 +77,13 @@ const [ isClosed, setIsClosed ] = useState(false); this means that when the close button is clicked the \`onClose()\` callback will be called but you are responsible for setting \`isClosed\` to \`false\`, the \`\` wont close itself.`, - "control": { "type": null } + "control": false } }, "disabledProps": ["lang"] }); -export default meta; +export default { ...meta, title: "components/Alert" }; export const Default = getStory({ "severity": "success", diff --git a/stories/Badge.stories.tsx b/stories/Badge.stories.tsx index 724774c7d..010f5155f 100644 --- a/stories/Badge.stories.tsx +++ b/stories/Badge.stories.tsx @@ -1,11 +1,10 @@ import { Badge, type BadgeProps } from "../dist/Badge"; -import { sectionName } from "./sectionName"; + import { getStoryFactory } from "./getStory"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { Badge }, description: ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/badge) @@ -49,7 +48,7 @@ const { meta, getStory } = getStoryFactory({ "disabledProps": ["lang"] }); -export default meta; +export default { ...meta, title: "components/Badge" }; export const Default = getStory({ "severity": "success", diff --git a/stories/Breadcrumb.stories.tsx b/stories/Breadcrumb.stories.tsx index 51a812bc3..5f0d4c047 100644 --- a/stories/Breadcrumb.stories.tsx +++ b/stories/Breadcrumb.stories.tsx @@ -1,9 +1,8 @@ import { Breadcrumb } from "../dist/Breadcrumb"; -import { sectionName } from "./sectionName"; + import { getStoryFactory } from "./getStory"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { Breadcrumb }, "description": ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/fil-d-ariane) @@ -11,7 +10,7 @@ const { meta, getStory } = getStoryFactory({ "disabledProps": ["lang"] }); -export default meta; +export default { ...meta, title: "components/Breadcrumb" }; export const Default = getStory({ "homeLinkProps": { "href": "/" }, diff --git a/stories/Button.stories.tsx b/stories/Button.stories.tsx index eee756cf3..12e8561d7 100644 --- a/stories/Button.stories.tsx +++ b/stories/Button.stories.tsx @@ -1,11 +1,10 @@ import { Button, type ButtonProps } from "../dist/Button"; -import { sectionName } from "./sectionName"; + import { getStoryFactory, logCallbacks } from "./getStory"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { Button }, "description": ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bouton) @@ -64,17 +63,17 @@ const { meta, getStory } = getStoryFactory({ "nativeButtonProps": { "description": `Can be used to attach extra props to the underlying native button. Example: \`{ "aria-controls": "fr-modal-1", onMouseEnter: event => {...} }\``, - "control": { "type": null } + "control": false }, "children": { "description": "The label of the button", - "control": { "type": "string" } + "control": { "type": "text" } } }, "disabledProps": ["lang"] }); -export default meta; +export default { ...meta, title: "components/Button" }; export const Default = getStory({ "children": "Label button", diff --git a/stories/ButtonsGroup.stories.tsx b/stories/ButtonsGroup.stories.tsx index 2fd3be8a1..d77046180 100644 --- a/stories/ButtonsGroup.stories.tsx +++ b/stories/ButtonsGroup.stories.tsx @@ -1,11 +1,10 @@ import { ButtonsGroup, type ButtonsGroupProps } from "../dist/ButtonsGroup"; -import { sectionName } from "./sectionName"; + import { getStoryFactory } from "./getStory"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { ButtonsGroup }, "description": ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/groupe-de-boutons) @@ -89,14 +88,14 @@ const { meta, getStory } = getStoryFactory({ }, "buttons": { "description": `An array of ButtonProps (at least 1)`, - "control": { "type": null } + "control": false } }, "disabledProps": ["lang"], "defaultContainerWidth": 800 }); -export default meta; +export default { ...meta, title: "components/ButtonsGroup" }; export const Default = getStory({ "buttons": [ diff --git a/stories/CallOut.stories.tsx b/stories/CallOut.stories.tsx index 9732d3f98..5c23c2a87 100644 --- a/stories/CallOut.stories.tsx +++ b/stories/CallOut.stories.tsx @@ -1,11 +1,10 @@ import { CallOut, type CallOutProps } from "../dist/CallOut"; -import { sectionName } from "./sectionName"; + import { getStoryFactory, logCallbacks } from "./getStory"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; const { meta, getStory } = getStoryFactory({ - sectionName, "wrappedComponent": { CallOut }, "description": ` - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mise-en-avant) @@ -15,6 +14,10 @@ const { meta, getStory } = getStoryFactory({ "title": { "description": "Optional" }, + "children": { + "description": "Optional", + "control": { "type": "text" } + }, "colorVariant": { "options": (() => { const options = [ @@ -63,12 +66,12 @@ const { meta, getStory } = getStoryFactory({ "buttonProps": { "description": "The same props you would pass to a `