├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── DocsContainer.js ├── customTheme.js ├── main.js ├── manager-head.html ├── preview.js └── static │ ├── CNAME │ ├── logo.png │ └── repo-card.png ├── CONTRIBUTING.md ├── LICENSE ├── README.fr.md ├── README.md ├── package.json ├── patches └── @gouvfr+dsfr+1.13.2.patch ├── publiccode.yml ├── scripts ├── README.md ├── build │ ├── build.ts │ ├── cssToTs │ │ ├── breakpoints.ts │ │ ├── classNames.ts │ │ ├── colorDecisionAndCorrespondingOptions.ts │ │ ├── colorDecisions.ts │ │ ├── colorOptions.ts │ │ ├── cssToTs.ts │ │ ├── cssVariable.ts │ │ ├── index.ts │ │ ├── spacing.ts │ │ └── typography.ts │ ├── icons.ts │ ├── index.ts │ ├── main.css │ ├── marianne-index.css │ ├── parseCss.ts │ └── patchCssForMui.ts ├── link-in-external-project.ts ├── link-in-integration-apps.ts ├── picto-builder.ts ├── tools │ ├── cssColorRegexp.ts │ ├── multiReplace.ts │ ├── threeDigitColorHexToSixDigitsColorHex.ts │ └── tsc.ts └── tsconfig.json ├── src ├── Accordion.tsx ├── AgentConnectButton.tsx ├── Alert.tsx ├── Badge.tsx ├── Breadcrumb.tsx ├── Button.tsx ├── ButtonsGroup.tsx ├── CallOut.tsx ├── Card.tsx ├── Chart │ ├── BarChart.tsx │ ├── BarLineChart.tsx │ ├── GaugeChart.tsx │ ├── LineChart.tsx │ ├── MultiLineChart.tsx │ ├── PieChart.tsx │ ├── RadarChart.tsx │ ├── ScatterChart.tsx │ └── chartWrapper.tsx ├── Checkbox.tsx ├── Display │ ├── Artwork │ │ ├── Artwork.tsx │ │ ├── ArtworkGov.tsx │ │ ├── ArtworkWhiteLabel │ │ │ ├── ArtworkWhiteLabel.tsx │ │ │ ├── DisplayArtworkWhiteLabelProvider.tsx │ │ │ ├── context.ts │ │ │ └── useArtworkWhiteLabel.ts │ │ └── index.ts │ ├── Display.tsx │ └── index.ts ├── Download.tsx ├── Follow.tsx ├── Footer.tsx ├── FranceConnectButton.tsx ├── Header │ ├── Header.tsx │ ├── index.ts │ └── useIsHeaderMenuModalOpen.ts ├── Highlight.tsx ├── Input.tsx ├── LanguageSelect.tsx ├── MainNavigation │ ├── MainNavigation.tsx │ ├── MegaMenu.tsx │ ├── Menu.tsx │ └── index.ts ├── Modal │ ├── Modal.tsx │ ├── index.ts │ └── useIsModalOpen.ts ├── MonCompteProButton.tsx ├── Notice.tsx ├── Pagination.tsx ├── ProConnectButton.tsx ├── Quote.tsx ├── RadioButtons.tsx ├── Range.tsx ├── SearchBar │ ├── SearchBar.tsx │ ├── SearchButton.tsx │ └── index.tsx ├── SegmentedControl.tsx ├── Select.tsx ├── SelectNext.tsx ├── SideMenu.tsx ├── SkipLinks.tsx ├── Stepper.tsx ├── Summary.tsx ├── Table.tsx ├── Tabs.tsx ├── Tag.tsx ├── TagsGroup.tsx ├── Tile.tsx ├── ToggleSwitch.tsx ├── ToggleSwitchGroup.tsx ├── Tooltip.tsx ├── Upload.tsx ├── assets │ ├── agentconnect-btn-alternatif-hover.svg │ ├── agentconnect-btn-alternatif.svg │ ├── agentconnect-btn-principal-hover.svg │ ├── agentconnect-btn-principal.svg │ ├── agentconnect.css │ ├── blank-favicon.svg │ ├── dsfr_plus_icons.css │ ├── dsfr_plus_icons.scss │ ├── language-select.css │ ├── moncomptepro.css │ ├── proconnect-btn.css │ └── search-bar.css ├── bin │ ├── README.md │ ├── copy-dsfr-to-public.ts │ ├── only-include-used-icons.ts │ ├── react-dsfr.ts │ ├── readPublicDirPath.ts │ ├── tools │ │ ├── crawl.ts │ │ ├── crawlSync.ts │ │ ├── fnv1aHashToHex.ts │ │ ├── fs.existsAsync.ts │ │ ├── getAbsoluteAndInOsFormatPath.ts │ │ ├── getProjectRoot.ts │ │ ├── modifyHtmlHrefs.ts │ │ └── transformCodebase.ts │ └── tsconfig.json ├── blocks │ └── PasswordInput.tsx ├── consentManagement │ ├── ConsentBannerAndConsentManagement │ │ ├── ConsentBanner.tsx │ │ ├── ConsentBannerAndConsentManagement.tsx │ │ ├── ConsentManagement.tsx │ │ ├── index.ts │ │ └── translation.tsx │ ├── Placeholder.tsx │ ├── createConsentManagement.ts │ ├── index.ts │ ├── processConsentChanges.ts │ ├── types.ts │ └── useConsent.ts ├── eulerianAnalytics.ts ├── fr │ ├── breakpoints.ts │ ├── colors.ts │ ├── cx.ts │ ├── index.ts │ ├── shadows.ts │ └── spacing.ts ├── global.d.ts ├── i18n.ts ├── link.tsx ├── mui │ ├── index.ts │ ├── mui.tsx │ └── useIsGov.tsx ├── next-app-router │ ├── DsfrHead.tsx │ ├── DsfrProvider.tsx │ ├── getHtmlAttributes.tsx │ ├── index.ts │ ├── server-only-index.ts │ └── zz_internal │ │ ├── defaultColorScheme.ts │ │ └── fontUrlByFileBasename.ts ├── next-pagesdir.tsx ├── picto │ ├── Accessibility.tsx │ ├── Airport.tsx │ ├── Application.tsx │ ├── Archive.tsx │ ├── ArmyTank.tsx │ ├── Art.tsx │ ├── Astronaut.tsx │ ├── Audio.tsx │ ├── Avatar.tsx │ ├── Backpack.tsx │ ├── Base.tsx │ ├── Binders.tsx │ ├── Book.tsx │ ├── Calendar.tsx │ ├── Catalog.tsx │ ├── CityHall.tsx │ ├── Coding.tsx │ ├── Community.tsx │ ├── Companie.tsx │ ├── Compass.tsx │ ├── Conclusion.tsx │ ├── ConnectionLost.tsx │ ├── Contract.tsx │ ├── Culture.tsx │ ├── DataVisualization.tsx │ ├── Datalma.tsx │ ├── DigitalArt.tsx │ ├── Doctor.tsx │ ├── Document.tsx │ ├── DocumentAdd.tsx │ ├── DocumentDownload.tsx │ ├── DocumentSearch.tsx │ ├── DrivingLicense.tsx │ ├── DrivingLicenseNew.tsx │ ├── EarOff.tsx │ ├── Ecosystem.tsx │ ├── Environment.tsx │ ├── Error.tsx │ ├── EyeOff.tsx │ ├── Factory.tsx │ ├── Firefighter.tsx │ ├── FlowList.tsx │ ├── FlowSettings.tsx │ ├── Food.tsx │ ├── Gendarmerie.tsx │ ├── Grocery.tsx │ ├── Health.tsx │ ├── Hospital.tsx │ ├── House.tsx │ ├── HumanCooperation.tsx │ ├── InProgress.tsx │ ├── Information.tsx │ ├── Innovation.tsx │ ├── InternationalDrivingLicense.tsx │ ├── InternationalDrivingLicenseNew.tsx │ ├── Internet.tsx │ ├── JusticeScales.tsx │ ├── Language.tsx │ ├── Leaf.tsx │ ├── LocationFrance.tsx │ ├── LocationOverseasFrance.tsx │ ├── Luggage.tsx │ ├── MainSend.tsx │ ├── Map.tsx │ ├── MapPin.tsx │ ├── MedicalResearch.tsx │ ├── MentalDisabilities.tsx │ ├── Money.tsx │ ├── Moon.tsx │ ├── Mountain.tsx │ ├── NationalIdentityCard.tsx │ ├── NationalIdentityCardPassport.tsx │ ├── NavyAnchor.tsx │ ├── NavyBachi.tsx │ ├── Notification.tsx │ ├── NuclearPlant.tsx │ ├── Paint.tsx │ ├── Passport.tsx │ ├── Pictures.tsx │ ├── Podcast.tsx │ ├── Police.tsx │ ├── PressCard.tsx │ ├── School.tsx │ ├── Search.tsx │ ├── SelfTraining.tsx │ ├── SignDocument.tsx │ ├── Smartphone.tsx │ ├── Success.tsx │ ├── Sun.tsx │ ├── System.tsx │ ├── TaxStamp.tsx │ ├── TechnicalError.tsx │ ├── TravelBack.tsx │ ├── Tree.tsx │ ├── Vaccine.tsx │ ├── VehicleRegistrationDocument.tsx │ ├── Video.tsx │ ├── VideoGames.tsx │ ├── Virus.tsx │ ├── Warning.tsx │ ├── Wheelchair.tsx │ ├── index.d.ts │ ├── index.ts │ ├── pictogrammes-svg │ │ ├── Accessibility.svg │ │ ├── Airport.svg │ │ ├── Application.svg │ │ ├── Archive.svg │ │ ├── ArmyTank.svg │ │ ├── Art.svg │ │ ├── Astronaut.svg │ │ ├── Audio.svg │ │ ├── Avatar.svg │ │ ├── Backpack.svg │ │ ├── Base.svg │ │ ├── Binders.svg │ │ ├── Book.svg │ │ ├── Calendar.svg │ │ ├── Catalog.svg │ │ ├── CityHall.svg │ │ ├── Coding.svg │ │ ├── Community.svg │ │ ├── Companie.svg │ │ ├── Compass.svg │ │ ├── Conclusion.svg │ │ ├── ConnectionLost.svg │ │ ├── Contract.svg │ │ ├── Culture.svg │ │ ├── DataVisualization.svg │ │ ├── Datalma.svg │ │ ├── DigitalArt.svg │ │ ├── Doctor.svg │ │ ├── Document.svg │ │ ├── DocumentAdd.svg │ │ ├── DocumentDownload.svg │ │ ├── DocumentSearch.svg │ │ ├── DrivingLicense.svg │ │ ├── DrivingLicenseNew.svg │ │ ├── EarOff.svg │ │ ├── Ecosystem.svg │ │ ├── Environment.svg │ │ ├── Error.svg │ │ ├── EyeOff.svg │ │ ├── Factory.svg │ │ ├── Firefighter.svg │ │ ├── FlowList.svg │ │ ├── FlowSettings.svg │ │ ├── Food.svg │ │ ├── Gendarmerie.svg │ │ ├── Grocery.svg │ │ ├── Health.svg │ │ ├── Hospital.svg │ │ ├── House.svg │ │ ├── HumanCooperation.svg │ │ ├── InProgress.svg │ │ ├── Information.svg │ │ ├── Innovation.svg │ │ ├── InternationalDrivingLicense.svg │ │ ├── InternationalDrivingLicenseNew.svg │ │ ├── Internet.svg │ │ ├── JusticeScales.svg │ │ ├── Language.svg │ │ ├── Leaf.svg │ │ ├── LocationFrance.svg │ │ ├── LocationOverseasFrance.svg │ │ ├── Luggage.svg │ │ ├── MainSend.svg │ │ ├── Map.svg │ │ ├── MapPin.svg │ │ ├── MedicalResearch.svg │ │ ├── MentalDisabilities.svg │ │ ├── Money.svg │ │ ├── Moon.svg │ │ ├── Mountain.svg │ │ ├── NationalIdentityCard.svg │ │ ├── NationalIdentityCardPassport.svg │ │ ├── NavyAnchor.svg │ │ ├── NavyBachi.svg │ │ ├── Notification.svg │ │ ├── NuclearPlant.svg │ │ ├── Paint.svg │ │ ├── Passport.svg │ │ ├── Pictures.svg │ │ ├── Podcast.svg │ │ ├── Police.svg │ │ ├── PressCard.svg │ │ ├── School.svg │ │ ├── Search.svg │ │ ├── SelfTraining.svg │ │ ├── SignDocument.svg │ │ ├── Smartphone.svg │ │ ├── Success.svg │ │ ├── Sun.svg │ │ ├── System.svg │ │ ├── TaxStamp.svg │ │ ├── TechnicalError.svg │ │ ├── TravelBack.svg │ │ ├── Tree.svg │ │ ├── Vaccine.svg │ │ ├── VehicleRegistrationDocument.svg │ │ ├── Video.svg │ │ ├── VideoGames.svg │ │ ├── Virus.svg │ │ ├── Warning.svg │ │ └── Wheelchair.svg │ └── utils │ │ └── IconWrapper.tsx ├── shared │ └── Fieldset.tsx ├── spa.ts ├── start.ts ├── tools │ ├── Deferred.ts │ ├── JSX.ts │ ├── StatefulObservable │ │ ├── README.md │ │ ├── StatefulObservable.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useObservable.ts │ │ │ └── useRerenderOnChange.ts │ │ └── index.ts │ ├── UnpackProps.ts │ ├── cx.ts │ ├── deepAssign.ts │ ├── deepCopy.ts │ ├── generateValidHtmlId.ts │ ├── getAssetUrl.ts │ ├── getBaseFontSizePx.ts │ ├── isBrowser.ts │ ├── memoize.ts │ ├── observeInputValue.ts │ ├── partition.ts │ ├── powerhooks │ │ ├── useCallbackFactory.ts │ │ ├── useConst.ts │ │ ├── useConstCallback.ts │ │ └── useNamedState.tsx │ ├── signal.ts │ ├── structuredCloneButFunctions.ts │ ├── useAnalyticsId.ts │ └── useWindowInnerSize.ts ├── tsconfig.json ├── tss.ts ├── useBreakpointsValues.ts ├── useBreakpointsValuesPx.ts ├── useColors.ts ├── useIsDark │ ├── client.ts │ ├── constants.ts │ ├── index.ts │ ├── scriptToRunAsap.ts │ └── server.ts └── zz_internal │ └── brandTopAndHomeLinkProps.ts ├── stories ├── Accordion.stories.tsx ├── AgentConnectButton.stories.tsx ├── Alert.stories.tsx ├── Badge.stories.tsx ├── Breadcrumb.stories.tsx ├── Button.stories.tsx ├── ButtonsGroup.stories.tsx ├── CallOut.stories.tsx ├── Card.stories.tsx ├── Checkbox.stories.tsx ├── ColorHelper │ ├── ColorDecisionCard.tsx │ ├── ColorHelper.tsx │ ├── CopyToClipboardButton.tsx │ ├── Search.tsx │ ├── index.ts │ └── tools │ │ └── useQueryParams.ts ├── ConsentManagement.stories.tsx ├── Display.stories.tsx ├── Download.stories.tsx ├── Follow.stories.tsx ├── Footer.stories.tsx ├── FranceConnectButton.stories.tsx ├── Header.stories.tsx ├── Highlight.stories.tsx ├── Input.stories.tsx ├── LanguageSelect.stories.tsx ├── MainNavigation.stories.tsx ├── Modal.stories.tsx ├── MonComptePro.stories.tsx ├── Notice.stories.tsx ├── Pagination.stories.tsx ├── Pictograms.stories.mdx ├── ProConnectButton.stories.tsx ├── Quote.stories.tsx ├── RadioButtons.stories.tsx ├── Range.stories.tsx ├── RotatingLogo.tsx ├── SearchBar.stories.tsx ├── SegmentedControl.stories.tsx ├── Select.stories.tsx ├── SelectNext.stories.tsx ├── SideMenu.stories.tsx ├── SkipLinks.stories.tsx ├── Stepper.stories.tsx ├── Summary.stories.tsx ├── Table.stories.tsx ├── Tabs.stories.tsx ├── Tag.stories.tsx ├── TagsGroup.stories.tsx ├── Tile.stories.tsx ├── ToggleSwitch.stories.tsx ├── ToggleSwitchGroup.stories.tsx ├── Tooltip.stories.tsx ├── Upload.stories.tsx ├── assets │ ├── city-hall.svg │ ├── logo-in.png │ ├── logo-out.png │ ├── placeholder.16x9.png │ └── placeholder.9x16.png ├── blocks │ ├── PasswordInput.stories.tsx │ └── sectionName.ts ├── charts │ ├── BarChart.stories.tsx │ ├── BarLineChart.stories.tsx │ ├── GaugeChart.stories.tsx │ ├── LineChart.stories.tsx │ ├── MultiLineChart.stories.tsx │ ├── PieChart.stories.tsx │ ├── RadarChart.stories.tsx │ ├── ScatterChart.stories.tsx │ └── sectionName.ts ├── colorHelper.stories.mdx ├── getStory.tsx ├── global.d.ts ├── intro.stories.mdx ├── picto │ ├── Pictograms.tsx │ └── Search.tsx ├── sectionName.ts └── utils.css ├── test ├── integration │ ├── README.md │ ├── cra │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ ├── index.html │ │ │ └── robots.txt │ │ ├── src │ │ │ ├── Home.tsx │ │ │ ├── Mui.tsx │ │ │ ├── MyDialog.tsx │ │ │ ├── Picto.tsx │ │ │ ├── index.tsx │ │ │ ├── react-app-env.d.ts │ │ │ └── router.ts │ │ ├── tsconfig.json │ │ └── yarn.lock │ ├── next-appdir │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ │ ├── DefaultTrustedPolicy.tsx │ │ │ ├── Follow.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── StartDsfr.tsx │ │ │ ├── consentManagement.tsx │ │ │ ├── defaultColorScheme.ts │ │ │ ├── dsfr-chart │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── main.module.css │ │ │ ├── mui │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── middleware.ts │ │ ├── next-env.d.ts │ │ ├── next.config.js │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── vercel.svg │ │ ├── tsconfig.json │ │ ├── ui │ │ │ ├── ClientComponent.tsx │ │ │ ├── ClientFooterItem.tsx │ │ │ └── ClientHeaderQuickAccessItem.tsx │ │ └── yarn.lock │ ├── next-pagesdir │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── README.md │ │ ├── next-env.d.ts │ │ ├── next.config.js │ │ ├── package.json │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── index.tsx │ │ │ └── mui.tsx │ │ ├── tsconfig.json │ │ └── yarn.lock │ └── vite │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ └── .keep │ │ ├── src │ │ ├── Home.tsx │ │ ├── Mui.tsx │ │ ├── Picto.tsx │ │ ├── consentManagement.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── vite.config.ts │ │ └── yarn.lock ├── runtime │ ├── README.md │ ├── lib │ │ ├── gdpr.test.ts │ │ └── spacing.test.ts │ └── scripts │ │ ├── breakpoints │ │ ├── generateBreakpointsTsCode.test.ts │ │ └── parseBreakpointsValues.test.ts │ │ ├── classNames │ │ ├── generateClassNamesTsCode.test.ts │ │ └── parseClassNames.test.ts │ │ ├── colorDecisions │ │ ├── generateGetColorDecisionsTsCode.test.ts │ │ ├── parseColorDecisionName.test.ts │ │ └── parseColorDecisions.test.ts │ │ ├── colorOptions │ │ ├── generateGetColorOptionsTsCode.test.ts │ │ ├── getThemePath.test.ts │ │ ├── parseColorOptionName.test.ts │ │ └── parseColorOptions.test.ts │ │ ├── cssVariable.test.ts │ │ ├── icons │ │ ├── generateIconsRawCssCode.test.ts │ │ └── getPatchedRawCssCodeForCompatWithRemixIcon.test.ts │ │ ├── spacing │ │ ├── generateSpacingTsCode.test.ts │ │ └── parseSpacing.test.ts │ │ └── typography │ │ ├── generateTypographyTsCode.test.ts │ │ └── parseTypographyVariants.test.ts └── types │ ├── Checkbox.tsx │ ├── README.md │ ├── RadioButtons.tsx │ ├── Select.tsx │ ├── Tag.tsx │ ├── Tooltip.tsx │ ├── consentManagement.tsx │ ├── i18n.ts │ ├── libBinSync.ts │ └── spacing.ts ├── tsproject.json ├── vitest.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | CHANGELOG.md 4 | .yarn_home/ 5 | /test/integration/ 6 | /storybook-static/ 7 | 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:storybook/recommended"], 6 | "rules": { 7 | "no-extra-boolean-cast": "off", 8 | "@typescript-eslint/explicit-module-boundary-types": "off", 9 | "@typescript-eslint/no-explicit-any": "off", 10 | "@typescript-eslint/no-namespace": "off", 11 | "@typescript-eslint/ban-types": "off", 12 | "@typescript-eslint/ban-ts-comment": "off", 13 | }, 14 | 15 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test/integration/**/* linguist-documentation 2 | .eslintrc.js linguist-documentation 3 | src/bin/copy-dsfr-to-public.ts -linguist-detectable 4 | src/bin/only-include-used-icons.ts -linguist-detectable 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .vscode 40 | .idea 41 | 42 | .DS_Store 43 | 44 | /dist 45 | /.yarn_home 46 | /dsfr 47 | /src/fr/generatedFromCss 48 | /storybook-static 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | /CHANGELOG.md 4 | /.yarn_home/ 5 | /test/integration/ 6 | /dsfr/ 7 | /src/fr/generatedFromCss/ 8 | /storybook-static/ 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "preserve", 8 | "trailingComma": "none", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/customTheme.js: -------------------------------------------------------------------------------- 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 | }); 36 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/*.stories.mdx", 4 | "../stories/*.stories.@(ts|tsx)", 5 | "../stories/blocks/*.stories.@(ts|tsx)", 6 | "../stories/charts/*.stories.@(ts|tsx)", 7 | "../stories/picto/*.stories.@(ts|tsx)", 8 | ], 9 | "addons": [ 10 | "@storybook/addon-links", 11 | "@storybook/addon-essentials", 12 | "storybook-dark-mode", 13 | "@storybook/addon-a11y" 14 | ], 15 | "core": { 16 | "builder": "webpack5" 17 | }, 18 | "staticDirs": ["../dist", "./static"] 19 | }; 20 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react-dsfr components 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.storybook/static/CNAME: -------------------------------------------------------------------------------- 1 | components.react-dsfr.codegouv.studio -------------------------------------------------------------------------------- /.storybook/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/.storybook/static/logo.png -------------------------------------------------------------------------------- /.storybook/static/repo-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/.storybook/static/repo-card.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub user u/garronej 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /patches/@gouvfr+dsfr+1.13.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@gouvfr/dsfr/dist/artwork/dark.svg b/node_modules/@gouvfr/dsfr/dist/artwork/dark.svg 2 | index e4d335c..6c53dce 100644 3 | --- a/node_modules/@gouvfr/dsfr/dist/artwork/dark.svg 4 | +++ b/node_modules/@gouvfr/dsfr/dist/artwork/dark.svg 5 | @@ -12,3 +12,4 @@ 6 | 7 | 8 | 9 | + 10 | diff --git a/node_modules/@gouvfr/dsfr/dist/dsfr.module.js b/node_modules/@gouvfr/dsfr/dist/dsfr.module.js 11 | index 3566bc7..e58a87f 100644 12 | --- a/node_modules/@gouvfr/dsfr/dist/dsfr.module.js 13 | +++ b/node_modules/@gouvfr/dsfr/dist/dsfr.module.js 14 | @@ -6453,9 +6453,11 @@ class HeaderLinks extends api.core.Instance { 15 | case api.Modes.ANGULAR: 16 | case api.Modes.REACT: 17 | case api.Modes.VUE: 18 | + /* 19 | this.warn(`header__tools-links content is different from header__menu-links content. 20 | As you're using a dynamic framework, you should handle duplication of this content yourself, please refer to documentation: 21 | ${api.header.doc}`); 22 | + */ 23 | break; 24 | 25 | default: 26 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | In this directory are the scripts that are run at build time. 2 | We run them directly using `ts-node`. 3 | -------------------------------------------------------------------------------- /scripts/build/cssToTs/colorDecisionAndCorrespondingOptions.ts: -------------------------------------------------------------------------------- 1 | import { parseColorOptions } from "./colorOptions"; 2 | import { parseColorDecisions } from "./colorDecisions"; 3 | import { exclude } from "tsafe/exclude"; 4 | import type { ColorDecision } from "./colorDecisions"; 5 | import type { ColorOption } from "./colorOptions"; 6 | import { symToStr } from "tsafe/symToStr"; 7 | 8 | export type ColorDecisionAndCorrespondingOption = Omit & { 9 | colorOption: ColorOption; 10 | }; 11 | 12 | export function generateColorDecisionAndCorrespondingOptionsTsCode(rawCssCode: string): string { 13 | const colorOptions = parseColorOptions(rawCssCode); 14 | 15 | const colorDecisionAndCorrespondingOptions = parseColorDecisions(rawCssCode) 16 | .map(colorDecision => { 17 | const colorOption = colorOptions.find( 18 | colorOption => 19 | colorOption.themePath.join(".") === colorDecision.optionThemePath.join(".") 20 | ); 21 | return colorOption === undefined ? undefined : ([colorDecision, colorOption] as const); 22 | }) 23 | .filter(exclude(undefined)) 24 | .map( 25 | ([ 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | { optionThemePath, ...colorDecision }, 28 | colorOption 29 | ]): ColorDecisionAndCorrespondingOption => ({ 30 | ...colorDecision, 31 | colorOption 32 | }) 33 | ); 34 | 35 | return [ 36 | ``, 37 | `export const ${symToStr({ colorDecisionAndCorrespondingOptions })}= ${JSON.stringify( 38 | colorDecisionAndCorrespondingOptions, 39 | null, 40 | 4 41 | )} as const;`, 42 | `` 43 | ].join("\n"); 44 | } 45 | -------------------------------------------------------------------------------- /scripts/build/cssToTs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cssToTs"; 2 | -------------------------------------------------------------------------------- /scripts/build/index.ts: -------------------------------------------------------------------------------- 1 | import "./build"; 2 | -------------------------------------------------------------------------------- /scripts/build/main.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @import url(./dsfr/utility/icons/icons.min.css); 3 | @import url(./dsfr/dsfr.css); -------------------------------------------------------------------------------- /scripts/build/parseCss.ts: -------------------------------------------------------------------------------- 1 | import css from "css"; 2 | import memoize from "memoizee"; 3 | 4 | export type ParsedCss = css.Stylesheet; 5 | 6 | export const parseCss = memoize((rawCssCode: string): ParsedCss => css.parse(rawCssCode)); 7 | -------------------------------------------------------------------------------- /scripts/build/patchCssForMui.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import * as css from "css"; 3 | 4 | export function patchCssForMui(params: { rawDsfrCssCode: string }) { 5 | const { rawDsfrCssCode } = params; 6 | 7 | const parsedCss = css.parse(rawDsfrCssCode); 8 | 9 | (function callee(rules: any[], media: string): any[] { 10 | return [ 11 | ...rules.filter(({ type }) => type === "rule").map(rule => [rule, media]), 12 | ...rules 13 | .filter(({ type }) => type === "media") 14 | .map(({ rules, media }) => callee(rules, media)) 15 | .flat() 16 | ]; 17 | })(parsedCss.stylesheet!.rules as any[], "root").forEach( 18 | ([rule, media]) => 19 | (rule.selectors = rule.selectors.map((selector: string) => { 20 | const selectorNotMui = `${selector}:not([class^="Mui"])`; 21 | 22 | if (media === "(hover: hover) and (pointer: fine)") { 23 | if ( 24 | selector === "button:not(:disabled):hover" || 25 | selector === "button:not(:disabled):active" 26 | ) { 27 | return selectorNotMui; 28 | } 29 | } 30 | 31 | return selector; 32 | })) 33 | ); 34 | 35 | return { 36 | "rawDsfrCssCodePatchedForMui": css.stringify(parsedCss, { "compress": false }), 37 | "rawDsfrCssCodePatchedForMuiMinified": css.stringify(parsedCss, { "compress": true }) 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /scripts/tools/cssColorRegexp.ts: -------------------------------------------------------------------------------- 1 | export const cssColorRegexp = 2 | /(#(?:[0-9a-fA-F]{2}){2,4}$|(#[0-9a-fA-F]{3}$)|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d.]+%?\)$|black$|silver$|gray$|whitesmoke$|maroon$|red$|purple$|fuchsia$|green$|lime$|olivedrab$|yellow$|navy$|blue$|teal$|aquamarine$|orange$|aliceblue$|antiquewhite$|aqua$|azure$|beige$|bisque$|blanchedalmond$|blueviolet$|brown$|burlywood$|cadetblue$|chartreuse$|chocolate$|coral$|cornflowerblue$|cornsilk$|crimson$|darkblue$|darkcyan$|darkgoldenrod$|darkgray$|darkgreen$|darkgrey$|darkkhaki$|darkmagenta$|darkolivegreen$|darkorange$|darkorchid$|darkred$|darksalmon$|darkseagreen$|darkslateblue$|darkslategray$|darkslategrey$|darkturquoise$|darkviolet$|deeppink$|deepskyblue$|dimgray$|dimgrey$|dodgerblue$|firebrick$|floralwhite$|forestgreen$|gainsboro$|ghostwhite$|goldenrod$|gold$|greenyellow$|grey$|honeydew$|hotpink$|indianred$|indigo$|ivory$|khaki$|lavenderblush$|lavender$|lawngreen$|lemonchiffon$|lightblue$|lightcoral$|lightcyan$|lightgoldenrodyellow$|lightgray$|lightgreen$|lightgrey$|lightpink$|lightsalmon$|lightseagreen$|lightskyblue$|lightslategray$|lightslategrey$|lightsteelblue$|lightyellow$|limegreen$|linen$|mediumaquamarine$|mediumblue$|mediumorchid$|mediumpurple$|mediumseagreen$|mediumslateblue$|mediumspringgreen$|mediumturquoise$|mediumvioletred$|midnightblue$|mintcream$|mistyrose$|moccasin$|navajowhite$|oldlace$|olive$|orangered$|orchid$|palegoldenrod$|palegreen$|paleturquoise$|palevioletred$|papayawhip$|peachpuff$|peru$|pink$|plum$|powderblue$|rosybrown$|royalblue$|saddlebrown$|salmon$|sandybrown$|seagreen$|seashell$|sienna$|skyblue$|slateblue$|slategray$|slategrey$|snow$|springgreen$|steelblue$|tan$|thistle$|tomato$|transparent$|turquoise$|violet$|wheat$|white$|yellowgreen$|rebeccapurple$)/i; 3 | -------------------------------------------------------------------------------- /scripts/tools/multiReplace.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | export function multiReplace(params: { input: string; keyValues: Record }) { 4 | const { input, keyValues } = params; 5 | 6 | let output = input; 7 | 8 | const keyByHash = new Map(); 9 | 10 | Object.keys(keyValues).forEach(key => { 11 | const hash = crypto.createHash("sha256").update(key).digest("hex"); 12 | 13 | const newOutput = output.replace(new RegExp(key, "g"), hash); 14 | 15 | if (newOutput === output) { 16 | return; 17 | } 18 | 19 | keyByHash.set(hash, key); 20 | 21 | output = newOutput; 22 | }); 23 | 24 | Array.from(keyByHash.entries()).forEach( 25 | ([hash, key]) => (output = output.replace(new RegExp(hash, "g"), keyValues[key])) 26 | ); 27 | 28 | return output; 29 | } 30 | -------------------------------------------------------------------------------- /scripts/tools/threeDigitColorHexToSixDigitsColorHex.ts: -------------------------------------------------------------------------------- 1 | export function threeDigitColorHexToSixDigitsColorHex(threeDigitColorHex: string) { 2 | let v = parseInt(threeDigitColorHex.substring(1), 16); // in rrggbb 3 | // nybble colors - fix to hex colors 4 | // 0x00000rgb -> 0x000r0g0b 5 | // 0x000r0g0b | 0x00r0g0b0 -> 0x00rrggbb 6 | const w = ((v & 0xf00) << 8) | ((v & 0x0f0) << 4) | (v & 0x00f); 7 | v = w | (w << 4); 8 | return `#${v.toString(16)}`; 9 | } 10 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsproject.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ES2020", 6 | "esModuleInterop": true, 7 | "lib": ["DOM", "ES2021"], 8 | "rootDir": "." 9 | }, 10 | "references": [{ "path": "../src/bin" }] 11 | } 12 | -------------------------------------------------------------------------------- /src/Badge.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react"; 2 | import { symToStr } from "tsafe/symToStr"; 3 | import { assert } from "tsafe/assert"; 4 | import type { Equals } from "tsafe"; 5 | import { fr } from "./fr"; 6 | import { cx } from "./tools/cx"; 7 | import type { AlertProps } from "./Alert"; 8 | import { useAnalyticsId } from "./tools/useAnalyticsId"; 9 | 10 | export type BadgeProps = { 11 | id?: string; 12 | className?: string; 13 | style?: CSSProperties; 14 | severity?: AlertProps.Severity | "new"; 15 | small?: boolean; 16 | noIcon?: boolean; 17 | /** Default: "p" */ 18 | as?: "p" | "span"; 19 | children: NonNullable; 20 | }; 21 | 22 | /** @see */ 23 | export const Badge = memo( 24 | forwardRef((props, ref) => { 25 | const { 26 | id: props_id, 27 | className, 28 | as = "p", 29 | style, 30 | severity, 31 | small: isSmall = false, 32 | noIcon = false, 33 | children, 34 | ...rest 35 | } = props; 36 | 37 | assert>(); 38 | 39 | const id = useAnalyticsId({ 40 | "defaultIdPrefix": "fr-badge", 41 | "explicitlyProvidedId": props_id 42 | }); 43 | 44 | return React.createElement( 45 | as, 46 | { 47 | "className": cx( 48 | fr.cx( 49 | "fr-badge", 50 | severity !== undefined && `fr-badge--${severity}`, 51 | { "fr-badge--sm": isSmall }, 52 | { "fr-badge--no-icon": noIcon || severity === undefined } 53 | ), 54 | className 55 | ), 56 | id, 57 | style, 58 | ref, 59 | ...rest 60 | }, 61 | <>{children} 62 | ); 63 | }) 64 | ); 65 | 66 | Badge.displayName = symToStr({ Badge }); 67 | 68 | export default Badge; 69 | -------------------------------------------------------------------------------- /src/Chart/BarChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/BarChart/bar-chart.common"; 5 | import "@gouvfr/dsfr-chart/BarChart/bar-chart.css"; 6 | import { 7 | chartWrapper, 8 | IntrinsicGraphType, 9 | BaseChartProps, 10 | stringifyObjectValue, 11 | MultiChartProps, 12 | ChartLineProps, 13 | IntrinsicGraphLineType 14 | } from "./chartWrapper"; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/BarChart.vue#L79 20 | "bar-chart": { 21 | horizontal?: string; 22 | stacked?: string; 23 | } & IntrinsicGraphType & 24 | IntrinsicGraphLineType; 25 | } 26 | } 27 | } 28 | 29 | export type BarChartBaseProps = { 30 | horizontal?: boolean; 31 | stacked?: boolean; 32 | } & MultiChartProps & 33 | ChartLineProps; 34 | 35 | export type BarChartProps = BarChartBaseProps & BaseChartProps; 36 | 37 | /** @see */ 38 | export const BarChart = chartWrapper((props: BarChartBaseProps) => { 39 | return ; 40 | }, "bar-chart"); 41 | BarChart.displayName = symToStr({ BarChart }); 42 | 43 | export default BarChart; 44 | 45 | // Just so that the icon is included: "fr-icon-arrow-go-back-fill" 46 | -------------------------------------------------------------------------------- /src/Chart/BarLineChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/BarLineChart/barline-chart.common"; 5 | import "@gouvfr/dsfr-chart/BarLineChart/barline-chart.css"; 6 | import { 7 | chartWrapper, 8 | IntrinsicGraphType, 9 | BaseChartProps, 10 | stringifyObjectValue, 11 | ChartProps, 12 | ChartLineProps, 13 | IntrinsicGraphLineType 14 | } from "./chartWrapper"; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/BarLineChart.vue#L75 20 | "bar-line-chart": { 21 | ybar: string; 22 | } & IntrinsicGraphType & 23 | IntrinsicGraphLineType; 24 | } 25 | } 26 | } 27 | 28 | export type BarLineChartBaseProps = { 29 | ybar: number[]; 30 | name?: string; 31 | nameBar?: string; 32 | horizontal?: boolean; 33 | stacked?: boolean; 34 | } & Omit & 35 | ChartLineProps; 36 | 37 | export type BarLineChartProps = BarLineChartBaseProps & BaseChartProps; 38 | 39 | /** @see */ 40 | export const BarLineChart = chartWrapper((props: BarLineChartBaseProps) => { 41 | return ; 42 | }, "bar-line-chart"); 43 | BarLineChart.displayName = symToStr({ BarLineChart }); 44 | 45 | export default BarLineChart; 46 | -------------------------------------------------------------------------------- /src/Chart/GaugeChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/GaugeChart/gauge-chart.common"; 5 | import "@gouvfr/dsfr-chart/GaugeChart/gauge-chart.css"; 6 | import { chartWrapper, BaseChartProps, stringifyObjectValue, ChartColor } from "./chartWrapper"; 7 | 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/GaugeChart.vue#L55 12 | "gauge-chart": { 13 | value: string; 14 | init: string; 15 | target: string; 16 | color: string; 17 | }; 18 | } 19 | } 20 | } 21 | 22 | export type GaugeChartBaseProps = { 23 | value: number; 24 | init: number; 25 | target: number; 26 | color?: ChartColor; 27 | }; 28 | 29 | export type GaugeChartProps = GaugeChartBaseProps & BaseChartProps; 30 | 31 | /** @see */ 32 | export const GaugeChart = chartWrapper( 33 | (props: GaugeChartBaseProps) => , 34 | "gauge-chart" 35 | ); 36 | GaugeChart.displayName = symToStr({ GaugeChart }); 37 | 38 | export default GaugeChart; 39 | -------------------------------------------------------------------------------- /src/Chart/LineChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/LineChart/line-chart.common"; 5 | import "@gouvfr/dsfr-chart/LineChart/line-chart.css"; 6 | import { 7 | chartWrapper, 8 | ChartProps, 9 | IntrinsicGraphType, 10 | BaseChartProps, 11 | stringifyObjectValue, 12 | ChartLineProps, 13 | IntrinsicGraphLineType 14 | } from "./chartWrapper"; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/LineChart.vue#L70 20 | "line-chart": IntrinsicGraphType & IntrinsicGraphLineType; 21 | } 22 | } 23 | } 24 | 25 | export type LineChartBaseProps = ChartProps & ChartLineProps; 26 | 27 | export type LineChartProps = LineChartBaseProps & BaseChartProps; 28 | 29 | /** @see */ 30 | export const LineChart = chartWrapper( 31 | (props: LineChartBaseProps) => , 32 | "line-chart" 33 | ); 34 | LineChart.displayName = symToStr({ LineChart }); 35 | 36 | export default LineChart; 37 | -------------------------------------------------------------------------------- /src/Chart/MultiLineChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/MultiLineChart/multiline-chart.common"; 5 | import "@gouvfr/dsfr-chart/MultiLineChart/multiline-chart.css"; 6 | import { 7 | chartWrapper, 8 | IntrinsicGraphType, 9 | BaseChartProps, 10 | stringifyObjectValue, 11 | MultiChartProps, 12 | ChartLineProps, 13 | IntrinsicGraphLineType 14 | } from "./chartWrapper"; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/MultiLineChart.vue#L74 20 | "multiline-chart": IntrinsicGraphType & IntrinsicGraphLineType; 21 | } 22 | } 23 | } 24 | 25 | export type MultiLineChartBaseProps = MultiChartProps & ChartLineProps; 26 | 27 | export type MultiLineChartProps = MultiLineChartBaseProps & BaseChartProps; 28 | 29 | /** @see */ 30 | export const MultiLineChart = chartWrapper( 31 | (props: MultiLineChartBaseProps) => , 32 | "multiline-chart" 33 | ); 34 | MultiLineChart.displayName = symToStr({ MultiLineChart }); 35 | 36 | export default MultiLineChart; 37 | -------------------------------------------------------------------------------- /src/Chart/PieChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/PieChart/pie-chart.common"; 5 | import "@gouvfr/dsfr-chart/PieChart/pie-chart.css"; 6 | import { 7 | chartWrapper, 8 | type ChartProps, 9 | type IntrinsicGraphType, 10 | type BaseChartProps, 11 | stringifyObjectValue, 12 | type ChartColor 13 | } from "./chartWrapper"; 14 | 15 | declare global { 16 | namespace JSX { 17 | interface IntrinsicElements { 18 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/PieChart.vue#L59 19 | "pie-chart": { 20 | fill?: string; 21 | } & IntrinsicGraphType; 22 | } 23 | } 24 | } 25 | 26 | export type PieChartBaseProps = { 27 | fill?: boolean; 28 | name?: string[]; 29 | color?: ChartColor[]; 30 | } & Omit; 31 | 32 | export type PieChartProps = PieChartBaseProps & BaseChartProps; 33 | 34 | /** @see */ 35 | export const PieChart = chartWrapper( 36 | (props: PieChartBaseProps) => , 37 | "pie-chart" 38 | ); 39 | PieChart.displayName = symToStr({ PieChart }); 40 | 41 | export default PieChart; 42 | -------------------------------------------------------------------------------- /src/Chart/RadarChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/RadarChart/radar-chart.common"; 5 | import "@gouvfr/dsfr-chart/RadarChart/radar-chart.css"; 6 | import { 7 | chartWrapper, 8 | IntrinsicGraphType, 9 | BaseChartProps, 10 | stringifyObjectValue, 11 | MultiChartProps 12 | } from "./chartWrapper"; 13 | 14 | declare global { 15 | namespace JSX { 16 | interface IntrinsicElements { 17 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/RadarChart.vue#L57 18 | "radar-chart": IntrinsicGraphType; 19 | } 20 | } 21 | } 22 | 23 | export type RadarChartBaseProps = MultiChartProps; 24 | 25 | export type RadarChartProps = RadarChartBaseProps & BaseChartProps; 26 | 27 | /** @see */ 28 | export const RadarChart = chartWrapper( 29 | (props: RadarChartBaseProps) => , 30 | "radar-chart" 31 | ); 32 | RadarChart.displayName = symToStr({ RadarChart }); 33 | 34 | export default RadarChart; 35 | -------------------------------------------------------------------------------- /src/Chart/ScatterChart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import "@gouvfr/dsfr-chart/ScatterChart/scatter-chart.common"; 5 | import "@gouvfr/dsfr-chart/ScatterChart/scatter-chart.css"; 6 | import { 7 | chartWrapper, 8 | BaseChartProps, 9 | MultiChartProps, 10 | IntrinsicGraphType, 11 | stringifyObjectValue, 12 | ChartLineProps, 13 | IntrinsicGraphLineType 14 | } from "./chartWrapper"; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | // https://github.com/GouvernementFR/dsfr-chart/blob/v1.0.0/src/components/ScatterChart.vue#L75 20 | "scatter-chart": { 21 | showLine?: string; 22 | } & IntrinsicGraphType & 23 | IntrinsicGraphLineType; 24 | } 25 | } 26 | } 27 | 28 | type ScatterChartBaseProps = { 29 | x: number[][]; 30 | showLine?: boolean; 31 | } & Omit & 32 | ChartLineProps; 33 | 34 | export type ScatterChartProps = ScatterChartBaseProps & BaseChartProps; 35 | 36 | /** @see */ 37 | export const ScatterChart = chartWrapper( 38 | (props: ScatterChartBaseProps) => , 39 | "scatter-chart" 40 | ); 41 | ScatterChart.displayName = symToStr({ ScatterChart }); 42 | 43 | export default ScatterChart; 44 | -------------------------------------------------------------------------------- /src/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef } from "react"; 2 | import { symToStr } from "tsafe/symToStr"; 3 | import { Fieldset, type FieldsetProps } from "./shared/Fieldset"; 4 | 5 | export type CheckboxProps = Omit; 6 | 7 | /** @see */ 8 | export const Checkbox = memo( 9 | forwardRef((props, ref) => ( 10 |
11 | )) 12 | ); 13 | 14 | Checkbox.displayName = symToStr({ Checkbox }); 15 | 16 | export default Checkbox; 17 | -------------------------------------------------------------------------------- /src/Display/Artwork/Artwork.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { ArtworkGov } from "./ArtworkGov"; 5 | import { useIsGov } from "../../mui/useIsGov"; 6 | import { useArtworkWhiteLabel } from "./ArtworkWhiteLabel/useArtworkWhiteLabel"; 7 | import { assert } from "tsafe/assert"; 8 | 9 | export function Artwork(props: { theme: "light" | "dark" | "system"; className?: string }) { 10 | const { theme, className } = props; 11 | 12 | const { isGov } = useIsGov(); 13 | const { ArtworkWhiteLabel } = useArtworkWhiteLabel(); 14 | 15 | if (!isGov) { 16 | assert(ArtworkWhiteLabel !== undefined); 17 | 18 | return ; 19 | } 20 | 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /src/Display/Artwork/ArtworkGov.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fr } from "../../fr"; 3 | import ArtworkLightSvg from "../../dsfr/artwork/light.svg"; 4 | import ArtworkDarkSvg from "../../dsfr/artwork/dark.svg"; 5 | import ArtworkSystemSvg from "../../dsfr/artwork/system.svg"; 6 | import { getAssetUrl } from "../../tools/getAssetUrl"; 7 | 8 | export function ArtworkGov(props: { theme: "light" | "dark" | "system"; className?: string }) { 9 | const { theme, className } = props; 10 | 11 | return ( 12 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/Display/Artwork/ArtworkWhiteLabel/DisplayArtworkWhiteLabelProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import { context } from "./context"; 3 | import { ArtworkWhiteLabel } from "./ArtworkWhiteLabel"; 4 | 5 | export function DisplayArtworkWhiteLabelProvider(props: { children: ReactNode }) { 6 | const { children } = props; 7 | 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/Display/Artwork/ArtworkWhiteLabel/context.ts: -------------------------------------------------------------------------------- 1 | import { JSX } from "../../../tools/JSX"; 2 | import { createContext } from "react"; 3 | 4 | export const context = createContext< 5 | ((props: { theme: "light" | "dark" | "system"; sizePx: number }) => JSX.Element) | undefined 6 | >(undefined); 7 | -------------------------------------------------------------------------------- /src/Display/Artwork/ArtworkWhiteLabel/useArtworkWhiteLabel.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { context } from "./context"; 3 | 4 | export function useArtworkWhiteLabel() { 5 | const ArtworkWhiteLabel = useContext(context); 6 | 7 | return { ArtworkWhiteLabel }; 8 | } 9 | -------------------------------------------------------------------------------- /src/Display/Artwork/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Artwork"; 2 | -------------------------------------------------------------------------------- /src/Display/index.ts: -------------------------------------------------------------------------------- 1 | export { headerFooterDisplayItem, addDisplayTranslations } from "./Display"; 2 | 3 | /** 4 | * @deprecated: It's no longer needed to manually mount this component. 5 | * @see: https://components.react-dsfr.codegouv.studio/?path=/docs/components-display 6 | */ 7 | export function Display() { 8 | return null; 9 | } 10 | -------------------------------------------------------------------------------- /src/Download.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef, type CSSProperties, type ReactNode } from "react"; 2 | import { symToStr } from "tsafe/symToStr"; 3 | import { assert } from "tsafe/assert"; 4 | import type { Equals } from "tsafe"; 5 | import { fr } from "./fr"; 6 | import { cx } from "./tools/cx"; 7 | import { getLink, type RegisteredLinkProps } from "./link"; 8 | import { useAnalyticsId } from "./tools/useAnalyticsId"; 9 | 10 | export type DownloadProps = { 11 | id?: string; 12 | className?: string; 13 | style?: CSSProperties; 14 | details: ReactNode; 15 | label: ReactNode; 16 | linkProps: RegisteredLinkProps; 17 | classes?: Partial>; 18 | }; 19 | 20 | /** @see */ 21 | export const Download = memo( 22 | forwardRef((props, ref) => { 23 | const { 24 | className, 25 | style, 26 | details, 27 | label, 28 | linkProps, 29 | classes = {}, 30 | id: props_id, 31 | ...rest 32 | } = props; 33 | 34 | assert>(); 35 | 36 | const id = useAnalyticsId({ 37 | "defaultIdPrefix": "fr-download", 38 | "explicitlyProvidedId": props_id 39 | }); 40 | 41 | const { Link } = getLink(); 42 | 43 | return ( 44 |
50 |

51 | 56 | {label} 57 | {details} 58 | 59 |

60 |
61 | ); 62 | }) 63 | ); 64 | 65 | Download.displayName = symToStr({ Download }); 66 | 67 | export default Download; 68 | -------------------------------------------------------------------------------- /src/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Header"; 2 | export { 3 | Header, 4 | type HeaderProps, 5 | HeaderQuickAccessItem, 6 | type HeaderQuickAccessItemProps, 7 | addHeaderTranslations, 8 | useTranslation 9 | } from "./Header"; 10 | -------------------------------------------------------------------------------- /src/Header/useIsHeaderMenuModalOpen.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { headerMenuModalIdPrefix } from "./Header"; 3 | import { useIsModalOpen } from "../Modal/useIsModalOpen"; 4 | import { symToStr } from "tsafe/symToStr"; 5 | 6 | export function useIsHeaderMenuModalOpen() { 7 | const [headerMenuModalId, setHeaderMenuModalId] = useState(""); 8 | 9 | useEffect(() => { 10 | const matchingElements = document.querySelectorAll(`[id^='${headerMenuModalIdPrefix}']`); 11 | 12 | if (matchingElements.length > 1) { 13 | throw new Error( 14 | `There is more than one Header mounted on the page, you can't use ${symToStr({ 15 | useIsHeaderMenuModalOpen 16 | })}` 17 | ); 18 | } 19 | 20 | if (matchingElements.length === 0) { 21 | throw new Error( 22 | `The header is not mounted on the page, you can't use ${symToStr({ 23 | useIsHeaderMenuModalOpen 24 | })}` 25 | ); 26 | } 27 | 28 | setHeaderMenuModalId(matchingElements[0].id); 29 | }, []); 30 | 31 | return useIsModalOpen({ 32 | "id": headerMenuModalId, 33 | "isOpenedByDefault": false 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/Highlight.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react"; 2 | import { symToStr } from "tsafe/symToStr"; 3 | import { assert } from "tsafe/assert"; 4 | import type { Equals } from "tsafe"; 5 | import { fr } from "./fr"; 6 | import { cx } from "./tools/cx"; 7 | import { useAnalyticsId } from "./tools/useAnalyticsId"; 8 | 9 | export type HighlightProps = { 10 | id?: string; 11 | className?: string; 12 | classes?: Partial>; 13 | size?: HighlightProps.Size; 14 | style?: CSSProperties; 15 | children: NonNullable; 16 | }; 17 | 18 | export namespace HighlightProps { 19 | export type Size = "sm" | "lg"; 20 | } 21 | 22 | /** @see */ 23 | export const Highlight = memo( 24 | forwardRef((props, ref) => { 25 | const { className, classes = {}, style, children, size, id: id_props, ...rest } = props; 26 | 27 | assert>(); 28 | 29 | const id = useAnalyticsId({ 30 | "defaultIdPrefix": "fr-highlight", 31 | "explicitlyProvidedId": id_props 32 | }); 33 | 34 | return ( 35 |
42 |

43 | {children} 44 |

45 |
46 | ); 47 | }) 48 | ); 49 | 50 | Highlight.displayName = symToStr({ Highlight }); 51 | 52 | export default Highlight; 53 | -------------------------------------------------------------------------------- /src/MainNavigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MainNavigation"; 2 | export { addMegaMenuTranslations } from "./MegaMenu"; 3 | 4 | import { MainNavigation } from "./MainNavigation"; 5 | 6 | export default MainNavigation; 7 | -------------------------------------------------------------------------------- /src/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Modal"; 2 | -------------------------------------------------------------------------------- /src/RadioButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef } from "react"; 2 | import { symToStr } from "tsafe/symToStr"; 3 | import { Fieldset, type FieldsetProps } from "./shared/Fieldset"; 4 | 5 | export type RadioButtonsProps = Omit; 6 | 7 | /** @see */ 8 | export const RadioButtons = memo( 9 | forwardRef((props, ref) => ( 10 |
11 | )) 12 | ); 13 | 14 | RadioButtons.displayName = symToStr({ RadioButtons }); 15 | 16 | export default RadioButtons; 17 | -------------------------------------------------------------------------------- /src/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchBar"; 2 | export { SearchBar, type SearchBarProps, addSearchBarTranslations } from "./SearchBar"; 3 | -------------------------------------------------------------------------------- /src/TagsGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, forwardRef, memo } from "react"; 2 | import { assert, Equals } from "tsafe"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | import Tag, { TagProps } from "./Tag"; 5 | import { useAnalyticsId } from "./tools/useAnalyticsId"; 6 | import { cx } from "./tools/cx"; 7 | import { fr } from "./fr"; 8 | 9 | export type TagsGroupProps = TagsGroupProps.Common; 10 | 11 | //https://main--ds-gouv.netlify.app/example/component/tag/#:~:text=Groupe%20de%20tags 12 | export namespace TagsGroupProps { 13 | export type Common = { 14 | id?: string; 15 | className?: string; 16 | style?: CSSProperties; 17 | /** @default false */ 18 | smallTags?: TagProps["small"]; 19 | /** 6 tags should be the maximum. */ 20 | tags: [TagProps, ...TagProps[]]; 21 | }; 22 | } 23 | 24 | export const TagsGroup = memo( 25 | forwardRef((props, ref) => { 26 | const { id: props_id, className, tags, smallTags = false, style, ...rest } = props; 27 | 28 | assert>(); 29 | 30 | const id = useAnalyticsId({ 31 | "defaultIdPrefix": "fr-tags-group", 32 | "explicitlyProvidedId": props_id 33 | }); 34 | 35 | const tagsGroupClassName = cx( 36 | fr.cx("fr-tags-group", smallTags && "fr-tags-group--sm"), 37 | className 38 | ); 39 | 40 | return ( 41 |
    42 | {tags.map((tagProps, i) => ( 43 |
  • 44 | 45 |
  • 46 | ))} 47 |
48 | ); 49 | }) 50 | ); 51 | 52 | TagsGroup.displayName = symToStr({ TagsGroup }); 53 | 54 | export default TagsGroup; 55 | -------------------------------------------------------------------------------- /src/assets/agentconnect.css: -------------------------------------------------------------------------------- 1 | 2 | .agentconnect-button__link.agentconnect-button__link { 3 | width: 206px; 4 | height: 60px; 5 | display: inline-block; 6 | background-image: url("./agentconnect-btn-principal.svg"); 7 | --active-tint: unset; 8 | background-color: transparent; 9 | background-position: 0 0; 10 | background-repeat: no-repeat; 11 | background-size: contain; 12 | } 13 | 14 | .agentconnect-button__preload-hover { 15 | visibility: collapse; 16 | } 17 | .agentconnect-button__link:hover, .agentconnect-button__preload-hover { 18 | background-image: url("./agentconnect-btn-principal-hover.svg"); 19 | } 20 | 21 | :root[data-fr-theme=dark] .agentconnect-button__link { 22 | background-image: url("./agentconnect-btn-alternatif.svg"); 23 | } 24 | 25 | :root[data-fr-theme=dark] .agentconnect-button__link:hover, 26 | :root[data-fr-theme=dark] .agentconnect-button__preload-hover { 27 | background-image: url("./agentconnect-btn-alternatif-hover.svg"); 28 | } 29 | 30 | .agentconnect-button__hint { 31 | color: var(--text-action-high-blue-france); 32 | } -------------------------------------------------------------------------------- /src/assets/blank-favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/dsfr_plus_icons.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('../dsfr/utility/icons/icons.min.css'); 3 | @import url('../dsfr/dsfr.min.css'); -------------------------------------------------------------------------------- /src/assets/dsfr_plus_icons.scss: -------------------------------------------------------------------------------- 1 | @use '../dsfr/utility/icons/icons.min.css'; 2 | @use '../dsfr/dsfr.min.css'; -------------------------------------------------------------------------------- /src/assets/language-select.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .language-select .short-label { 5 | text-transform: uppercase 6 | } 7 | .language-select .fr-menu { 8 | right: 0; 9 | } 10 | 11 | .language-select .fr-translate { 12 | display: inline-flex;; 13 | } -------------------------------------------------------------------------------- /src/assets/search-bar.css: -------------------------------------------------------------------------------- 1 | 2 | .fr-search-bar { 3 | position: relative; 4 | } 5 | 6 | .fr-search-bar--lg input { 7 | height: 3rem; 8 | } -------------------------------------------------------------------------------- /src/bin/README.md: -------------------------------------------------------------------------------- 1 | Here are the scripts exposed as utility to the user of `react-dsfr` 2 | -------------------------------------------------------------------------------- /src/bin/react-dsfr.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const [, , commandName, ...args] = process.argv; 4 | 5 | (async () => { 6 | switch (commandName) { 7 | case "update-icons": 8 | { 9 | const { main } = await import("./only-include-used-icons"); 10 | 11 | await main(args); 12 | } 13 | break; 14 | case "copy-static-assets": 15 | { 16 | const { main } = await import("./copy-dsfr-to-public"); 17 | 18 | await main(args); 19 | } 20 | break; 21 | default: 22 | console.error(`Unknown command ${commandName}`); 23 | process.exit(-1); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /src/bin/tools/crawlSync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { join as pathJoin, relative as pathRelative } from "path"; 3 | 4 | const crawlSyncRec = (dirPath: string, filePaths: string[]) => { 5 | for (const basename of fs.readdirSync(dirPath)) { 6 | const fileOrDirPath = pathJoin(dirPath, basename); 7 | 8 | if (fs.lstatSync(fileOrDirPath).isDirectory()) { 9 | crawlSyncRec(fileOrDirPath, filePaths); 10 | 11 | continue; 12 | } 13 | 14 | filePaths.push(fileOrDirPath); 15 | } 16 | }; 17 | 18 | /** List all files in a given directory return paths relative to the dir_path */ 19 | export function crawlSync(params: { 20 | dirPath: string; 21 | returnedPathsType: "absolute" | "relative to dirPath"; 22 | }): string[] { 23 | const { dirPath, returnedPathsType } = params; 24 | 25 | const filePaths: string[] = []; 26 | 27 | crawlSyncRec(dirPath, filePaths); 28 | 29 | switch (returnedPathsType) { 30 | case "absolute": 31 | return filePaths; 32 | case "relative to dirPath": 33 | return filePaths.map(filePath => pathRelative(dirPath, filePath)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/bin/tools/fnv1aHashToHex.ts: -------------------------------------------------------------------------------- 1 | export function fnv1aHashToHex(str: string) { 2 | let hash = 2166136261; 3 | for (let i = 0; i < str.length; i++) { 4 | hash ^= str.charCodeAt(i); 5 | hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); 6 | } 7 | return (hash >>> 0).toString(16); // Convert to unsigned 32-bit integer and then to hexadecimal 8 | } 9 | -------------------------------------------------------------------------------- /src/bin/tools/fs.existsAsync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | 3 | export async function existsAsync(path: string) { 4 | try { 5 | await fs.stat(path); 6 | return true; 7 | } catch (error) { 8 | if ((error as Error & { code: string }).code === "ENOENT") return false; 9 | throw error; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/bin/tools/getAbsoluteAndInOsFormatPath.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin } from "path"; 2 | 3 | export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: string }): string { 4 | const { pathIsh, cwd } = params; 5 | 6 | let pathOut = pathIsh; 7 | 8 | pathOut = pathOut.replace(/\//g, pathSep); 9 | 10 | pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut; 11 | 12 | if (!pathIsAbsolute(pathOut)) { 13 | pathOut = pathJoin(cwd, pathOut); 14 | } 15 | 16 | return pathOut; 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/tools/getProjectRoot.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | function getProjectRootRec(dirPath: string): string { 5 | if (fs.existsSync(path.join(dirPath, "package.json"))) { 6 | return dirPath; 7 | } 8 | return getProjectRootRec(path.join(dirPath, "..")); 9 | } 10 | 11 | let result: string | undefined = undefined; 12 | 13 | export function getProjectRoot(): string { 14 | if (result !== undefined) { 15 | return result; 16 | } 17 | 18 | return (result = getProjectRootRec(__dirname)); 19 | } 20 | -------------------------------------------------------------------------------- /src/bin/tools/modifyHtmlHrefs.ts: -------------------------------------------------------------------------------- 1 | export function modifyHtmlHrefs(params: { 2 | html: string; 3 | getModifiedHref: (href: string) => string; 4 | }): { modifiedHtml: string } { 5 | const { html, getModifiedHref } = params; 6 | 7 | let modifiedHtml = html; 8 | 9 | ( 10 | [ 11 | [/href="([^"]+)"/g, '"'], 12 | [/href='([^']+)'/g, "'"] 13 | ] as const 14 | ).forEach( 15 | ([regex, quoteSymbol]) => 16 | (modifiedHtml = modifiedHtml.replace( 17 | regex, 18 | (...[, href]) => `href=${quoteSymbol}${getModifiedHref(href)}${quoteSymbol}` 19 | )) 20 | ); 21 | 22 | return { modifiedHtml }; 23 | } 24 | -------------------------------------------------------------------------------- /src/bin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsproject.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ES5", 6 | "esModuleInterop": true, 7 | "lib": ["DOM", "ES2021"], 8 | "outDir": "../../dist/bin", 9 | "rootDir": "." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/consentManagement/ConsentBannerAndConsentManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConsentBannerAndConsentManagement"; 2 | export { addConsentManagementTranslations } from "./translation"; 3 | -------------------------------------------------------------------------------- /src/consentManagement/index.ts: -------------------------------------------------------------------------------- 1 | export { createConsentManagement } from "./createConsentManagement"; 2 | export { addConsentManagementTranslations } from "./ConsentBannerAndConsentManagement"; 3 | -------------------------------------------------------------------------------- /src/consentManagement/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type { ReactNode } from "react"; 3 | 4 | //See: test/types/consentManagement.ts to understand theses types 5 | 6 | export type FinalityConsent = { 7 | readonly [K in Finality as K extends `${infer _P}.${infer _C}` ? never : K]: boolean; 8 | } & { 9 | readonly [K in Finality as K extends `${infer P}.${infer _C}` ? P : never]: Readonly< 10 | Record 11 | > & { readonly isFullConsent: boolean }; 12 | } & { readonly isFullConsent: boolean }; 13 | 14 | export type ExtractFinalityFromFinalityDescription< 15 | FinalityDescription extends Record< 16 | string, 17 | { title: ReactNode; subFinalities?: Record } 18 | > 19 | > = { 20 | [K in keyof FinalityDescription]: K extends string 21 | ? FinalityDescription[K] extends { subFinalities: Record } 22 | ? `${K}.${ExtractFinalityFromFinalityDescription.SubFinalities}` 23 | : K 24 | : never; 25 | }[keyof FinalityDescription]; 26 | 27 | export namespace ExtractFinalityFromFinalityDescription { 28 | export type SubFinalities = T extends { subFinalities: infer U } 29 | ? U extends Record 30 | ? keyof U 31 | : never 32 | : never; 33 | } 34 | -------------------------------------------------------------------------------- /src/consentManagement/useConsent.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from "../tools/isBrowser"; 2 | import { useConstCallback } from "../tools/powerhooks/useConstCallback"; 3 | import type { FinalityConsent } from "./types"; 4 | import type { ConsentCallback, ProcessConsentChanges } from "./processConsentChanges"; 5 | 6 | export type UseConsent = (params?: { 7 | consentCallback: ConsentCallback; 8 | }) => { 9 | finalityConsent: FinalityConsent | undefined; 10 | assumeConsent: (finality: Finality) => void; 11 | }; 12 | 13 | export function createUseConsent(params: { 14 | useFinalityConsent: () => FinalityConsent | undefined; 15 | processConsentChanges: ProcessConsentChanges; 16 | useConsentCallback: (params: { 17 | consentCallback: ConsentCallback | undefined; 18 | }) => void; 19 | }): { useConsent: UseConsent } { 20 | const { useFinalityConsent, processConsentChanges, useConsentCallback } = params; 21 | 22 | const useConsentManagementClientSide: UseConsent = params => { 23 | const { consentCallback } = params ?? {}; 24 | 25 | useConsentCallback({ consentCallback }); 26 | 27 | const finalityConsent = useFinalityConsent(); 28 | 29 | const assumeConsent = useConstCallback((finality: Finality) => 30 | processConsentChanges({ 31 | "type": "atomic change", 32 | finality, 33 | "isConsentGiven": true 34 | }) 35 | ); 36 | 37 | return { 38 | assumeConsent, 39 | finalityConsent 40 | }; 41 | }; 42 | 43 | const useConsentManagementServerSide: UseConsent = () => { 44 | return { 45 | "finalityConsent": undefined, 46 | "assumeConsent": () => { 47 | throw new Error("Cannot assume consent on the server side"); 48 | } 49 | }; 50 | }; 51 | 52 | const useConsent = isBrowser ? useConsentManagementClientSide : useConsentManagementServerSide; 53 | 54 | return { useConsent }; 55 | } 56 | -------------------------------------------------------------------------------- /src/fr/colors.ts: -------------------------------------------------------------------------------- 1 | import { colorOptions, type ColorOptions } from "./generatedFromCss/colorOptions"; 2 | import { getColorOptionsHex } from "./generatedFromCss/getColorOptionsHex"; 3 | import { colorDecisions, type ColorDecisions } from "./generatedFromCss/colorDecisions"; 4 | import { getColorDecisionsHex } from "./generatedFromCss/getColorDecisionsHex"; 5 | 6 | export type Colors = { 7 | options: ColorOptions<"css var">; 8 | decisions: ColorDecisions<"css var">; 9 | getHex: (params: { isDark: boolean }) => { 10 | options: ColorOptions<"hex">; 11 | decisions: ColorDecisions<"hex">; 12 | }; 13 | }; 14 | 15 | export const colors: Colors = { 16 | "options": colorOptions, 17 | "decisions": colorDecisions, 18 | "getHex": (() => { 19 | const getHex: Colors["getHex"] = ({ isDark }) => { 20 | const options = getColorOptionsHex({ isDark }); 21 | 22 | const decisions = getColorDecisionsHex({ "colorOptions": options }); 23 | 24 | return { 25 | options, 26 | decisions 27 | }; 28 | }; 29 | 30 | const cache: Record<"light" | "dark", ReturnType | undefined> = { 31 | "light": undefined, 32 | "dark": undefined 33 | }; 34 | 35 | return ({ isDark }) => (cache[isDark ? "dark" : "light"] ??= getHex({ isDark })); 36 | })() 37 | }; 38 | -------------------------------------------------------------------------------- /src/fr/cx.ts: -------------------------------------------------------------------------------- 1 | import type { FrClassName } from "./generatedFromCss/classNames"; 2 | import { cx as genericCx } from "../tools/cx"; 3 | 4 | export type FrCxArg = 5 | | undefined 6 | | null 7 | | FrClassName 8 | | boolean 9 | | Partial> 10 | | readonly FrCxArg[]; 11 | 12 | /** Copy pasted from 13 | * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 14 | **/ 15 | export const cx: (...args: FrCxArg[]) => string = genericCx; 16 | -------------------------------------------------------------------------------- /src/fr/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./colors"; 2 | export type { BreakpointKeys } from "./breakpoints"; 3 | import { breakpoints } from "./breakpoints"; 4 | import { spacing } from "./spacing"; 5 | export type { SpacingToken } from "./spacing"; 6 | import { cx } from "./cx"; 7 | export type { FrCxArg } from "./cx"; 8 | export type { Colors } from "./colors"; 9 | export type { ColorOptions } from "./generatedFromCss/colorOptions"; 10 | export type { ColorDecisions } from "./generatedFromCss/colorDecisions"; 11 | import { colors } from "./colors"; 12 | export type { FrClassName, FrIconClassName, RiIconClassName } from "./generatedFromCss/classNames"; 13 | import { typography } from "./generatedFromCss/typography"; 14 | 15 | export const fr = { 16 | breakpoints, 17 | spacing, 18 | cx, 19 | colors, 20 | typography 21 | }; 22 | -------------------------------------------------------------------------------- /src/fr/shadows.ts: -------------------------------------------------------------------------------- 1 | export const shadowsOption = [ 2 | "none", 3 | /** Light / SM */ 4 | "0px 2px 6px 0px rgba(0,0,18,0.16)", 5 | /** Light / MD */ 6 | "0px 4px 12px 0px rgba(0,0,18,0.16)", 7 | /** Light / LG */ 8 | "0px 6px 18px 0px rgba(0,0,18,0.16)", 9 | /** Dark / SM */ 10 | "0px 2px 6px 0px rgba(0,0,18,0.32)", 11 | /** Dark / MD */ 12 | "0px 4px 12px 0px rgba(0,0,18,0.32)", 13 | /** Dark / LG */ 14 | "0px 4px 18px 0px rgba(0,0,18,0.32)" 15 | ] as const; 16 | 17 | // Shadows decisions (shadow applied to an usecase or a context) 18 | 19 | /** Raised */ 20 | // Use Light or Dark / SM 21 | 22 | /** Overlap */ 23 | // Use Light or Dark / MD 24 | 25 | /** Lifted */ 26 | // Use Light or Dark / LG 27 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const _default: string | { src: string }; 3 | export default _default; 4 | } 5 | 6 | declare module "*.png" { 7 | const _default: string | { src: string }; 8 | export default _default; 9 | } 10 | 11 | declare module "*.ico" { 12 | const _default: string | { src: string }; 13 | export default _default; 14 | } 15 | 16 | declare module "*.webmanifest" { 17 | const _default: string; 18 | export default _default; 19 | } 20 | 21 | declare module "*.woff2" { 22 | const _default: string; 23 | export default _default; 24 | } 25 | -------------------------------------------------------------------------------- /src/mui/index.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export * from "./mui"; 4 | import { MuiDsfrThemeProvider } from "./mui"; 5 | export { useIsGov } from "./useIsGov"; 6 | export default MuiDsfrThemeProvider; 7 | -------------------------------------------------------------------------------- /src/mui/useIsGov.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useContext, type ReactNode } from "react"; 4 | 5 | const react = createContext(true); 6 | 7 | export function useIsGov() { 8 | const isGov = useContext(react); 9 | 10 | return { isGov }; 11 | } 12 | 13 | export function IsGovProvider(props: { children: ReactNode; isGov: boolean }) { 14 | const { children, isGov } = props; 15 | 16 | return {children}; 17 | } 18 | -------------------------------------------------------------------------------- /src/next-app-router/getHtmlAttributes.tsx: -------------------------------------------------------------------------------- 1 | import { data_fr_scheme, data_fr_theme } from "../useIsDark/constants"; 2 | import type { ColorScheme } from "../useIsDark"; 3 | import { 4 | type DefaultColorScheme, 5 | setDefaultColorSchemeServerSide 6 | } from "./zz_internal/defaultColorScheme"; 7 | import { setUseLang } from "../i18n"; 8 | 9 | const suppressHydrationWarning = true; 10 | 11 | export function createGetHtmlAttributes(params: { defaultColorScheme: DefaultColorScheme }) { 12 | const { defaultColorScheme } = params; 13 | 14 | function getHtmlAttributes(params: { 15 | lang: string | undefined; 16 | }): { suppressHydrationWarning: true; lang?: string } & ( 17 | | Record 18 | | {} 19 | ) { 20 | const { lang } = params; 21 | 22 | setDefaultColorSchemeServerSide({ defaultColorScheme }); 23 | 24 | if (lang !== undefined) { 25 | setUseLang({ useLang: () => lang }); 26 | } 27 | 28 | if (defaultColorScheme === "system") { 29 | return { 30 | lang, 31 | suppressHydrationWarning 32 | }; 33 | } 34 | 35 | return { 36 | lang, 37 | suppressHydrationWarning, 38 | [data_fr_scheme]: defaultColorScheme, 39 | [data_fr_theme]: defaultColorScheme 40 | }; 41 | } 42 | return { getHtmlAttributes }; 43 | } 44 | -------------------------------------------------------------------------------- /src/next-app-router/index.ts: -------------------------------------------------------------------------------- 1 | export type { RegisterLink } from "../link"; 2 | export type { DefaultColorScheme } from "./zz_internal/defaultColorScheme"; 3 | export { DsfrProviderBase, type DsfrProviderProps, StartDsfrOnHydration } from "./DsfrProvider"; 4 | -------------------------------------------------------------------------------- /src/next-app-router/server-only-index.ts: -------------------------------------------------------------------------------- 1 | export { createGetHtmlAttributes } from "./getHtmlAttributes"; 2 | export { DsfrHeadBase, type DsfrHeadProps } from "./DsfrHead"; 3 | -------------------------------------------------------------------------------- /src/next-app-router/zz_internal/defaultColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import type { ColorScheme } from "../../useIsDark"; 3 | 4 | export type DefaultColorScheme = ColorScheme | "system"; 5 | 6 | let defaultColorSchemeServerSide: DefaultColorScheme | undefined = undefined; 7 | 8 | export function getDefaultColorSchemeServerSide(): DefaultColorScheme { 9 | assert(defaultColorSchemeServerSide !== undefined); 10 | return defaultColorSchemeServerSide; 11 | } 12 | 13 | export function setDefaultColorSchemeServerSide(params: { 14 | defaultColorScheme: DefaultColorScheme; 15 | }): void { 16 | const { defaultColorScheme } = params; 17 | 18 | defaultColorSchemeServerSide = defaultColorScheme; 19 | } 20 | -------------------------------------------------------------------------------- /src/next-app-router/zz_internal/fontUrlByFileBasename.ts: -------------------------------------------------------------------------------- 1 | import marianneLightWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Light.woff2"; 2 | import marianneItalicWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Light_Italic.woff2"; 3 | import marianneRegularWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Regular.woff2"; 4 | import marianneRegularItalicWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Regular_Italic.woff2"; 5 | import marianneMediumWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Medium.woff2"; 6 | import marianneMediumItalicWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Medium_Italic.woff2"; 7 | import marianneBoldWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Bold.woff2"; 8 | import marianneBoldItalicWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Marianne-Bold_Italic.woff2"; 9 | import spectralRegularWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Spectral-Regular.woff2"; 10 | import spectralExtraBoldWoff2Url from "@codegouvfr/react-dsfr/dsfr/fonts/Spectral-ExtraBold.woff2"; 11 | 12 | export const fontUrlByFileBasename = { 13 | "Marianne-Light": marianneLightWoff2Url, 14 | "Marianne-Light_Italic": marianneItalicWoff2Url, 15 | "Marianne-Regular": marianneRegularWoff2Url, 16 | "Marianne-Regular_Italic": marianneRegularItalicWoff2Url, 17 | "Marianne-Medium": marianneMediumWoff2Url, 18 | "Marianne-Medium_Italic": marianneMediumItalicWoff2Url, 19 | "Marianne-Bold": marianneBoldWoff2Url, 20 | "Marianne-Bold_Italic": marianneBoldItalicWoff2Url, 21 | "Spectral-Regular": spectralRegularWoff2Url, 22 | "Spectral-ExtraBold": spectralExtraBoldWoff2Url 23 | } as const; 24 | -------------------------------------------------------------------------------- /src/picto/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createIcon } from "./utils/IconWrapper"; 3 | 4 | export default createIcon( 5 | <> 6 | 10 | 14 | 18 | 22 | , 23 | "Error" 24 | ); 25 | -------------------------------------------------------------------------------- /src/picto/Success.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createIcon } from "./utils/IconWrapper"; 3 | 4 | export default createIcon( 5 | <> 6 | 12 | 16 | 20 | 24 | , 25 | "Success" 26 | ); 27 | -------------------------------------------------------------------------------- /src/picto/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createIcon } from "./utils/IconWrapper"; 3 | 4 | export default createIcon( 5 | <> 6 | 10 | 14 | 18 | 22 | 26 | , 27 | "Warning" 28 | ); 29 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Datalma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Error.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Moon.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Success.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/picto/pictogrammes-svg/Warning.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/picto/utils/IconWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | type IconSize = "small" | "medium" | "large" | "inherit" | (string & {}); 4 | export type IconProps = { 5 | fontSize?: IconSize; 6 | } & Omit, "fontSize">; 7 | 8 | const getSize = (size: IconSize) => { 9 | switch (size) { 10 | case "small": 11 | return "1.25em"; 12 | case "medium": 13 | return "1.5em"; 14 | case "large": 15 | return "2.5em"; 16 | case "inherit": 17 | return "inherit"; 18 | default: 19 | return size; 20 | } 21 | }; 22 | 23 | export const IconWrapper: React.FC = memo( 24 | ({ children, fontSize = "medium", ...props }) => ( 25 | 38 | ) 39 | ); 40 | 41 | export function createIcon(SvgPath: JSX.Element, displayName: string) { 42 | const IconComponent: React.FC = props => ( 43 | {SvgPath} 44 | ); 45 | 46 | IconComponent.displayName = displayName; 47 | return IconComponent; 48 | } 49 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from "./tools/isBrowser"; 2 | import { assert } from "tsafe/assert"; 3 | import type { ColorScheme } from "./useIsDark"; 4 | import { startClientSideIsDarkLogic } from "./useIsDark/client"; 5 | import { Deferred } from "./tools/Deferred"; 6 | 7 | type Params = { 8 | defaultColorScheme: ColorScheme | "system"; 9 | verbose: boolean; 10 | nextParams: 11 | | { 12 | doPersistDarkModePreferenceWithCookie: boolean; 13 | registerEffectAction: (effect: () => void) => void; 14 | } 15 | | undefined; 16 | doCheckNonce: boolean; 17 | trustedTypesPolicyName: string; 18 | }; 19 | 20 | let isStarted = false; 21 | 22 | export async function start(params: Params) { 23 | const { defaultColorScheme, verbose, nextParams, doCheckNonce, trustedTypesPolicyName } = 24 | params; 25 | 26 | assert(isBrowser); 27 | 28 | if (isStarted) { 29 | return; 30 | } 31 | 32 | isStarted = true; 33 | 34 | const registerEffectAction: (action: () => void) => void = 35 | nextParams === undefined ? action => action() : nextParams.registerEffectAction; 36 | 37 | startClientSideIsDarkLogic({ 38 | "colorSchemeExplicitlyProvidedAsParameter": defaultColorScheme, 39 | "doPersistDarkModePreferenceWithCookie": 40 | nextParams === undefined ? false : nextParams.doPersistDarkModePreferenceWithCookie, 41 | registerEffectAction, 42 | doCheckNonce, 43 | trustedTypesPolicyName 44 | }); 45 | 46 | // @ts-expect-error 47 | window.dsfr = { 48 | verbose, 49 | "mode": "react" 50 | }; 51 | 52 | // @ts-expect-error 53 | await import("./dsfr/dsfr.module"); 54 | 55 | dDsfrLoaded.resolve(); 56 | 57 | registerEffectAction(() => { 58 | // @ts-expect-error 59 | window.dsfr.start(); 60 | }); 61 | } 62 | 63 | const dDsfrLoaded = new Deferred(); 64 | 65 | export const prDsfrLoaded = dDsfrLoaded.pr; 66 | -------------------------------------------------------------------------------- /src/tools/Deferred.ts: -------------------------------------------------------------------------------- 1 | import { overwriteReadonlyProp } from "tsafe/lab/overwriteReadonlyProp"; 2 | 3 | export class Deferred { 4 | public readonly pr: Promise; 5 | 6 | /** NOTE: Does not need to be called bound to instance*/ 7 | public readonly resolve: (value: T) => void; 8 | public readonly reject: (error: any) => void; 9 | 10 | constructor() { 11 | let resolve!: (value: T) => void; 12 | let reject!: (error: any) => void; 13 | 14 | this.pr = new Promise((resolve_, reject_) => { 15 | resolve = value => { 16 | overwriteReadonlyProp(this, "isPending", false); 17 | resolve_(value); 18 | }; 19 | 20 | reject = error => { 21 | overwriteReadonlyProp(this, "isPending", false); 22 | reject_(error); 23 | }; 24 | }); 25 | 26 | this.resolve = resolve; 27 | this.reject = reject; 28 | } 29 | 30 | public readonly isPending: boolean = true; 31 | } 32 | 33 | export namespace Deferred { 34 | export type Unpack> = T extends Deferred ? U : never; 35 | } 36 | 37 | export class VoidDeferred extends Deferred { 38 | public declare readonly resolve: () => void; 39 | } 40 | -------------------------------------------------------------------------------- /src/tools/JSX.ts: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from "react"; 2 | 3 | export namespace JSX { 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface Element extends ReactElement {} 6 | } 7 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/README.md: -------------------------------------------------------------------------------- 1 | `StatefulObservable` is a construct that allow to avoid having to depend on [EVT](https://evt.land). 2 | 3 | A `StatefulObservable` can be used to implement a signal: Wire a non react-land function to a react component. 4 | Example: 5 | 6 | ```tsx 7 | import { createStatefulObservable, useRerenderOnChange } from "tools/StatefulObservable"; 8 | 9 | const $counter = createStatefulObservable(() => 0); 10 | 11 | export function incrementCounter() { 12 | $counter.current++; 13 | } 14 | 15 | export function Counter() { 16 | useRerenderOnChange($counter); 17 | 18 | const counter = $counter.current; 19 | 20 | return Counter: {counter}; 21 | } 22 | ``` 23 | 24 | WARNING: Unlike `StatefulEvt`, `StatefulObservable` do not post when we first attach. 25 | If the current value was not yet evaluated `next()` is called on the initial value returned by the function that 26 | returns it. 27 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/StatefulObservable.ts: -------------------------------------------------------------------------------- 1 | import { assert, is } from "tsafe/assert"; 2 | 3 | export type StatefulObservable = { 4 | current: T; 5 | subscribe: (next: (data: T) => void) => Subscription; 6 | }; 7 | 8 | export type Subscription = { 9 | unsubscribe(): void; 10 | }; 11 | 12 | export function createStatefulObservable(getInitialValue: () => T): StatefulObservable { 13 | const nextFunctions: ((data: T) => void)[] = []; 14 | 15 | const { get, set } = (() => { 16 | let wrappedState: [T] | undefined = undefined; 17 | 18 | function set(data: T) { 19 | wrappedState = [data]; 20 | 21 | nextFunctions.forEach(next => next(data)); 22 | } 23 | 24 | return { 25 | "get": () => { 26 | if (wrappedState === undefined) { 27 | set(getInitialValue()); 28 | assert(!is(wrappedState)); 29 | } 30 | return wrappedState[0]; 31 | }, 32 | set 33 | }; 34 | })(); 35 | 36 | return Object.defineProperty( 37 | { 38 | "current": null as any as T, 39 | "subscribe": (next: (data: T) => void) => { 40 | nextFunctions.push(next); 41 | 42 | return { 43 | "unsubscribe": () => nextFunctions.splice(nextFunctions.indexOf(next), 1) 44 | }; 45 | } 46 | }, 47 | "current", 48 | { 49 | "enumerable": true, 50 | get, 51 | set 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useObservable"; 2 | export * from "./useRerenderOnChange"; 3 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/hooks/useObservable.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import type { Subscription } from "../StatefulObservable"; 3 | 4 | /** 5 | * Equivalent of https://docs.evt.land/api/react-hooks 6 | */ 7 | export function useObservable( 8 | effect: (params: { registerSubscription: (subscription: Subscription) => void }) => void, 9 | deps: React.DependencyList 10 | ): void { 11 | useEffect(() => { 12 | const subscriptions: Subscription[] = []; 13 | 14 | effect({ 15 | "registerSubscription": subscription => subscriptions.push(subscription) 16 | }); 17 | 18 | return () => { 19 | subscriptions.forEach(subscription => subscription.unsubscribe()); 20 | subscriptions.length = 0; 21 | }; 22 | }, deps); 23 | } 24 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/hooks/useRerenderOnChange.ts: -------------------------------------------------------------------------------- 1 | import { useObservable } from "./useObservable"; 2 | import { useState } from "react"; 3 | import type { StatefulObservable } from "../StatefulObservable"; 4 | 5 | /** 6 | * Equivalent of https://docs.evt.land/api/react-hooks 7 | * */ 8 | export function useRerenderOnChange($: StatefulObservable): void { 9 | //NOTE: We use function in case the state is a function 10 | const [, setCurrent] = useState(() => $.current); 11 | 12 | useObservable( 13 | ({ registerSubscription }) => { 14 | const subscription = $.subscribe(current => setCurrent(() => current)); 15 | registerSubscription(subscription); 16 | }, 17 | [$] 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/tools/StatefulObservable/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./StatefulObservable"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /src/tools/UnpackProps.ts: -------------------------------------------------------------------------------- 1 | export type UnpackProps = T extends React.ComponentType ? P : never; 2 | -------------------------------------------------------------------------------- /src/tools/cx.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import { typeGuard } from "tsafe/typeGuard"; 3 | 4 | export type CxArg = 5 | | undefined 6 | | null 7 | | string 8 | | boolean 9 | | Partial> 10 | | readonly CxArg[]; 11 | 12 | export const cx = (...args: CxArg[]): string => { 13 | const len = args.length; 14 | let i = 0; 15 | let cls = ""; 16 | for (; i < len; i++) { 17 | const arg = args[i]; 18 | if (arg == null) continue; 19 | 20 | let toAdd; 21 | switch (typeof arg) { 22 | case "boolean": 23 | break; 24 | case "object": { 25 | if (Array.isArray(arg)) { 26 | toAdd = cx(...arg); 27 | } else { 28 | assert(!typeGuard<{ length: number }>(arg, false)); 29 | 30 | toAdd = ""; 31 | for (const k in arg) { 32 | if (arg[k as string] && k) { 33 | toAdd && (toAdd += " "); 34 | toAdd += k; 35 | } 36 | } 37 | } 38 | break; 39 | } 40 | default: { 41 | toAdd = arg; 42 | } 43 | } 44 | if (toAdd) { 45 | cls && (cls += " "); 46 | cls += toAdd; 47 | } 48 | } 49 | return cls; 50 | }; 51 | -------------------------------------------------------------------------------- /src/tools/deepAssign.ts: -------------------------------------------------------------------------------- 1 | import { assert, is } from "tsafe/assert"; 2 | import { structuredCloneButFunctions } from "./structuredCloneButFunctions"; 3 | 4 | /** NOTE: Array a copied over, not merged. */ 5 | export function deepAssign(params: { 6 | target: Record; 7 | source: Record; 8 | }): void { 9 | const { target, source } = params; 10 | 11 | Object.keys(source).forEach(key => { 12 | const dereferencedSource = source[key]; 13 | 14 | if (dereferencedSource === undefined) { 15 | delete target[key]; 16 | return; 17 | } 18 | 19 | if (dereferencedSource instanceof Date) { 20 | assign({ 21 | target, 22 | key, 23 | value: new Date(dereferencedSource.getTime()) 24 | }); 25 | 26 | return; 27 | } 28 | 29 | if (dereferencedSource instanceof Array) { 30 | assign({ 31 | target, 32 | key, 33 | value: structuredCloneButFunctions(dereferencedSource) 34 | }); 35 | 36 | return; 37 | } 38 | 39 | if (dereferencedSource instanceof Function || !(dereferencedSource instanceof Object)) { 40 | assign({ 41 | target, 42 | key, 43 | value: dereferencedSource 44 | }); 45 | 46 | return; 47 | } 48 | 49 | if (!(target[key] instanceof Object)) { 50 | target[key] = {}; 51 | } 52 | 53 | const dereferencedTarget = target[key]; 54 | 55 | assert(is>(dereferencedTarget)); 56 | assert(is>(dereferencedSource)); 57 | 58 | deepAssign({ 59 | target: dereferencedTarget, 60 | source: dereferencedSource 61 | }); 62 | }); 63 | } 64 | 65 | function assign(params: { target: Record; key: string; value: unknown }): void { 66 | const { target, key, value } = params; 67 | 68 | Object.defineProperty(target, key, { 69 | enumerable: true, 70 | writable: true, 71 | configurable: true, 72 | value 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/tools/deepCopy.ts: -------------------------------------------------------------------------------- 1 | /** Assert obj is serializable */ 2 | export function deepCopy(obj: T): T { 3 | return JSON.parse(JSON.stringify(obj)); 4 | } 5 | -------------------------------------------------------------------------------- /src/tools/generateValidHtmlId.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | export function generateValidHtmlId(params: { 4 | text: ReactNode; 5 | fallback?: string | number; 6 | }): string { 7 | const { text, fallback } = params; 8 | 9 | if (typeof text !== "string" || text === "") { 10 | return fallback !== undefined ? `-${fallback}` : ""; 11 | } 12 | 13 | // Remove any space characters 14 | let result = text.replace(/\s+/g, ""); 15 | 16 | // Replace any non-alphanumeric characters with underscores 17 | result = result.replace(/[^a-zA-Z0-9]/g, "_"); 18 | 19 | return `-${result}`; 20 | } 21 | -------------------------------------------------------------------------------- /src/tools/getAssetUrl.ts: -------------------------------------------------------------------------------- 1 | export function getAssetUrl(componentOrUrl: { src: string } | string): string { 2 | return typeof componentOrUrl === "string" ? componentOrUrl : componentOrUrl.src; 3 | } 4 | -------------------------------------------------------------------------------- /src/tools/getBaseFontSizePx.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import { isBrowser } from "./isBrowser"; 3 | 4 | export function getBaseFontSizePx(): number { 5 | if (!isBrowser) { 6 | return 16; 7 | } 8 | 9 | const htmlElement = document.querySelector("html"); 10 | 11 | assert(htmlElement !== null); 12 | 13 | const computedStyle = window.getComputedStyle(htmlElement); 14 | const fontSize = computedStyle.getPropertyValue("font-size"); 15 | 16 | const fontSizeInPixels = parseFloat(fontSize); 17 | 18 | return fontSizeInPixels; 19 | } 20 | -------------------------------------------------------------------------------- /src/tools/isBrowser.ts: -------------------------------------------------------------------------------- 1 | export const isBrowser = typeof window === "object" && typeof document === "object"; 2 | -------------------------------------------------------------------------------- /src/tools/memoize.ts: -------------------------------------------------------------------------------- 1 | type SimpleType = number | string | boolean | null | undefined; 2 | type FuncWithSimpleParams = (...args: T) => R; 3 | 4 | export function memoize( 5 | fn: FuncWithSimpleParams, 6 | options?: { 7 | argsLength?: number; 8 | max?: number; 9 | } 10 | ): FuncWithSimpleParams { 11 | const cache = new Map>>(); 12 | 13 | const { argsLength = fn.length, max = Infinity } = options ?? {}; 14 | 15 | return ((...args: Parameters>) => { 16 | const key = JSON.stringify( 17 | args 18 | .slice(0, argsLength) 19 | .map(v => { 20 | if (v === null) { 21 | return "null"; 22 | } 23 | if (v === undefined) { 24 | return "undefined"; 25 | } 26 | switch (typeof v) { 27 | case "number": 28 | return `number-${v}`; 29 | case "string": 30 | return `string-${v}`; 31 | case "boolean": 32 | return `boolean-${v ? "true" : "false"}`; 33 | } 34 | }) 35 | .join("-sIs9sAslOdeWlEdIos3-") 36 | ); 37 | 38 | if (cache.has(key)) { 39 | return cache.get(key); 40 | } 41 | 42 | if (max === cache.size) { 43 | for (const key of cache.keys()) { 44 | cache.delete(key); 45 | break; 46 | } 47 | } 48 | 49 | const value = fn(...args); 50 | 51 | cache.set(key, value); 52 | 53 | return value; 54 | }) as any; 55 | } 56 | -------------------------------------------------------------------------------- /src/tools/partition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an array of elements split into two groups, the first of which contains elements 3 | * `predicate` returns truthy for, the second of which contains elements `predicate` returns 4 | * falsy for. The predicate is invoked with one argument: (value). 5 | */ 6 | export function partition(arr: T[], predicate: (value: T) => boolean): [T[], T[]] { 7 | return [arr.filter(value => predicate(value)), arr.filter(item => !predicate(item))]; 8 | } 9 | -------------------------------------------------------------------------------- /src/tools/powerhooks/useCallbackFactory.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { id } from "tsafe/id"; 3 | import { memoize } from "../memoize"; 4 | 5 | export type CallbackFactory = ( 6 | ...factoryArgs: FactoryArgs 7 | ) => (...args: Args) => R; 8 | 9 | /** 10 | * https://docs.powerhooks.dev/api-reference/usecallbackfactory 11 | * 12 | * const callbackFactory= useCallbackFactory( 13 | * ([key]: [string], [params]: [{ foo: number; }]) => { 14 | * ... 15 | * }, 16 | * [] 17 | * ); 18 | * 19 | * WARNING: Factory args should not be of variable length. 20 | * 21 | */ 22 | export function useCallbackFactory< 23 | FactoryArgs extends (string | number | boolean)[], 24 | Args extends unknown[], 25 | R = void 26 | >(callback: (...callbackArgs: [FactoryArgs, Args]) => R): CallbackFactory { 27 | type Out = CallbackFactory; 28 | 29 | const callbackRef = useRef(callback); 30 | 31 | callbackRef.current = callback; 32 | 33 | const memoizedRef = useRef(undefined); 34 | 35 | return useState(() => 36 | id((...factoryArgs) => { 37 | if (memoizedRef.current === undefined) { 38 | memoizedRef.current = memoize( 39 | (...factoryArgs: FactoryArgs) => 40 | (...args: Args) => 41 | callbackRef.current(factoryArgs, args), 42 | { "argsLength": factoryArgs.length } 43 | ); 44 | } 45 | 46 | return memoizedRef.current(...factoryArgs); 47 | }) 48 | )[0]; 49 | } 50 | -------------------------------------------------------------------------------- /src/tools/powerhooks/useConst.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | /** 4 | * Compute a value on first render and never again, 5 | * Equivalent of const [x] = useState(()=> ...) 6 | */ 7 | export function useConst(getValue: () => T): T { 8 | const [value] = useState(getValue); 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /src/tools/powerhooks/useConstCallback.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { Parameters } from "tsafe/Parameters"; 3 | 4 | /** https://stackoverflow.com/questions/65890278/why-cant-usecallback-always-return-the-same-ref */ 5 | export function useConstCallback unknown) | undefined | null>( 6 | callback: NonNullable 7 | ): T { 8 | const callbackRef = useRef(null as any); 9 | 10 | callbackRef.current = callback; 11 | 12 | return useState( 13 | () => 14 | (...args: Parameters) => 15 | callbackRef.current(...args) 16 | )[0] as T; 17 | } 18 | -------------------------------------------------------------------------------- /src/tools/powerhooks/useNamedState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { capitalize } from "tsafe/capitalize"; 3 | import type { Dispatch, SetStateAction } from "react"; 4 | 5 | export type UseNamedStateReturnType = Record & 6 | Record<`set${Capitalize}`, Dispatch>>; 7 | 8 | export function useNamedState( 9 | name: Name, 10 | initialState: T | (() => T) 11 | ): UseNamedStateReturnType { 12 | const [state, setState] = useState(initialState); 13 | 14 | return { 15 | [name]: state, 16 | [`set${capitalize(name)}`]: setState 17 | } as any; 18 | } 19 | -------------------------------------------------------------------------------- /src/tools/signal.ts: -------------------------------------------------------------------------------- 1 | import { useConstCallback } from "./powerhooks/useConstCallback"; 2 | import { overwriteReadonlyProp } from "tsafe/lab/overwriteReadonlyProp"; 3 | import type { UseNamedStateReturnType } from "./powerhooks/useNamedState"; 4 | import { typeGuard } from "tsafe/typeGuard"; 5 | import { capitalize } from "tsafe/capitalize"; 6 | import { 7 | createStatefulObservable, 8 | useRerenderOnChange, 9 | type StatefulObservable 10 | } from "./StatefulObservable"; 11 | 12 | export function createSignal(params: { 13 | name: Name; 14 | initialValue: T; 15 | }): Record<`use${Capitalize}`, () => UseNamedStateReturnType> & 16 | Record<`$${Name}`, StatefulObservable> { 17 | const { name, initialValue } = params; 18 | 19 | const $xyz = createStatefulObservable(() => initialValue); 20 | 21 | function useXyz() { 22 | useRerenderOnChange($xyz); 23 | 24 | return { 25 | [name]: $xyz.current, 26 | [`set${capitalize(name)}`]: useConstCallback( 27 | (setStateAction: T | ((prevState: T) => T)) => 28 | ($xyz.current = typeGuard<(prevState: T) => T>( 29 | setStateAction, 30 | typeof setStateAction === "function" 31 | ) 32 | ? setStateAction($xyz.current) 33 | : setStateAction) 34 | ) 35 | } as any; 36 | } 37 | 38 | overwriteReadonlyProp(useXyz as any, "name", `use${capitalize(name)}`); 39 | 40 | return { 41 | [useXyz.name]: useXyz, 42 | [`$${name}`]: $xyz 43 | } as any; 44 | } 45 | -------------------------------------------------------------------------------- /src/tools/structuredCloneButFunctions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Functionally equivalent to structuredClone but 3 | * functions are not cloned but kept as is. 4 | * (as opposed to structuredClone that chokes if it encounters a function) 5 | */ 6 | export function structuredCloneButFunctions( 7 | o: T, 8 | replacer?: (params: { key: string; value: unknown }) => unknown 9 | ): T { 10 | if (!(o instanceof Object)) { 11 | return o; 12 | } 13 | 14 | if (typeof o === "function") { 15 | return o; 16 | } 17 | 18 | if (o instanceof Array) { 19 | return o.map(o => structuredCloneButFunctions(o, replacer)) as any; 20 | } 21 | 22 | return Object.fromEntries( 23 | Object.entries(o).map(([key, value]) => [ 24 | key, 25 | structuredCloneButFunctions( 26 | replacer === undefined ? value : replacer({ key, value }), 27 | replacer 28 | ) 29 | ]) 30 | ) as any; 31 | } 32 | -------------------------------------------------------------------------------- /src/tools/useAnalyticsId.ts: -------------------------------------------------------------------------------- 1 | import { useId } from "react"; 2 | 3 | /** 4 | * Eulerian analytics requires every element to have a unique ID. 5 | * This hook help generate such an ID in the case they are not explicitly provided. 6 | */ 7 | export function useAnalyticsId(params: { explicitlyProvidedId?: string; defaultIdPrefix: string }) { 8 | const { explicitlyProvidedId, defaultIdPrefix } = params; 9 | 10 | const id = useId(); 11 | 12 | return explicitlyProvidedId ?? `${defaultIdPrefix}-${id}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/tools/useWindowInnerSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from "react"; 2 | 3 | export function useWindowInnerSize() { 4 | const [, triggerRerender] = useReducer(() => ({}), {}); 5 | 6 | useEffect(() => { 7 | const handleResize = () => triggerRerender(); 8 | 9 | window.addEventListener("resize", handleResize); 10 | 11 | return () => window.removeEventListener("resize", handleResize); 12 | }, []); 13 | 14 | return { 15 | "windowInnerWidth": window.innerWidth, 16 | "windowInnerHeight": window.innerHeight 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsproject.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/", 5 | "rootDir": ".", 6 | "module": "ES2020", 7 | "target": "ES2017", 8 | "lib": ["es2015", "DOM", "ES2019.Object"], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "exclude": ["./bin"] 14 | } 15 | -------------------------------------------------------------------------------- /src/tss.ts: -------------------------------------------------------------------------------- 1 | import { useColors } from "./useColors"; 2 | import { createMakeAndWithStyles } from "tss-react"; 3 | 4 | /** @deprecated: Please use import { makeStyles } from "tss-react/dsfr"; instead. */ 5 | export const { makeStyles, withStyles, useStyles } = createMakeAndWithStyles({ 6 | "useTheme": useColors 7 | }); 8 | -------------------------------------------------------------------------------- /src/useBreakpointsValues.ts: -------------------------------------------------------------------------------- 1 | import { type BreakpointKeys, type BreakpointsValues } from "./fr/breakpoints"; 2 | import { useBreakpointsValuesPx } from "./useBreakpointsValuesPx"; 3 | 4 | export type { BreakpointKeys, BreakpointsValues }; 5 | 6 | /** @deprecated Use import { useBreakpointsValuesPx } from "@codegouvfr/react-dsfr/useBreakpointsValuesPx"; instead */ 7 | export const useBreakpointsValues = useBreakpointsValuesPx; 8 | -------------------------------------------------------------------------------- /src/useBreakpointsValuesPx.ts: -------------------------------------------------------------------------------- 1 | import { useReducer, useEffect, useMemo } from "react"; 2 | import { assert } from "tsafe/assert"; 3 | import { breakpoints, type BreakpointKeys, type BreakpointsValues } from "./fr/breakpoints"; 4 | 5 | export type { BreakpointKeys, BreakpointsValues }; 6 | 7 | /** Return the breakpoint values in px, the values ger refreshed 8 | * when the base font size change. */ 9 | export function useBreakpointsValuesPx() { 10 | const [breakpointsValuesDependency, triggerRefresh] = useReducer(() => ({}), {}); 11 | 12 | useEffect(() => { 13 | const htmlElement = document.querySelector("html"); 14 | 15 | assert(htmlElement !== null); 16 | 17 | // Create a new MutationObserver to detect changes in the base font size 18 | const observer = new MutationObserver(mutationsList => { 19 | if ( 20 | mutationsList.find( 21 | mutation => 22 | mutation.target === htmlElement && 23 | mutation.attributeName === "style" && 24 | (mutation.oldValue ?? "").indexOf("font-size") !== -1 25 | ) !== undefined 26 | ) { 27 | triggerRefresh(); 28 | } 29 | }); 30 | 31 | // Observe changes to the style attribute of the html element 32 | observer.observe(htmlElement, { 33 | "attributes": true, 34 | "attributeOldValue": true, 35 | "attributeFilter": ["style"] 36 | }); 37 | 38 | return () => { 39 | observer.disconnect(); 40 | }; 41 | }, []); 42 | 43 | const breakpointsValues = useMemo( 44 | () => breakpoints.getPxValues(), 45 | [breakpointsValuesDependency] 46 | ); 47 | 48 | return { breakpointsValues }; 49 | } 50 | -------------------------------------------------------------------------------- /src/useColors.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useIsDark } from "./useIsDark"; 4 | import { fr, type ColorOptions, type ColorDecisions } from "./fr"; 5 | 6 | export type ColorTheme = { 7 | isDark: boolean; 8 | options: ColorOptions<"css var">; 9 | decisions: ColorDecisions<"css var">; 10 | }; 11 | 12 | /** 13 | * A hook is no longer required to get the colors, this will soon be deprecated 14 | * when the documentation is updated to reflect the new way of getting the colors. 15 | * 16 | * Before you would do: 17 | * ```ts 18 | * import { useColors } from "@codegouvfr/react-dsfr/useColors"; 19 | * // ... 20 | * const theme = useColors(); 21 | * // ... 22 | * theme.decisions.background.default.grey.default 23 | * ``` 24 | * Now you should do: 25 | * ```ts 26 | * import { fr } from "@codegouvfr/react-dsfr"; 27 | * // ... 28 | * fr.colors.decisions.background.default.grey.default 29 | * ``` 30 | * We don't need a hook anymore as the the colors are expressed as CSS variables and thus don't need to be 31 | * switched at runtime when the user changes the dark mode. 32 | * 33 | * If however you need the colors in the HEX format you can do: 34 | * 35 | * ```ts 36 | * import { fr } from "@codegouvfr/react-dsfr"; 37 | * import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark"; 38 | * // ... 39 | * const { isDark } = useIsDark(); 40 | * const theme = fr.colors.getHex({ isDark }); 41 | * // ... 42 | * theme.decisions.background.default.grey.default 43 | * ``` 44 | **/ 45 | export function useColors(): ColorTheme { 46 | const { isDark } = useIsDark(); 47 | 48 | return { 49 | isDark, 50 | "options": fr.colors.options, 51 | "decisions": fr.colors.decisions 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/useIsDark/constants.ts: -------------------------------------------------------------------------------- 1 | export const data_fr_theme = "data-fr-theme"; 2 | export const data_fr_scheme = "data-fr-scheme"; 3 | 4 | export const rootColorSchemeStyleTagId = "dsfr-root-color-scheme"; 5 | -------------------------------------------------------------------------------- /src/useIsDark/index.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import { isBrowser } from "../tools/isBrowser"; 3 | import { useIsDarkServerSide } from "./server"; 4 | import { useIsDarkClientSide, getIsDarkClientSide } from "./client"; 5 | export type { ColorScheme } from "./client"; 6 | 7 | export const useIsDark = isBrowser ? useIsDarkClientSide : useIsDarkServerSide; 8 | 9 | export const getIsDark = () => { 10 | assert(isBrowser, "getIsDark can only be used on the client side"); 11 | 12 | return getIsDarkClientSide(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/useIsDark/server.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext } from "react"; 4 | import type { UseIsDark } from "./client"; 5 | import { useConstCallback } from "../tools/powerhooks/useConstCallback"; 6 | import { assert } from "tsafe/assert"; 7 | 8 | const ssrIsDarkContext = createContext(undefined); 9 | 10 | export const { Provider: SsrIsDarkProvider } = ssrIsDarkContext; 11 | 12 | export const useIsDarkServerSide: UseIsDark = () => { 13 | const setIsDark = useConstCallback(() => { 14 | /* nothing */ 15 | }); 16 | 17 | const isDark = useContext(ssrIsDarkContext); 18 | 19 | assert(isDark !== undefined, "Not within provider"); 20 | 21 | return { 22 | isDark, 23 | setIsDark 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/zz_internal/brandTopAndHomeLinkProps.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import type { RegisteredLinkProps } from "../link"; 3 | 4 | let wrap: 5 | | { 6 | brandTop: ReactNode; 7 | homeLinkProps: RegisteredLinkProps & { title: string }; 8 | } 9 | | undefined = undefined; 10 | 11 | export function setBrandTopAndHomeLinkProps(params: { 12 | brandTop: ReactNode; 13 | homeLinkProps: RegisteredLinkProps & { title: string }; 14 | }) { 15 | wrap = params; 16 | } 17 | 18 | export function getBrandTopAndHomeLinkProps() { 19 | return wrap; 20 | } 21 | -------------------------------------------------------------------------------- /stories/AgentConnectButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { AgentConnectButton } from "../dist/AgentConnectButton"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory, logCallbacks } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { AgentConnectButton }, 8 | "description": ` 9 | - [See AgentConnect documentation](https://github.com/france-connect/Documentation-AgentConnect/blob/main/doc_fs/implementation_fca/bouton_fca.md) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/AgentConnectButton.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | "url": "https://example.com" 17 | }); 18 | 19 | export const Centered = getStory({ 20 | "style": { 21 | "textAlign": "center" 22 | }, 23 | "url": "https://example.com" 24 | }); 25 | 26 | export const WithOnClick = getStory({ 27 | ...logCallbacks(["onClick"]) 28 | }); 29 | -------------------------------------------------------------------------------- /stories/Breadcrumb.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from "../dist/Breadcrumb"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { Breadcrumb }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/fil-d-ariane) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Breadcrumb.tsx)`, 11 | "disabledProps": ["lang"] 12 | }); 13 | 14 | export default meta; 15 | 16 | export const Default = getStory({ 17 | "homeLinkProps": { "href": "/" }, 18 | "segments": [ 19 | { 20 | "label": "Segment 1", 21 | "linkProps": { 22 | "href": "/segment-1" 23 | } 24 | }, 25 | { 26 | "label": "Segment 2", 27 | "linkProps": { 28 | "href": "/segment-1/segment-2" 29 | } 30 | }, 31 | { 32 | "label": "Segment 3", 33 | "linkProps": { 34 | "href": "/segment-1/segment-2/segment-3" 35 | } 36 | }, 37 | { 38 | "label": "Segment 4", 39 | "linkProps": { 40 | "href": "/segment-1/segment-2/segment-3/segment-4" 41 | } 42 | } 43 | ], 44 | "currentPageLabel": "Page Actuelle" 45 | }); 46 | -------------------------------------------------------------------------------- /stories/ColorHelper/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ColorHelper"; 2 | -------------------------------------------------------------------------------- /stories/ColorHelper/tools/useQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const getQuery = () => { 4 | if (typeof window !== "undefined") { 5 | return new URLSearchParams(window.location.search); 6 | } 7 | return new URLSearchParams(); 8 | }; 9 | 10 | const getQueryStringVal = (key: string): string | null => { 11 | return getQuery().get(key); 12 | }; 13 | 14 | export const useQueryParam = (key: string, defaultVal: string): [string, (val: string) => void] => { 15 | const [query, setQuery] = useState(getQueryStringVal(key) || defaultVal); 16 | 17 | const updateUrl = (newVal: string) => { 18 | setQuery(newVal); 19 | 20 | const query = getQuery(); 21 | 22 | if (newVal.trim() !== "") { 23 | query.set(key, newVal); 24 | } else { 25 | query.delete(key); 26 | } 27 | 28 | // This check is necessary if using the hook with Gatsby 29 | if (typeof window !== "undefined") { 30 | const { protocol, pathname, host } = window.location; 31 | const newUrl = `${protocol}//${host}${pathname}?${query.toString()}`; 32 | window.history.pushState({}, "", newUrl); 33 | } 34 | }; 35 | 36 | return [query, updateUrl]; 37 | }; 38 | 39 | export default useQueryParam; 40 | -------------------------------------------------------------------------------- /stories/Download.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Download } from "../dist/Download"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { Download }, 8 | description: ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/telechargement-de-fichier) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Download.tsx)`, 11 | "argTypes": { 12 | "label": { 13 | "description": `Required - the label of the anchor element. In case the file to download is in a different language than the current document one, it should contains the mention of the language.` 14 | }, 15 | "details": { 16 | "description": `Required - informations about the file to download (size, format, etc.). ` 17 | } 18 | } 19 | }); 20 | 21 | export default meta; 22 | 23 | export const Default = getStory({ 24 | "label": "Télécharger le document lorem ipsum sit dolores amet", 25 | "details": "JPG – 61,88 ko", 26 | linkProps: { 27 | href: "[À MODIFIER]" 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /stories/FranceConnectButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { FranceConnectButton } from "../dist/FranceConnectButton"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory, logCallbacks } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { FranceConnectButton }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants/bouton-franceconnect/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/FranceConnectButton.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | "url": "https://example.com" 17 | }); 18 | 19 | export const Plus = getStory({ 20 | "url": "https://example.com", 21 | "plus": true 22 | }); 23 | 24 | export const Centered = getStory({ 25 | "style": { 26 | "textAlign": "center" 27 | }, 28 | "url": "https://example.com" 29 | }); 30 | 31 | export const WithOnClick = getStory({ 32 | ...logCallbacks(["onClick"]) 33 | }); 34 | -------------------------------------------------------------------------------- /stories/MonComptePro.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MonCompteProButton } from "../dist/MonCompteProButton"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory, logCallbacks } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { MonCompteProButton }, 8 | "description": ` 9 | - [See MonComptePro documentation](https://github.com/betagouv/moncomptepro#sp%C3%A9cifications-visuelles) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/MonCompteProButton.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | "url": "https://example.com" 17 | }); 18 | 19 | export const Centered = getStory({ 20 | "style": { 21 | "textAlign": "center" 22 | }, 23 | "url": "https://example.com" 24 | }); 25 | 26 | export const WithOnClick = getStory({ 27 | ...logCallbacks(["onClick"]) 28 | }); 29 | -------------------------------------------------------------------------------- /stories/Pagination.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Pagination } from "../dist/Pagination"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { Pagination }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/pagination) 10 | - [See DSFR demos](https://main--ds-gouv.netlify.app/example/component/pagination/) 11 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Pagination.tsx)`, 12 | "disabledProps": ["lang"] 13 | }); 14 | 15 | export default meta; 16 | 17 | export const Default = getStory({ 18 | count: 100, 19 | defaultPage: 2, 20 | showFirstLast: true, 21 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 22 | }); 23 | 24 | export const SummaryWithNoPage = getStory({ 25 | count: 0, 26 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 27 | }); 28 | 29 | export const SummaryWithSinglePage = getStory({ 30 | count: 1, 31 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 32 | }); 33 | 34 | export const SummaryWith132Pages = getStory({ 35 | count: 132, 36 | defaultPage: 42, 37 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 38 | }); 39 | 40 | export const SummaryWithoutShowFirstLast = getStory({ 41 | count: 45, 42 | defaultPage: 42, 43 | showFirstLast: false, 44 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 45 | }); 46 | 47 | export const SummaryWithLastPage = getStory({ 48 | count: 24, 49 | defaultPage: 24, 50 | getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) 51 | }); 52 | -------------------------------------------------------------------------------- /stories/Pictograms.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/addon-docs"; 2 | import { Pictograms } from "./picto/Pictograms"; 3 | 4 | 16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /stories/ProConnectButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ProConnectButton } from "../dist/ProConnectButton"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory, logCallbacks } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { ProConnectButton }, 8 | "description": ` 9 | - [See DSFR documentation](https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/bouton_proconnect.md) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/ProConnectButton.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | "url": "https://example.com" 17 | }); 18 | 19 | export const WithOnClick = getStory({ 20 | ...logCallbacks(["onClick"]) 21 | }); 22 | -------------------------------------------------------------------------------- /stories/RotatingLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { memo, useState } from "react"; 3 | import { useConstCallback } from "powerhooks"; 4 | import { keyframes, createMakeAndWithStyles } from "tss-react"; 5 | import keycloakifyLogoHeroMovingPngUrl from "./assets/logo-in.png"; 6 | import keycloakifyLogoHeroStillPngUrl from "./assets/logo-out.png"; 7 | 8 | const { makeStyles } = createMakeAndWithStyles({ 9 | "useTheme": () => ({}) 10 | }); 11 | 12 | export type Props = { 13 | style?: React.CSSProperties; 14 | id?: string; 15 | onLoad?: () => void; 16 | }; 17 | 18 | export const RotatingLogo = memo((props: Props) => { 19 | const { id, style, onLoad: onLoadProp } = props; 20 | 21 | const [isImageLoaded, setIsImageLoaded] = useState(false); 22 | 23 | const onLoad = useConstCallback(() => { 24 | setIsImageLoaded(true); 25 | onLoadProp?.(); 26 | }); 27 | 28 | const { classes } = useStyles({ 29 | isImageLoaded 30 | }); 31 | return ( 32 |
33 | {"keyhole"} 38 | {"Rotating 44 |
45 | ); 46 | }); 47 | 48 | const useStyles = makeStyles<{ isImageLoaded: boolean }>({ 49 | "name": { RotatingLogo } 50 | })((_theme, { isImageLoaded }) => ({ 51 | "root": { 52 | "position": "relative" 53 | }, 54 | "rotatingImg": { 55 | "animation": `${keyframes({ 56 | "from": { 57 | "transform": "rotate(0deg)" 58 | }, 59 | "to": { 60 | "transform": "rotate(360deg)" 61 | } 62 | })} infinite 20s linear`, 63 | "width": isImageLoaded ? "100%" : undefined, 64 | "height": isImageLoaded ? "auto" : undefined 65 | }, 66 | "stillImg": { 67 | "position": "absolute", 68 | "top": "0", 69 | "left": "0", 70 | "width": isImageLoaded ? "100%" : undefined, 71 | "height": isImageLoaded ? "auto" : undefined 72 | } 73 | })); 74 | -------------------------------------------------------------------------------- /stories/SkipLinks.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SkipLinks, type SkipLinksProps } from "../dist/SkipLinks"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | wrappedComponent: { SkipLinks }, 8 | description: ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/lien-d-evitement) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/SkipLinks.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | links: [ 17 | { 18 | label: "Contenu", 19 | anchor: "#contenu" 20 | }, 21 | { 22 | label: "Menu", 23 | anchor: "#header-navigation" 24 | }, 25 | { 26 | label: "Recherche", 27 | anchor: "#header-search" 28 | }, 29 | { 30 | label: "Pied de page", 31 | anchor: "#footer" 32 | } 33 | ], 34 | classes: { 35 | root: "fr-mt-9v" // Just to fix storybook preview toolbar overlapping 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /stories/Stepper.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Stepper } from "../dist/Stepper"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { Stepper }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/indicateur-d-etapes) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Stepper.tsx)` 11 | }); 12 | 13 | export default meta; 14 | 15 | export const Default = getStory({ 16 | "stepCount": 3, 17 | "currentStep": 1, 18 | "title": "Titre de l’étape en cours", 19 | "nextTitle": "Titre de la prochaine étape" 20 | }); 21 | 22 | export const StepperWithoutNext = getStory({ 23 | "stepCount": 4, 24 | "currentStep": 4, 25 | "title": "Titre de la dernière étape en cours" 26 | }); 27 | -------------------------------------------------------------------------------- /stories/TagsGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import { TagsGroup, TagsGroupProps } from "../dist/TagsGroup"; 2 | import { sectionName } from "./sectionName"; 3 | import { getStoryFactory } from "./getStory"; 4 | import { TagProps } from "../dist/Tag"; 5 | 6 | const { meta, getStory } = getStoryFactory({ 7 | sectionName, 8 | "wrappedComponent": { TagsGroup }, 9 | "description": ` 10 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants/tag) 11 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/TagsGroup.tsx)`, 12 | "argTypes": { 13 | "smallTags": { 14 | "description": ` 15 | Default: false, if true, the tags will be smaller. 16 | `, 17 | "control": { "type": "boolean" } 18 | }, 19 | "tags": { 20 | "description": `An array of TagProps (at least 1, max recommended: 6).`, 21 | "control": { "type": null } 22 | } 23 | }, 24 | "disabledProps": ["lang"], 25 | "defaultContainerWidth": 800 26 | }); 27 | 28 | export default meta; 29 | 30 | const tagsWithProps = (props?: Omit) => 31 | Array.from( 32 | { "length": 6 }, 33 | (_, i) => 34 | ({ 35 | ...props, 36 | "children": `Libellé tag ${i + 1}` 37 | } as TagProps) 38 | ) as TagsGroupProps["tags"]; 39 | 40 | export const Default = getStory({ 41 | "tags": tagsWithProps() 42 | }); 43 | 44 | export const SmallTags = getStory({ 45 | "tags": tagsWithProps(), 46 | "smallTags": true 47 | }); 48 | 49 | export const TagsAsAnchor = getStory({ 50 | "tags": tagsWithProps({ "linkProps": { "href": "#" } }) 51 | }); 52 | 53 | export const SmallTagsAsAnchor = getStory({ 54 | "tags": tagsWithProps({ "linkProps": { "href": "#" } }), 55 | "smallTags": true 56 | }); 57 | 58 | export const TagsPressed = getStory({ 59 | "tags": tagsWithProps({ "pressed": true }) 60 | }); 61 | 62 | export const SmallTagsPressed = getStory({ 63 | "tags": tagsWithProps({ "pressed": true }), 64 | "smallTags": true 65 | }); 66 | 67 | export const TagsDismissable = getStory({ 68 | "tags": tagsWithProps({ "dismissible": true }) 69 | }); 70 | 71 | export const SmallTagsDismissable = getStory({ 72 | "tags": tagsWithProps({ "dismissible": true }), 73 | "smallTags": true 74 | }); 75 | -------------------------------------------------------------------------------- /stories/Tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { assert, Equals } from "tsafe/assert"; 3 | 4 | import { Tooltip, type TooltipProps } from "../src/Tooltip"; 5 | 6 | import { sectionName } from "./sectionName"; 7 | import { getStoryFactory } from "./getStory"; 8 | 9 | const { meta, getStory } = getStoryFactory({ 10 | sectionName, 11 | "wrappedComponent": { Tooltip }, 12 | "description": ` 13 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/infobulle) 14 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Tooltip.tsx)`, 15 | "argTypes": { 16 | "id": { 17 | "control": { "type": "text" }, 18 | "description": 19 | "Optional: tootlip Id, which is also use as aria-describedby for hovered/clicked element" 20 | }, 21 | "className": { 22 | "control": { "type": "text" }, 23 | "description": "Optional" 24 | }, 25 | "kind": { 26 | "control": { "type": "select" }, 27 | "options": (() => { 28 | const options = ["hover", "click"] as const; 29 | 30 | assert>(); 31 | 32 | return options; 33 | })(), 34 | "description": "Optional." 35 | }, 36 | "title": { 37 | "control": { "type": "text" } 38 | }, 39 | "children": { 40 | "control": { "type": "text" } 41 | } 42 | } 43 | }); 44 | 45 | export default meta; 46 | 47 | const defaultOnHoverProps: TooltipProps.WithHoverAction = { 48 | "kind": "hover", 49 | "title": "lorem ipsum" 50 | }; 51 | 52 | export const Default = getStory(defaultOnHoverProps); 53 | 54 | export const TooltipOnHover = getStory(defaultOnHoverProps); 55 | 56 | export const TooltipOnHoverWithChild = getStory({ 57 | ...defaultOnHoverProps, 58 | children: Some link 59 | }); 60 | 61 | export const TooltipOnClick = getStory({ 62 | "kind": "click", 63 | "title": "lorem ipsum" 64 | }); 65 | -------------------------------------------------------------------------------- /stories/assets/logo-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/stories/assets/logo-in.png -------------------------------------------------------------------------------- /stories/assets/logo-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/stories/assets/logo-out.png -------------------------------------------------------------------------------- /stories/assets/placeholder.16x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/stories/assets/placeholder.16x9.png -------------------------------------------------------------------------------- /stories/assets/placeholder.9x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/stories/assets/placeholder.9x16.png -------------------------------------------------------------------------------- /stories/blocks/PasswordInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import PasswordInput from "../../dist/blocks/PasswordInput"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName, 7 | "wrappedComponent": { PasswordInput }, 8 | "description": `\`import { PasswordInput } from "@codegouvfr/react-dsfr/blocks/PasswordInput"\` 9 | 10 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mot-de-passe/) 11 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/blocks/PasswordInput.tsx) `, 12 | "argTypes": { 13 | "disabled": { 14 | "control": { "type": "boolean" } 15 | }, 16 | "nativeInputProps": { 17 | "description": `An object that is forwarded as props to te underlying native \`\` element. 18 | This is where you pass the \`name\` prop or \`onChange\` for example.`, 19 | "control": { "type": null } 20 | }, 21 | "messagesHint": { 22 | "description": `The text that is displayed before the list of messages. 23 | Default to "Your password must contain:" (internationalized). 24 | If you pass an empty string, the hint block wont be displayed.` 25 | } 26 | }, 27 | "doHideImportInstruction": true 28 | }); 29 | 30 | export default meta; 31 | 32 | export const Default = getStory({ 33 | "label": "Mot de passe" 34 | }); 35 | 36 | export const WithHint = getStory({ 37 | "label": "Mot de passe", 38 | /* spell-checker: disable */ 39 | "hintText": "Texte de description additionnel" 40 | /* spell-checker: english */ 41 | }); 42 | 43 | export const WithMessagesGroup = getStory({ 44 | "label": "Mot de passe", 45 | "messages": [ 46 | /* spell-checker: disable */ 47 | { 48 | "message": "12 caractères minimum", 49 | "severity": "info" 50 | }, 51 | { 52 | "message": "1 caractère spécial minimum", 53 | "severity": "valid" 54 | }, 55 | { 56 | "message": "1 chiffre minimum", 57 | "severity": "error" 58 | } 59 | /* spell-checker: enabled */ 60 | ] 61 | }); 62 | 63 | export const Disabled = getStory({ 64 | "label": "Mot de passe", 65 | "disabled": true 66 | }); 67 | -------------------------------------------------------------------------------- /stories/blocks/sectionName.ts: -------------------------------------------------------------------------------- 1 | export const sectionName = "blocks"; 2 | -------------------------------------------------------------------------------- /stories/charts/GaugeChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { GaugeChart, type GaugeChartProps } from "../../dist/Chart/GaugeChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { GaugeChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "value": { 20 | "description": "Current value" 21 | }, 22 | "init": { 23 | "description": "Base value" 24 | }, 25 | "target": { 26 | "description": "Target value" 27 | }, 28 | "color": { 29 | "description": "Color" 30 | } 31 | }, 32 | "disabledProps": ["lang"], 33 | isChartComponent: true 34 | }); 35 | 36 | export default meta; 37 | 38 | export const Default = getStory({ 39 | value: 16, 40 | init: 10, 41 | target: 20, 42 | color: "blue-france" 43 | }); 44 | -------------------------------------------------------------------------------- /stories/charts/LineChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart, type LineChartProps } from "../../dist/Chart/LineChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { LineChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "x": { 20 | "description": "Array of value for the x axis" 21 | }, 22 | "y": { 23 | "description": "Array of value for the y axis" 24 | }, 25 | "name": { "description": "Array of name", control: "object" }, 26 | "color": { "description": "Array of color", control: "object" }, 27 | "hline": { "description": "Array of horizontal lines to add", control: "object" }, 28 | "hlinename": { "description": "Name of the horizontal lines", control: "object" }, 29 | "vline": { "description": "Array of vertical lines to add", control: "object" }, 30 | "vlinename": { "description": "Name of the vertical lines", control: "object" }, 31 | "vlinecolor": { "description": "Color of the horizontal lines", control: "object" }, 32 | "hlinecolor": { "description": "Color of the vertical lines", control: "object" } 33 | }, 34 | "disabledProps": ["lang"], 35 | isChartComponent: true 36 | }); 37 | 38 | export default meta; 39 | 40 | export const Default = getStory({ 41 | x: [1, 2, 3], 42 | y: [10, 20, 30] 43 | }); 44 | -------------------------------------------------------------------------------- /stories/charts/MultiLineChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MultiLineChart, type MultiLineChartProps } from "../../dist/Chart/MultiLineChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { MultiLineChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "x": { 20 | "description": "Array of value for the x axis" 21 | }, 22 | "y": { 23 | "description": "Array of value for the y axis" 24 | }, 25 | "name": { "description": "Array of name", control: "object" }, 26 | "color": { "description": "Array of color", control: "object" }, 27 | "hline": { "description": "Array of horizontal lines to add", control: "object" }, 28 | "hlinename": { "description": "Name of the horizontal lines", control: "object" }, 29 | "vline": { "description": "Array of vertical lines to add", control: "object" }, 30 | "vlinename": { "description": "Name of the vertical lines", control: "object" }, 31 | "vlinecolor": { "description": "Color of the horizontal lines", control: "object" }, 32 | "hlinecolor": { "description": "Color of the vertical lines", control: "object" } 33 | }, 34 | "disabledProps": ["lang"], 35 | isChartComponent: true 36 | }); 37 | 38 | export default meta; 39 | 40 | export const Default = getStory({ 41 | x: [ 42 | [1, 2, 3], 43 | [1, 2, 3] 44 | ], 45 | y: [ 46 | [30, 10, 20], 47 | [10, 20, 30] 48 | ] 49 | }); 50 | -------------------------------------------------------------------------------- /stories/charts/PieChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { PieChart, type PieChartProps } from "../../dist/Chart/PieChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { PieChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "x": { 20 | "description": "Array of value for the x axis" 21 | }, 22 | "y": { 23 | "description": "Array of value for the y axis" 24 | }, 25 | "name": { "description": "Array of name" }, 26 | "color": { "description": "Array of color" }, 27 | "fill": { control: "boolean" } 28 | }, 29 | "disabledProps": ["lang"], 30 | isChartComponent: true 31 | }); 32 | 33 | export default meta; 34 | 35 | export const Default = getStory({ 36 | x: [1, 2, 3], 37 | y: [10, 20, 30], 38 | name: ["Serie 1", "Serie 2", "Serie 3"], 39 | color: ["blue-france", "green-bourgeon", "blue-ecume"] 40 | }); 41 | -------------------------------------------------------------------------------- /stories/charts/RadarChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { RadarChart, type RadarChartProps } from "../../dist/Chart/RadarChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { RadarChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "x": { 20 | "description": "Array of value for the x axis" 21 | }, 22 | "y": { 23 | "description": "Array of value for the y axis" 24 | }, 25 | "name": { "description": "Array of name", control: "object" }, 26 | "color": { "description": "Array of color", control: "object" } 27 | }, 28 | "disabledProps": ["lang"], 29 | isChartComponent: true 30 | }); 31 | 32 | export default meta; 33 | 34 | export const Default = getStory({ 35 | x: [ 36 | ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], 37 | ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], 38 | ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"] 39 | ], 40 | y: [ 41 | [65, 59, 90, 81, 56, 55, 40], 42 | [28, 48, 40, 19, 96, 27, 100], 43 | [12, 12, 20, 23, 13, 14, 15] 44 | ] 45 | }); 46 | -------------------------------------------------------------------------------- /stories/charts/ScatterChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ScatterChart, type ScatterChartProps } from "../../dist/Chart/ScatterChart"; 2 | import { getStoryFactory } from "../getStory"; 3 | import { sectionName } from "./sectionName"; 4 | 5 | const { meta, getStory } = getStoryFactory({ 6 | sectionName: sectionName, 7 | "wrappedComponent": { ScatterChart }, 8 | "description": ` 9 | - [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants-beta/graphiques-charts/) 10 | - [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Chart/BarChart.tsx) 11 | 12 | To use this component you need to add \`@gouvfr/dsfr-chart\` to your dependencies. 13 | 14 | Note for Next users: Chart components are not SSR compatible. You need to import them dynamically with [\`next/dynamic\`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic). 15 | You can find an example [here](https://github.com/codegouvfr/react-dsfr/blob/bc2c2be290b09684711c53176b7a379cebed08a8/test/integration/next-appdir/app/dsfr-chart/page.tsx#L8-L9). 16 | 17 | `, 18 | "argTypes": { 19 | "x": { 20 | "description": "Array of value for the x axis" 21 | }, 22 | "y": { 23 | "description": "Array of value for the y axis" 24 | }, 25 | "name": { "description": "Array of name", control: "object" }, 26 | "color": { "description": "Array of color", control: "object" }, 27 | "hline": { "description": "Array of horizontal lines to add", control: "object" }, 28 | "hlinename": { "description": "Name of the horizontal lines", control: "object" }, 29 | "vline": { "description": "Array of vertical lines to add", control: "object" }, 30 | "vlinename": { "description": "Name of the vertical lines", control: "object" }, 31 | "vlinecolor": { "description": "Color of the horizontal lines", control: "object" }, 32 | "hlinecolor": { "description": "Color of the vertical lines", control: "object" } 33 | }, 34 | "disabledProps": ["lang"], 35 | isChartComponent: true 36 | }); 37 | 38 | export default meta; 39 | 40 | export const Default = getStory({ 41 | x: [ 42 | [1, 5, 8], 43 | [1, 2, 15] 44 | ], 45 | y: [ 46 | [30, 10, 20], 47 | [10, 20, 30] 48 | ] 49 | }); 50 | -------------------------------------------------------------------------------- /stories/charts/sectionName.ts: -------------------------------------------------------------------------------- 1 | export const sectionName = "charts"; 2 | -------------------------------------------------------------------------------- /stories/colorHelper.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/addon-docs"; 2 | import { ColorHelper } from "./ColorHelper"; 3 | 4 | 16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /stories/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare module "*.svg" { 7 | import * as React from "react"; 8 | 9 | export const ReactComponent: React.FunctionComponent< 10 | React.SVGProps & { title?: string } 11 | >; 12 | 13 | const src: string; 14 | export default src; 15 | } 16 | -------------------------------------------------------------------------------- /stories/intro.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/addon-docs"; 2 | import { useDarkMode } from "storybook-dark-mode"; 3 | import { RotatingLogo } from "./RotatingLogo"; 4 | 5 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 🇫🇷 The French State Design System React toolkit 🇫🇷 24 |
25 | 26 | [Github](https://github.com/codegouvfr/react-dsfr) - [Documentation](https://react-dsfr.codegouv.studio/) 27 | 28 | 29 | 30 |
31 | 32 | This Storybook website serves as your daily companion for developing with React-DSFR. 33 | It not only provides visibility into the available components but also allows you to preview them in various states, 34 | enabling you to copy and paste the code directly into your project. 35 | 36 | Additionally, this website features a handy [color helper tool](/?path=/docs/%F0%9F%8E%A8-color-helper--page). 37 | This tool empowers you to navigate through the DSFR color palette and choose the most fitting shades for your specific use cases. 38 | 39 |
40 | -------------------------------------------------------------------------------- /stories/sectionName.ts: -------------------------------------------------------------------------------- 1 | export const sectionName = "components"; 2 | -------------------------------------------------------------------------------- /stories/utils.css: -------------------------------------------------------------------------------- 1 | 2 | .margin-bottom-50px { 3 | margin-bottom: 50px; 4 | } 5 | 6 | .margin-bottom-300px { 7 | margin-bottom: 300px; 8 | } 9 | 10 | .margin-bottom-600px { 11 | margin-bottom: 600px; 12 | } -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | # integration tests 2 | 3 | In this directory we test the integration of the module against mainstream React frameworks. 4 | -------------------------------------------------------------------------------- /test/integration/cra/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /public/dsfr 26 | 27 | /public/dsfr/ 28 | -------------------------------------------------------------------------------- /test/integration/cra/README.md: -------------------------------------------------------------------------------- 1 | Run the App: 2 | 3 | ```bash 4 | git clone https://github.com/codegouvfr/react-dsfr 5 | cd react-dsfr 6 | yarn 7 | yarn start-cra 8 | ``` -------------------------------------------------------------------------------- /test/integration/cra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dsfr-test-app-cra", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "18.2.0", 7 | "react-dom": "18.2.0", 8 | "react-scripts": "5.0.1", 9 | "type-route": "^0.7.2", 10 | "@mui/material": "^5.14.18", 11 | "@emotion/react": "^11.11.0", 12 | "@emotion/styled": "^11.11.0", 13 | "@mui/icons-material": "^5.14.18", 14 | "@mui/x-date-pickers": "^6.18.2", 15 | "dayjs": "^1.11.6", 16 | "@codegouvfr/react-dsfr": "file:../../../dist" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^16.7.13", 20 | "@types/react": "18.0.21", 21 | "@types/react-dom": "18.0.6", 22 | "typescript": "^4.8.3" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject", 29 | "copy-dsfr-to-public": "node node_modules/@codegouvfr/react-dsfr/bin/copy-dsfr-to-public.js", 30 | "only-include-used-icons": "node node_modules/@codegouvfr/react-dsfr/bin/only-include-used-icons.js", 31 | "prestart": "yarn copy-dsfr-to-public && yarn only-include-used-icons", 32 | "prebuild": "yarn copy-dsfr-to-public && yarn only-include-used-icons" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /test/integration/cra/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /test/integration/cra/src/Picto.tsx: -------------------------------------------------------------------------------- 1 | import { fr } from "@codegouvfr/react-dsfr"; 2 | 3 | import * as Pictogrammes from '@codegouvfr/react-dsfr/picto'; 4 | 5 | export function Picto() { 6 | return ( 7 |
8 | { 9 | Object.entries(Pictogrammes).map(([name, Component]) => ( 10 | 14 | )) 15 | } 16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /test/integration/cra/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/integration/cra/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, defineRoute } from "type-route"; 2 | 3 | const routeDefs = { 4 | "home": defineRoute("/"), 5 | "mui": defineRoute("/mui"), 6 | "picto": defineRoute("/picto"), 7 | }; 8 | 9 | export const { RouteProvider, useRoute, routes } = createRouter(routeDefs); 10 | -------------------------------------------------------------------------------- /test/integration/cra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /test/integration/next-appdir/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /test/integration/next-appdir/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | -------------------------------------------------------------------------------- /test/integration/next-appdir/README.md: -------------------------------------------------------------------------------- 1 | 2 | Run the App: 3 | 4 | ```bash 5 | git clone https://github.com/codegouvfr/react-dsfr 6 | cd react-dsfr 7 | yarn 8 | yarn start-next-appdir 9 | ``` 10 | -------------------------------------------------------------------------------- /test/integration/next-appdir/app/DefaultTrustedPolicy.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export function DefaultTrustedPolicy() { 6 | useEffect(() => { 7 | // You need to add a default trusted type to enable dsfr-chart to inject charts 8 | // Or you can disable require-trusted-types-for CSP 9 | if ( 10 | window.trustedTypes && 11 | window.trustedTypes.createPolicy && 12 | !window.trustedTypes.defaultPolicy 13 | ) { 14 | window.trustedTypes.createPolicy("default", { 15 | createHTML: string => string 16 | }); 17 | } 18 | }); 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/next-appdir/app/Follow.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Follow as BaseFollow } from "@codegouvfr/react-dsfr/Follow"; 4 | import { useState } from 'react' 5 | 6 | export const Follow = () => { 7 | const [success, setSuccess] = useState(false) 8 | return setSuccess(true) 13 | }, 14 | form: { 15 | formComponent: ({ children }) =>
{children}
, 16 | success, 17 | } 18 | }} 19 | social= {{ 20 | buttons: [ 21 | { 22 | type: "facebook", 23 | linkProps: { 24 | href: "#facebook" 25 | } 26 | }, 27 | { 28 | type: "twitter-x", 29 | linkProps: { 30 | href: "#twitter" 31 | } 32 | }, 33 | { 34 | type: "linkedin", 35 | linkProps: { 36 | href: "#linkedin" 37 | } 38 | }, 39 | { 40 | type: "instagram", 41 | linkProps: { 42 | href: "#instagram" 43 | } 44 | }, 45 | { 46 | type: "youtube", 47 | linkProps: { 48 | href: "#youtube" 49 | } 50 | } 51 | ] 52 | }} 53 | /> 54 | } -------------------------------------------------------------------------------- /test/integration/next-appdir/app/Navigation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MainNavigation } from "@codegouvfr/react-dsfr/MainNavigation"; 4 | import { useSelectedLayoutSegment } from "next/navigation"; 5 | 6 | export function Navigation() { 7 | const segment = useSelectedLayoutSegment(); 8 | 9 | return ( 10 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /test/integration/next-appdir/app/StartDsfr.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { startReactDsfr } from "@codegouvfr/react-dsfr/next-appdir"; 4 | import { defaultColorScheme } from "./defaultColorScheme"; 5 | import { addAlertTranslations } from "@codegouvfr/react-dsfr/Alert"; 6 | import Link from "next/link"; 7 | 8 | declare module "@codegouvfr/react-dsfr/next-appdir" { 9 | interface RegisterLink { 10 | Link: typeof Link; 11 | } 12 | } 13 | 14 | startReactDsfr({ 15 | defaultColorScheme, 16 | Link, 17 | "doCheckNonce": true 18 | }); 19 | 20 | export function StartDsfr(){ 21 | return null; 22 | } 23 | 24 | addAlertTranslations({ 25 | "lang": "fr", 26 | "messages": { 27 | "hide message": "Masquer le message (modifié)", 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /test/integration/next-appdir/app/consentManagement.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createConsentManagement } from "@codegouvfr/react-dsfr/consentManagement"; 4 | 5 | export const { 6 | ConsentBannerAndConsentManagement, 7 | FooterConsentManagementItem, 8 | FooterPersonalDataPolicyItem, 9 | useConsent 10 | } = createConsentManagement({ 11 | "finalityDescription": ({ lang }) => ({ 12 | "advertising": { 13 | "title": "Publicité", 14 | "description": "Nous utilisons des cookies pour vous proposer des publicités adaptées à vos centres d’intérêts et mesurer leur efficacité." 15 | }, 16 | "analytics": { 17 | "title": "Analyse", 18 | "description": "Nous utilisons des cookies pour mesurer l’audience de notre site et améliorer son contenu." 19 | }, 20 | "personalization": { 21 | "title": "Personnalisation", 22 | "description": "Nous utilisons des cookies pour vous proposer des contenus adaptés à vos centres d’intérêts." 23 | }, 24 | "statistics": { 25 | "title": "Statistiques", 26 | "description": "Nous utilisons des cookies pour mesurer l’audience de notre site et améliorer son contenu.", 27 | "subFinalities": { 28 | "deviceInfo": "Informations sur votre appareil", 29 | "traffic": "Informations sur votre navigation", 30 | } 31 | } 32 | }), 33 | "personalDataPolicyLinkProps": { 34 | "href": "/politique-de-confidentialite", 35 | }, 36 | "consentCallback": async ({ finalityConsent, finalityConsent_prev })=> { 37 | 38 | if( finalityConsent_prev === undefined && !finalityConsent.isFullConsent ){ 39 | location.reload(); 40 | await new Promise(()=> {/*never*/}); 41 | } 42 | 43 | console.log("callback from gdpr hook"); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /test/integration/next-appdir/app/defaultColorScheme.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultColorScheme } from "@codegouvfr/react-dsfr/next-appdir"; 2 | 3 | export const defaultColorScheme: DefaultColorScheme = "system"; -------------------------------------------------------------------------------- /test/integration/next-appdir/app/main.module.css: -------------------------------------------------------------------------------- 1 | .app, .container { 2 | flex: 1; 3 | margin: auto; 4 | max-width: 1000px; 5 | } -------------------------------------------------------------------------------- /test/integration/next-appdir/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export function middleware(request: NextRequest) { 4 | const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); 5 | 6 | const cspHeader = ` 7 | default-src 'self'; 8 | script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${ 9 | process.env.NODE_ENV === "development" ? "'unsafe-eval'" : "" 10 | }; 11 | style-src 'self' 'unsafe-inline'; 12 | img-src 'self' blob: data:; 13 | font-src 'self'; 14 | object-src 'none'; 15 | base-uri 'self'; 16 | form-action 'self'; 17 | frame-ancestors 'none'; 18 | block-all-mixed-content; 19 | upgrade-insecure-requests; 20 | require-trusted-types-for 'script'; 21 | trusted-types default react-dsfr react-dsfr-asap nextjs#bundler; 22 | `; 23 | 24 | const requestHeaders = new Headers(); 25 | requestHeaders.set("x-nonce", nonce); 26 | requestHeaders.set( 27 | "Content-Security-Policy", 28 | // Replace newline characters and spaces 29 | cspHeader.replace(/\s{2,}/g, " ").trim() 30 | ); 31 | 32 | return NextResponse.next({ 33 | headers: requestHeaders, 34 | request: { 35 | headers: requestHeaders 36 | } 37 | }); 38 | } 39 | 40 | export const config = { 41 | matcher: [ 42 | /* 43 | * Match all request paths except for the ones starting with: 44 | * - api (API routes) 45 | * - _next/static (static files) 46 | * - _next/image (image optimization files) 47 | * - favicon.ico (favicon file) 48 | */ 49 | { 50 | source: "/((?!api|_next/static|_next/image|favicon.ico).*)", 51 | missing: [ 52 | { type: "header", key: "next-router-prefetch" }, 53 | { type: "header", key: "purpose", value: "prefetch" } 54 | ] 55 | } 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /test/integration/next-appdir/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /test/integration/next-appdir/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, // Recommended for the `pages` directory, default in `app`. 4 | swcMinify: true, 5 | webpack: config => { 6 | 7 | config.module.rules.push({ 8 | test: /\.woff2$/, 9 | type: "asset/resource" 10 | }); 11 | 12 | return config; 13 | } 14 | }; 15 | 16 | module.exports = nextConfig; 17 | -------------------------------------------------------------------------------- /test/integration/next-appdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-appdir-integration-test-setup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "only-include-used-icons": "node node_modules/@codegouvfr/react-dsfr/bin/only-include-used-icons.js", 11 | "predev": "yarn only-include-used-icons", 12 | "prebuild": "yarn only-include-used-icons" 13 | }, 14 | "dependencies": { 15 | "@emotion/react": "11.10.5", 16 | "@emotion/server": "11.10.0", 17 | "@emotion/styled": "11.10.5", 18 | "@mui/icons-material": "^5.14.18", 19 | "@mui/material": "^5.14.18", 20 | "next": "13.5.1", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "tss-react": "^4.9.1", 24 | "tsafe": "^1.4.1", 25 | "@mui/x-date-pickers": "^6.18.2", 26 | "dayjs": "^1.11.7", 27 | "@mui/x-data-grid": "^6.18.2", 28 | "@gouvfr/dsfr-chart": "^1.0.0", 29 | "@codegouvfr/react-dsfr": "file:../../../dist" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^18.8.2", 33 | "@types/react": "18.0.26", 34 | "@types/trusted-types": "^2.0.7", 35 | "eslint": "7.30.0", 36 | "eslint-config-next": "11.0.1", 37 | "typescript": "^4.9.4", 38 | "sass": "^1.62.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/integration/next-appdir/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/test/integration/next-appdir/public/favicon.ico -------------------------------------------------------------------------------- /test/integration/next-appdir/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /test/integration/next-appdir/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "paths": { 22 | "#/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "plugins": [ 27 | { 28 | "name": "next" 29 | } 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/integration/next-appdir/ui/ClientComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from 'react'; 4 | import Stack from '@mui/material/Stack'; 5 | import Button from '@mui/material/Button'; 6 | import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark"; 7 | import { createModal } from "@codegouvfr/react-dsfr/Modal"; 8 | import { useIsModalOpen } from "@codegouvfr/react-dsfr/Modal/useIsModalOpen"; 9 | import { useIsHeaderMenuModalOpen } from "@codegouvfr/react-dsfr/Header/useIsHeaderMenuModalOpen"; 10 | 11 | export function ClientComponent() { 12 | 13 | const { isDark } = useIsDark(); 14 | 15 | const isModalOpen = useIsModalOpen(modal); 16 | 17 | const isHeaderMenuModalOpen = useIsHeaderMenuModalOpen(); 18 | 19 | console.log(`Modal ${modal.id} is currently: ${isModalOpen ? "open" : "closed"}`); 20 | console.log(`Header Modal is currently: ${isHeaderMenuModalOpen ? "open" : "closed"}`); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 27 | 28 | 29 |

Is dark? {isDark ? "yes" : "no"}

30 | 31 | 32 |

Client modal content

33 | 34 |
35 | 36 | ); 37 | } 38 | 39 | const modal = createModal({ 40 | "isOpenedByDefault": false, 41 | "id": "client-modal" 42 | }); -------------------------------------------------------------------------------- /test/integration/next-appdir/ui/ClientFooterItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FooterBottomItem } from "@codegouvfr/react-dsfr/Footer"; 3 | 4 | export function ClientFooterItem() { 5 | return ( 6 | { 11 | 12 | alert("Click on client item"); 13 | 14 | } 15 | }, 16 | text: "A client side bottom item", 17 | 18 | }} 19 | /> 20 | ); 21 | } -------------------------------------------------------------------------------- /test/integration/next-appdir/ui/ClientHeaderQuickAccessItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HeaderQuickAccessItem } from "@codegouvfr/react-dsfr/Header"; 4 | 5 | export function ClientHeaderQuickAccessItem() { 6 | 7 | //NOTE: You can use hooks here 8 | 9 | return ( 10 | { 15 | 16 | alert("Click on client item"); 17 | 18 | } 19 | }, 20 | text: "A client side item", 21 | 22 | }} 23 | /> 24 | ); 25 | } -------------------------------------------------------------------------------- /test/integration/next-pagesdir/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"], 3 | "rules": { 4 | "@next/next/no-document-import-in-page": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/next-pagesdir/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /test/integration/next-pagesdir/README.md: -------------------------------------------------------------------------------- 1 | Run the App: 2 | 3 | ```bash 4 | git clone https://github.com/codegouvfr/react-dsfr 5 | cd react-dsfr 6 | yarn 7 | yarn start-next-pagesdir 8 | ``` 9 | 10 | -------------------------------------------------------------------------------- /test/integration/next-pagesdir/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /test/integration/next-pagesdir/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | webpack: config => { 6 | config.module.rules.push({ 7 | test: /\.woff2$/, 8 | type: "asset/resource" 9 | }); 10 | return config; 11 | }, 12 | //This option requires Next 13.1 or newer, if you can't update you can use this plugin instead: https://github.com/martpie/next-transpile-modules 13 | transpilePackages: ["@codegouvfr/react-dsfr", "tss-react"] 14 | }; 15 | 16 | module.exports = nextConfig -------------------------------------------------------------------------------- /test/integration/next-pagesdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dsfr-test-app-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "only-include-used-icons": "node node_modules/@codegouvfr/react-dsfr/bin/only-include-used-icons.js", 11 | "predev": "yarn only-include-used-icons", 12 | "prebuild": "yarn only-include-used-icons" 13 | }, 14 | "dependencies": { 15 | "next": "13.5.1", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "@emotion/react": "^11.10.5", 19 | "@emotion/server": "^11.10.0", 20 | "tss-react": "^4.9.1", 21 | "@mui/material": "^5.14.18", 22 | "@mui/icons-material": "^5.14.18", 23 | "@emotion/styled": "^11.10.5", 24 | "dayjs": "^1.11.6", 25 | "@mui/x-date-pickers": "^6.18.2", 26 | "@mui/x-data-grid": "^6.18.2", 27 | "@codegouvfr/react-dsfr": "file:../../../dist" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "18.0.21", 31 | "eslint": "7.30.0", 32 | "eslint-config-next": "11.0.1", 33 | "typescript": "^4.6.4" 34 | } 35 | } -------------------------------------------------------------------------------- /test/integration/next-pagesdir/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | import type { DocumentProps } from "next/document"; 3 | import { dsfrDocumentApi, augmentDocumentWithEmotionCache } from "./_app"; 4 | 5 | const { augmentDocumentForDsfr, getColorSchemeHtmlAttributes } = dsfrDocumentApi; 6 | 7 | export default function Document(props: DocumentProps) { 8 | return ( 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | augmentDocumentForDsfr(Document); 20 | 21 | augmentDocumentWithEmotionCache(Document); 22 | -------------------------------------------------------------------------------- /test/integration/next-pagesdir/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /test/integration/vite/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | dist 4 | dist-ssr 5 | *.local 6 | 7 | # Editor directories and files 8 | .vscode/* 9 | !.vscode/extensions.json 10 | .idea 11 | .DS_Store 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | *.sw? 17 | 18 | /public/dsfr/ 19 | -------------------------------------------------------------------------------- /test/integration/vite/README.md: -------------------------------------------------------------------------------- 1 | Run the App: 2 | 3 | ```bash 4 | git clone https://github.com/codegouvfr/react-dsfr 5 | cd react-dsfr 6 | yarn 7 | yarn start-vite 8 | ``` -------------------------------------------------------------------------------- /test/integration/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Vite + React + TS 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/integration/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dsfr-test-app-vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "copy-dsfr-to-public": "node node_modules/@codegouvfr/react-dsfr/bin/copy-dsfr-to-public.js", 11 | "only-include-used-icons": "node node_modules/@codegouvfr/react-dsfr/bin/only-include-used-icons.js", 12 | "predev": "yarn copy-dsfr-to-public && yarn only-include-used-icons", 13 | "prebuild": "yarn copy-dsfr-to-public && yarn only-include-used-icons" 14 | }, 15 | "dependencies": { 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "react-router-dom": "^6.8.1", 19 | "@mui/material": "^5.14.18", 20 | "@emotion/styled": "^11.11.0", 21 | "@emotion/react": "^11.11.0", 22 | "@mui/x-date-pickers": "^6.18.2", 23 | "@mui/icons-material": "^5.14.18", 24 | "dayjs": "^1.11.6", 25 | "@codegouvfr/react-dsfr": "file:../../../dist" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "18.0.21", 29 | "@types/react-dom": "18.0.6", 30 | "@vitejs/plugin-react": "^2.1.0", 31 | "typescript": "^4.6.4", 32 | "vite": "^3.1.0" 33 | } 34 | } -------------------------------------------------------------------------------- /test/integration/vite/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegouvfr/react-dsfr/320fd6280a0ffd343c28e051e1a1a1ec3db9af6f/test/integration/vite/public/.keep -------------------------------------------------------------------------------- /test/integration/vite/src/Picto.tsx: -------------------------------------------------------------------------------- 1 | import { fr } from "@codegouvfr/react-dsfr"; 2 | 3 | import * as Pictogrammes from '@codegouvfr/react-dsfr/picto'; 4 | 5 | export function Picto() { 6 | return ( 7 |
8 | { 9 | Object.entries(Pictogrammes).map(([name, Component]) => ( 10 | 14 | )) 15 | } 16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /test/integration/vite/src/consentManagement.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createConsentManagement } from "@codegouvfr/react-dsfr/consentManagement"; 4 | 5 | export const { 6 | ConsentBannerAndConsentManagement, 7 | FooterConsentManagementItem, 8 | FooterPersonalDataPolicyItem, 9 | useConsent 10 | } = createConsentManagement({ 11 | "finalityDescription": ({ lang }) => ({ 12 | "advertising": { 13 | "title": "Publicité", 14 | "description": "Nous utilisons des cookies pour vous proposer des publicités adaptées à vos centres d’intérêts et mesurer leur efficacité." 15 | }, 16 | "analytics": { 17 | "title": "Analyse", 18 | "description": "Nous utilisons des cookies pour mesurer l’audience de notre site et améliorer son contenu." 19 | }, 20 | "personalization": { 21 | "title": "Personnalisation", 22 | "description": "Nous utilisons des cookies pour vous proposer des contenus adaptés à vos centres d’intérêts." 23 | }, 24 | "statistics": { 25 | "title": "Statistiques", 26 | "description": "Nous utilisons des cookies pour mesurer l’audience de notre site et améliorer son contenu.", 27 | "subFinalities": { 28 | "deviceInfo": "Informations sur votre appareil", 29 | "traffic": "Informations sur votre navigation", 30 | } 31 | } 32 | }), 33 | "personalDataPolicyLinkProps": { 34 | "to": "/politique-de-confidentialite", 35 | }, 36 | "consentCallback": async ({ finalityConsent, finalityConsent_prev })=> { 37 | 38 | if( finalityConsent_prev === undefined && !finalityConsent.isFullConsent ){ 39 | location.reload(); 40 | await new Promise(()=> {/*never*/}); 41 | } 42 | 43 | console.log("callback from gdpr hook"); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /test/integration/vite/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/integration/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /test/integration/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | publicDir: "public", 8 | server: { 9 | fs: { 10 | allow: ['../../..'] 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /test/runtime/README.md: -------------------------------------------------------------------------------- 1 | # runtime tests 2 | 3 | In the directory we write unit tests to make sure that, at runtime, things actually behave are expected to. 4 | This is classic unit testing. 5 | -------------------------------------------------------------------------------- /test/runtime/lib/spacing.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { spacing } from "../../../src/fr/spacing"; 3 | 4 | describe("Testing the replacer function", () => { 5 | it("with one argument", () => { 6 | const got = spacing("1v"); 7 | 8 | const expected = "0.25rem"; 9 | 10 | expect(got).toBe(expected); 11 | }); 12 | 13 | it("Works with margin topBottom", () => { 14 | const got = spacing("margin", { "topBottom": "13w" }); 15 | 16 | const expected = { 17 | "marginTop": "6.5rem", 18 | "marginBottom": "6.5rem" 19 | }; 20 | 21 | expect(got).toStrictEqual(expected); 22 | }); 23 | 24 | it("Works with margin topBottom and left", () => { 25 | const got = spacing("margin", { "topBottom": "13w", "left": 3 }); 26 | 27 | const expected = { 28 | "marginTop": "6.5rem", 29 | "marginBottom": "6.5rem", 30 | "marginLeft": 3 31 | }; 32 | 33 | expect(got).toStrictEqual(expected); 34 | }); 35 | 36 | it("Works with margin topBottom overwriting bottom", () => { 37 | const got = spacing("margin", { "topBottom": "13w", "bottom": 3 }); 38 | 39 | const expected = { 40 | "marginTop": "6.5rem", 41 | "marginBottom": 3 42 | }; 43 | 44 | expect(got).toStrictEqual(expected); 45 | }); 46 | 47 | it("Works with padding and rightLeft", () => { 48 | const got = spacing("padding", { "rightLeft": "1v" }); 49 | 50 | const expected = { 51 | "paddingRight": "0.25rem", 52 | "paddingLeft": "0.25rem" 53 | }; 54 | 55 | expect(got).toStrictEqual(expected); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/runtime/scripts/breakpoints/generateBreakpointsTsCode.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { generateBreakpointsTsCode } from "../../../../scripts/build/cssToTs/breakpoints"; 3 | 4 | it("Generation of TS code for breakpoints", () => { 5 | const input = ` 6 | @media (min-width: 36em) { 7 | /*! media sm */ 8 | 9 | /*! media sm */ 10 | .fr-hidden-sm { 11 | display: none !important; 12 | } 13 | } 14 | 15 | @media (min-width: 48em) { 16 | /*! media md */ 17 | 18 | /*! media md */ 19 | h6 { 20 | font-size: 1.25rem; 21 | line-height: 1.75rem; 22 | } 23 | } 24 | 25 | @media (min-width: 78em) { 26 | /*! media xl */ 27 | 28 | /*! media xl */ 29 | .fr-hidden-xl { 30 | display: none !important; 31 | } 32 | } 33 | 34 | @media (min-width: 62em) { 35 | /*! media lg */ 36 | 37 | /*! media lg */ 38 | .fr-hidden-lg { 39 | display: none !important; 40 | } 41 | } 42 | `; 43 | 44 | const expected = ` 45 | import { assert } from "tsafe/assert"; 46 | import type { Extends } from "tsafe"; 47 | 48 | export const breakpointsValuesUnit = "em"; 49 | 50 | export const breakpointKeys = ["xs", "sm", "md", "lg", "xl"] as const; 51 | 52 | export type BreakpointKeys = typeof breakpointKeys[number]; 53 | 54 | export const breakpointsValues = { 55 | "xs": 0, 56 | "sm": 36, 57 | "md": 48, 58 | "lg": 62, 59 | "xl": 78 60 | } as const; 61 | 62 | assert>>(); 63 | `.replace(/^\n/, ""); 64 | 65 | const got = generateBreakpointsTsCode(input); 66 | 67 | expect(got).toBe(expected); 68 | }); 69 | -------------------------------------------------------------------------------- /test/runtime/scripts/breakpoints/parseBreakpointsValues.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { parseBreakpointsValues } from "../../../../scripts/build/cssToTs/breakpoints"; 3 | import type { 4 | BreakpointsValues, 5 | MediaQueryByBreakpoint 6 | } from "../../../../scripts/build/cssToTs/breakpoints"; 7 | //import { assert } from "tsafe/assert"; 8 | //import { same } from "evt/tools/inDepth/same"; 9 | import { id } from "tsafe"; 10 | 11 | it("Parsing of breakpoint", () => { 12 | const input = ` 13 | @media (min-width: 36em) { 14 | /*! media sm */ 15 | 16 | /*! media sm */ 17 | .fr-hidden-sm { 18 | display: none !important; 19 | } 20 | } 21 | 22 | @media (min-width: 48em) { 23 | /*! media md */ 24 | 25 | /*! media md */ 26 | h6 { 27 | font-size: 1.25rem; 28 | line-height: 1.75rem; 29 | } 30 | } 31 | 32 | @media (min-width: 78em) { 33 | /*! media xl */ 34 | 35 | /*! media xl */ 36 | .fr-hidden-xl { 37 | display: none !important; 38 | } 39 | } 40 | 41 | @media (min-width: 62em) { 42 | /*! media lg */ 43 | 44 | /*! media lg */ 45 | .fr-hidden-lg { 46 | display: none !important; 47 | } 48 | } 49 | `; 50 | 51 | const expected = { 52 | "breakpointsValues": id({ 53 | "unit": "em", 54 | "sm": 36, 55 | "md": 48, 56 | "lg": 62, 57 | "xl": 78 58 | }), 59 | "mediaQueryByBreakpoint": id({ 60 | "sm": "(min-width: 36em)", 61 | "md": "(min-width: 48em)", 62 | "lg": "(min-width: 62em)", 63 | "xl": "(min-width: 78em)" 64 | }) 65 | }; 66 | 67 | const got = parseBreakpointsValues(input); 68 | 69 | expect(got).toStrictEqual(expected); 70 | }); 71 | -------------------------------------------------------------------------------- /test/runtime/scripts/classNames/generateClassNamesTsCode.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { generateClassNamesTsCode } from "../../../../scripts/build/cssToTs/classNames"; 3 | 4 | it("Generation of TS code for fr class names", () => { 5 | const input = ` 6 | .fr-text--light { 7 | font-weight: 300 !important; 8 | } 9 | 10 | .fr-text--xl, 11 | .fr-text--lead { 12 | font-size: 1.25rem !important; 13 | line-height: 2rem !important; 14 | margin: var(--text-spacing); 15 | } 16 | 17 | .fr-grid-row--gutters > [class^=fr-col-], 18 | .fr-grid-row--gutters > [class*=" fr-col-"], 19 | .fr-grid-row--gutters > .fr-col { 20 | padding: 0.5rem; 21 | } 22 | 23 | @media (min-width: 36em) { } 24 | @media (min-width: 48em) { } 25 | @media (min-width: 62em) { } 26 | @media (min-width: 78em) { } 27 | 28 | `; 29 | 30 | const expected = ` 31 | export const frCoreClassNames= [ 32 | "fr-text--light", 33 | "fr-text--xl", 34 | "fr-text--lead", 35 | "fr-grid-row--gutters", 36 | "fr-col" 37 | ] as const; 38 | 39 | export type FrCoreClassName = typeof frCoreClassNames[number]; 40 | 41 | export const frIconClassNames= [ 42 | "fr-icon-ancient-gate-fill" 43 | ] as const; 44 | 45 | export type FrIconClassName = typeof frIconClassNames[number]; 46 | 47 | export const riIconClassNames= [ 48 | "ri-airplay-fill", 49 | "ri-airplay-line" 50 | ] as const; 51 | 52 | export type RiIconClassName = typeof riIconClassNames[number]; 53 | 54 | export type FrClassName = FrCoreClassName | FrIconClassName | RiIconClassName; 55 | `.replace(/^\n/, ""); 56 | 57 | const got = generateClassNamesTsCode({ 58 | "rawCssCode": input, 59 | "dsfrIconClassNames": ["fr-icon-ancient-gate-fill"], 60 | "remixiconClassNames": ["ri-airplay-fill", "ri-airplay-line"] 61 | }); 62 | 63 | expect(got).toBe(expected); 64 | }); 65 | -------------------------------------------------------------------------------- /test/runtime/scripts/classNames/parseClassNames.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-useless-escape: 0 */ 2 | import { it, expect } from "vitest"; 3 | import { parseClassNames } from "../../../../scripts/build/cssToTs/classNames"; 4 | 5 | it("Parsing of fr classnames", () => { 6 | const input = ` 7 | .fr-text--light { 8 | font-weight: 300 !important; 9 | } 10 | 11 | .fr-text--xl, 12 | .fr-text--lead { 13 | font-size: 1.25rem !important; 14 | line-height: 2rem !important; 15 | margin: var(--text-spacing); 16 | } 17 | 18 | .fr-grid-row--gutters > [class^=fr-col-], 19 | .fr-grid-row--gutters > [class*=" fr-col-"], 20 | .fr-grid-row--gutters > .fr-col { 21 | padding: 0.5rem; 22 | } 23 | 24 | .fr-header__navbar .fr-btn--menu { 25 | color: #3a3a3a; 26 | } 27 | 28 | .fr-tile--vertical\@md { 29 | display: flex; 30 | flex-direction: column; 31 | } 32 | 33 | @supports (aspect-ratio: 16/9) { 34 | .fr-ratio-32x9 { 35 | aspect-ratio: 3.5555555556 !important; 36 | } 37 | 38 | .fr-ratio-16x9 { 39 | aspect-ratio: 1.7777777778 !important; 40 | } 41 | } 42 | 43 | @media (min-width: 36em) { } 44 | @media (min-width: 48em) { } 45 | @media (min-width: 62em) { } 46 | @media (min-width: 78em) { } 47 | 48 | `; 49 | 50 | const expected = [ 51 | "fr-text--light", 52 | "fr-text--xl", 53 | "fr-text--lead", 54 | "fr-grid-row--gutters", 55 | "fr-col", 56 | "fr-header__navbar", 57 | "fr-btn--menu", 58 | "fr-tile--vertical@md", 59 | "fr-ratio-32x9", 60 | "fr-ratio-16x9" 61 | ]; 62 | 63 | const got = parseClassNames(input); 64 | 65 | expect(got).toStrictEqual(expected); 66 | }); 67 | -------------------------------------------------------------------------------- /test/runtime/scripts/colorDecisions/generateGetColorDecisionsTsCode.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { generateGetColorDecisionsHexTsCode } from "../../../../scripts/build/cssToTs/colorDecisions"; 3 | 4 | it("Generates the correct TS code for breakpoints", () => { 5 | const input = ` 6 | :root { 7 | --grey-1000-50-hover: #ffffff; 8 | --grey-1000-50: #ffffff; 9 | --orange-terre-battue-850-200: #ffffff; 10 | --grey-975-100-hover: #ffffff; 11 | --grey-950-150: #ffffff; 12 | 13 | --background-default-grey-hover: var(--grey-1000-50-hover); 14 | --background-default-grey: var(--grey-1000-50); 15 | --border-action-low-orange-terre-battue: var(--orange-terre-battue-850-200); 16 | --background-alt-raised-grey-hover: var(--grey-975-100-hover); 17 | --background-contrast-overlap-grey: var(--grey-950-150); 18 | } 19 | 20 | :root[data-fr-theme=dark] { 21 | --grey-1000-50-hover: #000000; 22 | --grey-1000-50: #000000; 23 | --orange-terre-battue-850-200: #000000; 24 | --grey-975-100-hover: #000000; 25 | --grey-950-150: #000000; 26 | } 27 | 28 | @media (min-width: 36em) { } 29 | 30 | @media (min-width: 48em) { } 31 | 32 | @media (min-width: 62em) { } 33 | 34 | @media (min-width: 78em) { } 35 | `; 36 | 37 | const expected = ` 38 | export function getColorDecisionsHex( 39 | params: { 40 | colorOptions: ColorOptions<"hex">; 41 | } 42 | ) { 43 | 44 | const { colorOptions } = params; 45 | 46 | return { 47 | "background": { 48 | "default": { 49 | "grey": { 50 | "hover": colorOptions.grey._1000_50.hover, 51 | "default": colorOptions.grey._1000_50.default 52 | } 53 | }, 54 | "altRaised": { 55 | "grey": { 56 | "hover": colorOptions.grey._975_100.hover 57 | } 58 | }, 59 | "contrastOverlap": { 60 | "grey": { 61 | "default": colorOptions.grey._950_150.default 62 | } 63 | } 64 | }, 65 | "border": { 66 | "actionLow": { 67 | "orangeTerreBattue": { 68 | "default": colorOptions.orangeTerreBattue._850_200.default 69 | } 70 | } 71 | } 72 | } as const; 73 | }`.replace(/^\n/, ""); 74 | 75 | const got = generateGetColorDecisionsHexTsCode(input); 76 | 77 | expect(got).toBe(expected); 78 | }); 79 | -------------------------------------------------------------------------------- /test/runtime/scripts/spacing/generateSpacingTsCode.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { generateSpacingTsCode } from "../../../../scripts/build/cssToTs/spacing"; 3 | 4 | it("Generates spacing TS code", () => { 5 | const rawCssCode = ` 6 | .fr-m-7v { 7 | margin: 1.75rem !important; 8 | } 9 | 10 | .fr-m-12v, 11 | .fr-m-6w { 12 | margin: 3rem !important; 13 | } 14 | 15 | @media (min-width: 36em) { } 16 | 17 | @media (min-width: 48em) { } 18 | 19 | @media (min-width: 62em) { } 20 | 21 | @media (min-width: 78em) { } 22 | `.replace(/^\n/, ""); 23 | 24 | const got = generateSpacingTsCode(rawCssCode); 25 | 26 | const expected = ` 27 | export const spacingTokenByValue= { 28 | "7v": "1.75rem", 29 | "12v": "3rem", 30 | "6w": "3rem" 31 | } as const; 32 | 33 | export type SpacingTokenByValue = typeof spacingTokenByValue; 34 | 35 | export type SpacingToken = keyof SpacingTokenByValue; 36 | `.replace(/^\n/, ""); 37 | 38 | expect(got).toBe(expected); 39 | }); 40 | -------------------------------------------------------------------------------- /test/runtime/scripts/spacing/parseSpacing.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest"; 2 | import { parseSpacing } from "../../../../scripts/build/cssToTs/spacing"; 3 | import type { SpacingTokenAndValue } from "../../../../scripts/build/cssToTs/spacing"; 4 | 5 | it("Parse spacing successfully", () => { 6 | const input = ` 7 | .fr-m-7v { 8 | margin: 1.75rem !important; 9 | } 10 | 11 | .fr-m-12v, 12 | .fr-m-6w { 13 | margin: 3rem !important; 14 | } 15 | 16 | @media (min-width: 36em) { } 17 | 18 | @media (min-width: 48em) { } 19 | 20 | @media (min-width: 62em) { } 21 | 22 | @media (min-width: 78em) { } 23 | `; 24 | 25 | const expected: SpacingTokenAndValue[] = [ 26 | { 27 | "token": "7v", 28 | "value": "1.75rem" 29 | }, 30 | { 31 | "token": "12v", 32 | "value": "3rem" 33 | }, 34 | { 35 | "token": "6w", 36 | "value": "3rem" 37 | } 38 | ]; 39 | 40 | const got = parseSpacing(input); 41 | 42 | expect(got).toStrictEqual(expected); 43 | }); 44 | -------------------------------------------------------------------------------- /test/types/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Checkbox } from "../../src/Checkbox"; 3 | 4 | { 5 | ; 24 | } 25 | { 26 | 45 | } 46 | ]} 47 | />; 48 | } 49 | -------------------------------------------------------------------------------- /test/types/README.md: -------------------------------------------------------------------------------- 1 | # Types tests 2 | 3 | In this directory we test the type definition of the module API. 4 | Theses tests are not meant to be run. If they TypeScript says there's no error, they passes. 5 | 6 | See [tsafe](https://github.com/garronej/tsafe) for more info about type-lever testing. 7 | -------------------------------------------------------------------------------- /test/types/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tag } from "../../src/Tag"; 3 | 4 | { 5 | Label; 6 | } 7 | { 8 | Label; 9 | } 10 | { 11 | 12 | Label 13 | ; 14 | } 15 | { 16 | console.log("clicked")}> 17 | Label 18 | ; 19 | } 20 | { 21 | 27 | Label 28 | ; 29 | } 30 | { 31 | console.log("clicked")}>Label; 32 | } 33 | { 34 | console.log("clicked on my dismissible tag")}> 35 | Label 36 | ; 37 | } 38 | { 39 | console.log("clicked")}> 40 | Label 41 | ; 42 | } 43 | { 44 | //@ts-expect-error: children is required 45 | ; 46 | } 47 | { 48 | //@ts-expect-error: nativeSpanProps and onClick are mutually exclusive 49 | console.log("clicked")} 51 | nativeSpanProps={{ 52 | "id": "foo" 53 | }} 54 | > 55 | Label 56 | ; 57 | } 58 | { 59 | //@ts-expect-error: linkProps and onClick are mutually exclusive 60 | console.log("clicked")} 65 | > 66 | Label 67 | ; 68 | } 69 | { 70 | //@ts-expect-error: we shouldn't use Tag component as a span if it's dismissible 71 | 77 | Label 78 | ; 79 | } 80 | { 81 | //@ts-expect-error: we shouldn't use Tag component as an anchor if it's dismissible 82 | 88 | Label 89 | ; 90 | } 91 | -------------------------------------------------------------------------------- /test/types/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from "../../src/Tooltip"; 2 | 3 | { 4 | Exemple; 5 | } 6 | { 7 | 8 | Exemple 9 | ; 10 | } 11 | { 12 | ; 13 | } 14 | { 15 | 16 | Exemple 17 | ; 18 | } 19 | -------------------------------------------------------------------------------- /test/types/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createComponentI18nApi } from "../../src/lib/i18n"; 2 | import { assert } from "tsafe/assert"; 3 | import type { Equals } from "tsafe"; 4 | 5 | const { useTranslation, addSomeDsfrComponentTranslations } = createComponentI18nApi({ 6 | "componentName": "SomeDsfrComponent", 7 | "frMessages": { 8 | /* spell-checker: disable */ 9 | 10 | "do something": ({ what }: { what: string }) => `Faire ${what}`, 11 | "the good": "le bien", 12 | "bar": "bar" 13 | /* spell-checker: enable */ 14 | } 15 | }); 16 | 17 | addSomeDsfrComponentTranslations({ 18 | "lang": "en", 19 | "messages": { 20 | "do something": ({ what }) => { 21 | assert>(); 22 | return `Do ${what}`; 23 | }, 24 | "the good": "the good" 25 | } 26 | }); 27 | 28 | const { t } = useTranslation(); 29 | 30 | t("do something", { "what": t("the good") }); 31 | -------------------------------------------------------------------------------- /test/types/libBinSync.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: We can't import between src/lib and src/bin 3 | so when we copy declaration values we make sure they stay in sync here 4 | */ 5 | import { assert } from "tsafe/assert"; 6 | import type { Equals } from "tsafe"; 7 | import type { ColorScheme as ColorScheme_lib } from "../../src/useIsDark"; 8 | import type { ColorScheme as ColorScheme_bin } from "../../scripts/build/cssToTs/colorOptions"; 9 | 10 | assert>(); 11 | -------------------------------------------------------------------------------- /test/types/spacing.ts: -------------------------------------------------------------------------------- 1 | import { spacing } from "../../src/fr/spacing"; 2 | import { assert } from "tsafe/assert"; 3 | import { Equals } from "tsafe"; 4 | 5 | { 6 | const got = spacing("margin", { "topBottom": 4 }); 7 | 8 | type Expected = { 9 | marginTop: number; 10 | marginBottom: number; 11 | }; 12 | 13 | assert>(); 14 | } 15 | 16 | { 17 | const got = spacing("margin", { "topBottom": "1v" }); 18 | 19 | type Expected = { 20 | marginTop: "0.25rem"; 21 | marginBottom: "0.25rem"; 22 | }; 23 | 24 | assert>(); 25 | } 26 | 27 | { 28 | const got = spacing("margin", { "topBottom": "1v", "rightLeft": 33 }); 29 | 30 | type Expected = { 31 | marginTop: "0.25rem"; 32 | marginBottom: "0.25rem"; 33 | marginLeft: number; 34 | marginRight: number; 35 | }; 36 | 37 | assert>(); 38 | } 39 | 40 | { 41 | const got = spacing("margin", { "topBottom": "1v", "rightLeft": 33 }); 42 | 43 | type Expected = { 44 | marginTop: "0.25rem"; 45 | marginBottom: "0.25rem"; 46 | marginLeft: number; 47 | marginRight: number; 48 | }; 49 | 50 | assert>(); 51 | } 52 | 53 | { 54 | const got = spacing("margin", { "bottom": "1v" }); 55 | 56 | type Expected = { 57 | marginBottom: "0.25rem"; 58 | }; 59 | 60 | assert>(); 61 | } 62 | 63 | { 64 | const got = spacing("padding", { "bottom": "1v", "rightLeft": 42 }); 65 | 66 | type Expected = { 67 | paddingRight: number; 68 | paddingLeft: number; 69 | paddingBottom: "0.25rem"; 70 | }; 71 | 72 | assert>(); 73 | } 74 | 75 | { 76 | const got = spacing("margin", { "rightLeft": "auto" }); 77 | 78 | type Expected = { 79 | marginLeft: "auto"; 80 | marginRight: "auto"; 81 | }; 82 | 83 | assert>(); 84 | } 85 | -------------------------------------------------------------------------------- /tsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "sourceMap": true, 5 | "newLine": "LF", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "incremental": true, 9 | "strict": true, 10 | "downlevelIteration": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "composite": true, 13 | "outDir": "./dist", 14 | "skipLibCheck": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | "test": { 5 | // ref: https://vitest.dev/config/ 6 | "include": ["test/runtime/**/*.test.ts"], 7 | "watch": false, 8 | "outputFile": "./vitest-report.json", 9 | "testTimeout": 43600 10 | } 11 | }); 12 | --------------------------------------------------------------------------------