first commit:

- we have a static astro website under /website. It has the
implementation docs of the homepage/gaufre templates, and it handles the
few API endpoints (the gaufre js, backgrounds, logos)
- we have a vite app under /packages/integration. It has the react
components generating the homepage and the gaufre button, and their css.
Its used to generate an npm package
This commit is contained in:
Emmanuel Pelletier
2024-04-22 11:19:08 +02:00
commit d9859f1564
136 changed files with 17496 additions and 0 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
[packages/integration/dist/html/**/*.html]
indent_style = space
indent_size = 4

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# build output
dist/
# content generated by build scripts
website/public/api/backgrounds/v1/*
website/src/assets/logos/*
# this folder contains shutterstock photos to resize/compress
website/src/assets/backgrounds/sources/*
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
.aider*
!.gitkeep

View File

@@ -0,0 +1 @@
VITE_LASUITE_API_URL=https://integration.lasuite.numerique.gouv.fr

View File

@@ -0,0 +1 @@
dist/

View File

@@ -0,0 +1,4 @@
{
"semi": false,
"printWidth": 100
}

View File

@@ -0,0 +1,56 @@
# @gouvfr-lasuite/integration npm package
The `@gouvfr-lasuite/integration` npm package helps services of [La Suite numérique](https://lasuite.numerique.gouv.fr/) with already-made React and HTML templates of common _La Suite_ UIs.
For now, it helps developers integrate:
- a _La Suite_ branded homepage
- the _Gaufre_ ("waffle") button that lets users of a service easily switch between _La Suite_ services.
## Usage
```
npm install @gouvfr-lasuite/integration
```
If you use React, you can directly consume the exposed React components.
If you use anything else, you can copy and paste content from the HTML templates in the `dist/html` folder.
CSS is also available. Depending on whether or not you use the [DSFR](https://www.systeme-de-design.gouv.fr/), you can use different CSS files from `dist/css`.
Precise documentation on usage is available on [integration.lasuite.numerique.gouv.fr](https://integration.lasuite.numerique.gouv.fr).
## Development
This folder is meant to generate the `@gouvfr-lasuite/integration` npm package.
It's a vite app.
To start, `npm install` a first time and copy the example env file: `cp .env.example .env`. Make sure the API env var targets a running API. If you don't want to use the production one, you can run one locally easily: the API is exposed via the `/website` server, go check the README there.
Then, run the local dev server with `npm run dev`.
The main dev file is `src/dev.tsx` where a small testing React router is used to render the different React components while developing.
### Building
Run `npm run build` to build all the `dist/` files which are: React components, CSS files and HTML templates.
Internally, building all of this is a bit different than your usual vite app. We actually use 3 vite configs, and running `npm run build` runs them all:
#### React components
The default build generates the React components in es6 and commonjs files with the vite "lib" mode. Everything in `src/index.ts` is exposed in the generated file.
#### CSS
The `css-config` build generates the CSS files. They all go through purgecss. The list of CSS files to generate is in the css vite config.
CSS is rendered like that, and not through the main vite config, because CSS rendering in lib mode doesn't allow us to easily generate multiple CSS files while using the postcss-config.
#### HTML files
The `html-config` generates the HTML files.
**HTML files are not written by hand**: they are generated from the React components. The html vite config checks the `src/html.tsx` file and renders HTML files for every template listed.

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@gouvfr-lasuite/integration : exemples</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/dev.tsx"></script>
<script
id="lasuite-gaufre-script"
async
defer
src="%VITE_LASUITE_API_URL%/api/v1/gaufre.js"
></script>
</body>
</html>

4546
packages/integration/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
{
"name": "@gouvfr-lasuite/integration",
"version": "0.1.0",
"type": "module",
"files": [
"dist"
],
"main": "./dist/index.umd.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
},
"./dist/css/*.css": {
"import": "./dist/css/*.css",
"require": "./dist/css/*.css"
},
"./dist/html/*.html": {
"import": "./dist/html/*.html",
"require": "./dist/html/*.html"
},
"./dist/logos/*.svg": {
"import": "./dist/logos/*.svg",
"require": "./dist/logos/*.svg"
}
},
"scripts": {
"dev": "vite",
"build": "patch-package && tsc && vite build && concurrently \"vite build -c vite.css-config.ts\" \"vite build -c vite.html-config.ts\"",
"prepack": "npm run build"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "*",
"react-dom": "*",
"typescript": "*"
},
"devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.24.1",
"@fullhuman/postcss-purgecss": "^6.0.0",
"@gouvfr/dsfr": "^1.11.2",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2",
"dotenv": "^16.4.5",
"import-single-ts": "^1.0.3",
"patch-package": "^8.0.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-plugin-dts": "^3.9.0",
"wouter": "^3.1.2"
}
}

View File

@@ -0,0 +1,107 @@
diff --git a/node_modules/@gouvfr/dsfr/dist/dsfr.css b/node_modules/@gouvfr/dsfr/dist/dsfr.css
index 9444fd2..6877e96 100644
--- a/node_modules/@gouvfr/dsfr/dist/dsfr.css
+++ b/node_modules/@gouvfr/dsfr/dist/dsfr.css
@@ -1,8 +1,6 @@
/*!
* DSFR v1.11.2 | SPDX-License-Identifier: MIT | License-Filename: LICENSE.md | restricted use (see terms and conditions)
*/
-@charset "UTF-8";
-
/* ¯¯¯¯¯¯¯¯¯ *\
CORE
\* ˍˍˍˍˍˍˍˍˍ */
@@ -1341,80 +1339,6 @@ audio:not([href]) {
transition: none !important;
}
-/**
- * Déclaration des fontes
- */
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Light.woff2") format("woff2"), url("fonts/Marianne-Light.woff") format("woff");
- font-weight: 300;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Light_Italic.woff2") format("woff2"), url("fonts/Marianne-Light_Italic.woff") format("woff");
- font-weight: 300;
- font-style: italic;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Regular.woff2") format("woff2"), url("fonts/Marianne-Regular.woff") format("woff");
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Regular_Italic.woff2") format("woff2"), url("fonts/Marianne-Regular_Italic.woff") format("woff");
- font-weight: 400;
- font-style: italic;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Medium.woff2") format("woff2"), url("fonts/Marianne-Medium.woff") format("woff");
- font-weight: 500;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Medium_Italic.woff2") format("woff2"), url("fonts/Marianne-Medium_Italic.woff") format("woff");
- font-weight: 500;
- font-style: italic;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Bold.woff2") format("woff2"), url("fonts/Marianne-Bold.woff") format("woff");
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: Marianne;
- src: url("fonts/Marianne-Bold_Italic.woff2") format("woff2"), url("fonts/Marianne-Bold_Italic.woff") format("woff");
- font-weight: 700;
- font-style: italic;
- font-display: swap;
-}
-@font-face {
- font-family: Spectral;
- src: url("fonts/Spectral-Regular.woff2") format("woff2"), url("fonts/Spectral-Regular.woff") format("woff");
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: Spectral;
- src: url("fonts/Spectral-ExtraBold.woff2") format("woff2"), url("fonts/Spectral-ExtraBold.woff") format("woff");
- font-weight: 900;
- font-style: normal;
- font-display: swap;
-}
-
h6 {
font-weight: 700;
font-size: 1.125rem;
diff --git a/node_modules/@gouvfr/dsfr/dist/utility/utility.css b/node_modules/@gouvfr/dsfr/dist/utility/utility.css
index 2941a15..1b43d4a 100644
--- a/node_modules/@gouvfr/dsfr/dist/utility/utility.css
+++ b/node_modules/@gouvfr/dsfr/dist/utility/utility.css
@@ -1,8 +1,6 @@
/*!
* DSFR v1.11.2 | SPDX-License-Identifier: MIT | License-Filename: LICENSE.md | restricted use (see terms and conditions)
*/
-@charset "UTF-8";
-
.fr-background-default--grey {
background-color: var(--background-default-grey) !important;

View File

@@ -0,0 +1,13 @@
import purgecss from "@fullhuman/postcss-purgecss"
import autoprefixer from "autoprefixer"
export default {
plugins: [
purgecss({
content: ["./src/**/*.{js,jsx,ts,tsx,html}"],
css: ["./src/**/*.css"],
variables: true,
}),
autoprefixer(),
],
}

View File

@@ -0,0 +1,17 @@
<svg viewBox="0 0 41 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.7941 20.6945C33.8535 20.6945 32.052 19.4495 31.4319 17.4987C30.6646 15.0855 31.9962 12.5063 34.406 11.7379C34.868 11.5906 35.3361 11.5205 35.7965 11.5205C37.7371 11.5205 39.5386 12.7656 40.1587 14.7161C40.9261 17.1295 39.5944 19.7087 37.1846 20.4772C36.7227 20.6244 36.2544 20.6945 35.7941 20.6945Z" fill="#000091"/>
<path d="M34.5271 27.8442C35.25 27.4943 35.7975 26.809 35.9377 25.9554C36.1665 24.5616 35.2238 23.2458 33.832 23.0167C33.0544 22.8887 32.2816 22.8267 31.5179 22.8267C30.0882 22.8267 28.6926 23.0442 27.3699 23.452C26.4866 22.2232 25.8084 20.8654 25.3541 19.4365C24.8882 17.9708 24.6574 16.4304 24.6814 14.8774C26.7391 14.2179 28.6763 13.0776 30.315 11.4558C30.8765 10.9003 31.1266 10.1487 31.0647 9.41846C31.1336 9.9223 31.0533 10.4517 30.8025 10.9377C28.4985 15.4006 30.2496 20.9084 34.7064 23.2161C35.9591 23.8649 36.4499 25.4081 35.8021 26.6625C35.5168 27.2153 35.058 27.6199 34.5271 27.8442Z" fill="#000091"/>
<path d="M29.6422 38.9999C28.2165 38.9999 26.8121 38.3354 25.9176 37.0873C24.4433 35.0295 24.9138 32.1644 26.9685 30.6878C27.7768 30.107 28.7102 29.8276 29.6343 29.8276C31.0598 29.8276 32.4644 30.4919 33.3587 31.7402C34.8332 33.798 34.3628 36.6631 32.3081 38.1395C31.4997 38.7205 30.5664 38.9999 29.6422 38.9999Z" fill="#E1000F"/>
<path d="M20.0313 37.7184C19.8934 37.7184 19.7541 37.7071 19.6136 37.6842C19.0005 37.583 18.4746 37.2713 18.0977 36.8349C18.5686 37.3268 19.229 37.6241 19.9395 37.6241C20.0673 37.6241 20.1968 37.6145 20.3271 37.5946C21.7217 37.3818 22.6799 36.0774 22.4675 34.681C22.1266 32.4399 21.2783 30.4005 20.0573 28.6611C20.9513 27.4401 22.0312 26.3748 23.2479 25.5005C24.4958 24.6038 25.8874 23.9079 27.3698 23.4512C28.632 25.2071 30.3135 26.6994 32.3601 27.759C32.7353 27.9534 33.1362 28.0454 33.5312 28.0454C33.8743 28.0454 34.2133 27.9757 34.5269 27.8433C34.1886 28.0071 33.8119 28.0976 33.4204 28.0976C33.2828 28.0976 33.1432 28.0862 33.0029 28.0633C32.5052 27.9811 32.0101 27.9416 31.5212 27.9416C27.1486 27.9416 23.2809 31.1162 22.5482 35.5754C22.3423 36.8286 21.2591 37.7184 20.0313 37.7184Z" fill="#E1000F"/>
<path d="M33.5248 28.0462C33.1297 28.0462 32.7288 27.9543 32.3537 27.7599C30.307 26.7003 28.6255 25.208 27.3633 23.452C28.686 23.0442 30.0815 22.8267 31.5113 22.8267C32.2749 22.8267 33.0477 22.8887 33.8253 23.0167C35.2172 23.2458 36.1599 24.5616 35.9311 25.9554C35.7908 26.809 35.2433 27.4942 34.5205 27.8442C34.2068 27.9765 33.8679 28.0462 33.5248 28.0462Z" fill="#6D2700"/>
<path d="M10.3416 38.7923C9.39936 38.7923 8.44894 38.5023 7.63175 37.9003C5.59448 36.3995 5.15785 33.5291 6.65647 31.489C7.55399 30.2671 8.94222 29.6201 10.3489 29.6201C11.2912 29.6201 12.2416 29.9104 13.0588 30.5124C15.096 32.013 15.5327 34.8836 14.034 36.9237C13.1366 38.1454 11.7483 38.7923 10.3416 38.7923Z" fill="#E1000F"/>
<path d="M18.0934 36.8362C17.7418 36.4693 17.4958 35.9937 17.4135 35.4527C16.7284 30.9489 12.8409 27.7125 8.42383 27.7125C7.96983 27.7125 7.51056 27.7468 7.04827 27.8171C6.91806 27.837 6.78862 27.8466 6.6608 27.8466C5.42004 27.8466 4.33134 26.9397 4.13887 25.6739C4.04527 25.0586 4.17885 24.4614 4.47689 23.9673C4.0973 24.6761 4.05657 25.5525 4.4443 26.3257C4.89261 27.2199 5.79338 27.7363 6.72875 27.7363C7.11393 27.7363 7.5051 27.6487 7.87279 27.4637C9.89584 26.4467 11.5707 25.0083 12.8452 23.3081C14.2811 23.7821 15.6267 24.4815 16.8329 25.3701C18.0701 26.2816 19.1611 27.3918 20.053 28.6624C18.7755 30.4072 17.8777 32.4698 17.5038 34.7465C17.3756 35.5265 17.6144 36.2821 18.0934 36.8362Z" fill="#E1000F"/>
<path d="M19.9345 37.6241C19.224 37.6241 18.5636 37.3268 18.0926 36.8349C17.6137 36.2808 17.3748 35.5252 17.5031 34.7452C17.8769 32.4685 18.7747 30.4059 20.0522 28.6611C21.2733 30.4005 22.1216 32.4399 22.4625 34.681C22.6749 36.0774 21.7167 37.3819 20.3221 37.5946C20.1918 37.6145 20.0623 37.6241 19.9345 37.6241Z" fill="#6D2700"/>
<path d="M4.58127 20.3585C4.1036 20.3585 3.618 20.2832 3.13975 20.1244C0.739113 19.3276 -0.561933 16.7329 0.233815 14.3288C0.871033 12.4037 2.6598 11.1846 4.57985 11.1846C5.05753 11.1846 5.54314 11.26 6.02139 11.4187C8.42203 12.2156 9.72307 14.8105 8.92732 17.2145C8.29011 19.1395 6.50134 20.3585 4.58127 20.3585Z" fill="#000091"/>
<path d="M4.48047 23.9669C4.72047 23.5187 5.09596 23.1376 5.58427 22.8921C10.0678 20.6379 11.8839 15.1512 9.63285 10.6609C9.00015 9.39848 9.50889 7.86127 10.7693 7.22763C11.137 7.04273 11.5283 6.95508 11.9135 6.95508C12.11 6.95508 12.305 6.97788 12.4945 7.02216C12.3443 6.99509 12.192 6.98153 12.0397 6.98153C11.3912 6.98153 10.7423 7.22739 10.2446 7.71995C9.2413 8.71285 9.23173 10.3323 10.2233 11.3369C11.8145 12.9493 13.6979 14.1001 15.7066 14.7884C15.7001 16.3025 15.4516 17.8001 14.9805 19.2236C14.4972 20.6835 13.7798 22.0655 12.8488 23.3077C11.4468 22.8447 9.9589 22.5965 8.43031 22.5965C7.72172 22.5965 7.00417 22.6498 6.28274 22.7599C5.50254 22.879 4.85893 23.3396 4.48047 23.9669Z" fill="#000091"/>
<path d="M6.73234 27.7346C5.79697 27.7346 4.89619 27.2182 4.44789 26.324C4.06016 25.5508 4.10088 24.6744 4.48048 23.9656C4.85894 23.3383 5.50255 22.8776 6.28274 22.7586C7.00418 22.6485 7.72173 22.5952 8.43032 22.5952C9.95891 22.5952 11.4468 22.8433 12.8488 23.3064C11.5743 25.0067 9.89943 26.445 7.87638 27.462C7.50869 27.647 7.11751 27.7346 6.73234 27.7346Z" fill="#6D2700"/>
<path d="M20.3024 9.17157C20.2932 9.17157 20.2841 9.17155 20.2749 9.17148C17.7458 9.1565 15.7077 7.09128 15.7227 4.55868C15.7376 2.03535 17.7851 0 20.3013 0C20.3107 0 20.3196 4.36376e-05 20.329 8.73232e-05C22.8581 0.0150715 24.896 2.08029 24.8812 4.61289C24.8664 7.13624 22.8189 9.17157 20.3024 9.17157Z" fill="#000091"/>
<path d="M20.3304 15.5561C20.302 15.5561 20.2741 15.5561 20.2457 15.5559C18.7099 15.5467 17.1755 15.2905 15.7082 14.7877C15.7175 12.6241 15.2333 10.4269 14.1994 8.36499C13.8453 7.65864 13.2089 7.18804 12.4961 7.02148C12.996 7.11156 13.4738 7.3514 13.858 7.74064C15.6347 9.54114 17.9821 10.4433 20.3299 10.4433C22.642 10.4433 24.9547 9.56899 26.7254 7.81682C27.2232 7.32436 27.8721 7.07852 28.5206 7.07852C29.1793 7.07852 29.8376 7.33204 30.3371 7.83827C30.774 8.28092 31.0165 8.84278 31.0654 9.41786C30.9563 8.62108 30.4749 7.88805 29.7074 7.49067C29.3324 7.29651 28.9315 7.20446 28.5365 7.20446C27.6119 7.20446 26.7196 7.70883 26.2656 8.58803C25.2261 10.6017 24.7152 12.751 24.6821 14.8768C23.2695 15.3296 21.7996 15.5561 20.3304 15.5561Z" fill="#000091"/>
<path d="M24.6797 14.8774C24.7128 12.7517 25.2237 10.6024 26.2632 8.58864C26.7172 7.70945 27.6095 7.20508 28.5341 7.20508C28.9291 7.20508 29.33 7.29712 29.705 7.49128C30.4725 7.88867 30.9539 8.62169 31.063 9.41847C31.1249 10.1488 30.8748 10.9004 30.3133 11.4558C28.6746 13.0776 26.7374 14.2179 24.6797 14.8774Z" fill="#000091"/>
<path d="M15.713 14.7878C13.7042 14.0995 11.8208 12.9487 10.2296 11.3363C9.23809 10.3317 9.24766 8.71228 10.251 7.71937C10.7487 7.22682 11.3976 6.98096 12.0461 6.98096C12.1983 6.98096 12.3507 6.99452 12.5009 7.02158C13.2137 7.18814 13.8501 7.65874 14.2042 8.36509C15.2381 10.427 15.7223 12.6242 15.713 14.7878Z" fill="#000091"/>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 16.3267C0 14.8254 1.21889 13.6065 2.72014 13.6065C4.22138 13.6065 5.44027 14.8254 5.44027 16.3267V21.2194L25.8636 0.796048C26.9253 -0.265349 28.6487 -0.265349 29.7104 0.796048C30.7721 1.85772 30.7721 3.58147 29.7104 4.64286L4.64355 29.71C3.86559 30.488 2.69565 30.7205 1.67914 30.2995C0.662624 29.8784 0 28.8866 0 27.7863V16.3267Z" fill="#060383"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1243 23.6733C39.1243 25.1746 37.9054 26.3935 36.4041 26.3935C34.9029 26.3935 33.684 25.1746 33.684 23.6733V18.7806L13.2607 39.204C12.199 40.2653 10.4755 40.2653 9.41385 39.204C8.35219 38.1423 8.35219 36.4185 9.41385 35.3571L34.4807 10.29C35.2587 9.51204 36.4286 9.27947 37.4451 9.70054C38.4616 10.1216 39.1243 11.1134 39.1243 12.2137V23.6733Z" fill="#C62B23"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

View File

@@ -0,0 +1,10 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7322 0.963989C22.7817 0.963989 23.7867 1.40851 24.5293 2.12001C25.272 2.87598 25.6899 3.89868 25.6899 4.92147C25.6899 8.0343 25.6899 11.8589 25.6899 11.8589C25.6899 11.8589 21.701 11.8589 18.5571 11.8589C16.4848 11.8589 14.8083 10.1689 14.8083 8.12333C14.8083 4.96603 14.8083 0.963989 14.8083 0.963989C14.8083 0.963989 18.6194 0.963989 21.7322 0.963989Z" fill="#2CB0AF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.9968 7.90164C39.9968 8.95111 39.5523 9.95613 38.8408 10.6988C38.0848 11.4414 37.0621 11.8593 36.0393 11.8593C32.9265 11.8593 29.1019 11.8593 29.1019 11.8593C29.1019 11.8593 29.1019 7.87045 29.1019 4.72649C29.1019 2.65424 30.7919 0.977783 32.8375 0.977783C35.9948 0.977783 39.9968 0.977783 39.9968 0.977783C39.9968 0.977783 39.9968 4.78881 39.9968 7.90164Z" fill="#2CB0AF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7322 15.0231C22.7817 15.0231 23.7867 15.4676 24.5293 16.1791C25.272 16.9351 25.6899 17.9578 25.6899 18.9806C25.6899 22.0934 25.6899 25.918 25.6899 25.918H18.5571C16.4848 25.918 14.8083 24.228 14.8083 22.1824C14.8083 19.0251 14.8083 15.0231 14.8083 15.0231C14.8083 15.0231 18.6194 15.0231 21.7322 15.0231Z" fill="#D1D1D1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.9219 15.0231C7.97137 15.0231 8.97639 15.4676 9.71902 16.1791C10.4617 16.9351 10.8796 17.9578 10.8796 18.9806C10.8796 22.0934 10.8796 25.918 10.8796 25.918H3.74676C1.6745 25.918 -0.00195312 24.228 -0.00195312 22.1824C-0.00195312 19.0251 -0.00195312 15.0231 -0.00195312 15.0231C-0.00195312 15.0231 3.80907 15.0231 6.9219 15.0231Z" fill="#FAC980"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7322 28.3887C22.7817 28.3887 23.7867 28.8332 24.5293 29.5447C25.272 30.3007 25.6899 31.3234 25.6899 32.3462C25.6899 35.459 25.6899 39.2836 25.6899 39.2836C25.6899 39.2836 21.701 39.2836 18.5571 39.2836C16.4848 39.2836 14.8083 37.5936 14.8083 35.548C14.8083 32.3907 14.8083 28.3887 14.8083 28.3887C14.8083 28.3887 18.6194 28.3887 21.7322 28.3887Z" fill="#D1D1D1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.9219 28.3887C7.97137 28.3887 8.97639 28.8332 9.71902 29.5447C10.4617 30.3007 10.8796 31.3234 10.8796 32.3462C10.8796 35.459 10.8796 39.2836 10.8796 39.2836C10.8796 39.2836 6.89072 39.2836 3.74676 39.2836C1.6745 39.2836 -0.00195312 37.5936 -0.00195312 35.548C-0.00195312 32.3907 -0.00195312 28.3887 -0.00195312 28.3887C-0.00195312 28.3887 3.80907 28.3887 6.9219 28.3887Z" fill="#FAC980"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.9968 21.9538C39.9968 23.0032 39.5523 24.0083 38.8408 24.7509C38.0848 25.4935 37.0621 25.9115 36.0393 25.9115H29.1019V18.7786C29.1019 16.7064 30.7919 15.0299 32.8375 15.0299C35.9948 15.0299 39.9968 15.0299 39.9968 15.0299C39.9968 15.0299 39.9968 18.8409 39.9968 21.9538Z" fill="#D1D1D1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.9968 35.3388C39.9968 36.3882 39.5523 37.3933 38.8408 38.1359C38.0848 38.8785 37.0621 39.2965 36.0393 39.2965H29.1019V32.1636C29.1019 30.0914 30.7919 28.4149 32.8375 28.4149C35.9948 28.4149 39.9968 28.4149 39.9968 28.4149C39.9968 28.4149 39.9968 32.2259 39.9968 35.3388Z" fill="#D1D1D1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="38" height="37" viewBox="0 0 38 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.00585938 19.4141L37.5407 0.945312L11.54 23.581L0.00585938 19.4141Z" fill="#000091"/>
<path d="M16.4277 25.0516L37.5411 0.945312L30.7362 30.4229L16.4277 25.0516Z" fill="#000091"/>
<path d="M15.5469 27.7373L21.9982 30.1032L15.5469 36.9451V27.7373Z" fill="#E1000F"/>
</svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="resana.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="5.9"
inkscape:cx="20"
inkscape:cy="22.457627"
inkscape:window-width="1914"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 17.824291,0.58368972 c 1.345956,-0.77825321 3.005461,-0.77825321 4.351418,0 3.442584,1.99141078 10.108026,5.83671538 13.5507,7.82813068 1.345866,0.7782532 2.175754,2.2091616 2.175754,3.7656136 0,3.982849 0,11.673709 0,15.645087 0,1.556542 -0.829888,2.998562 -2.175754,3.776833 -3.442674,1.979954 -10.108116,5.825518 -13.5507,7.816943 -1.345957,0.778271 -3.005462,0.778271 -4.351418,0 C 14.381707,37.424872 7.7162198,33.579308 4.2735999,31.599354 2.9276766,30.821083 2.097837,29.379063 2.097837,27.822521 c 0,-3.971378 0,-11.662238 0,-15.645087 0,-1.556452 0.8298396,-2.9873604 2.1757629,-3.7656136 C 7.7162198,6.4204051 14.381707,2.5751005 17.824291,0.58368972 Z"
fill="#04043e"
id="path1"
style="stroke-width:0.89611" />
<path
d="M 13.25458,29.553088 V 9.7682435 h 6.048471 c 4.352672,0 7.037775,2.2329255 7.037775,5.9072435 0,2.40238 -1.158849,4.18304 -3.193824,5.1158 l 6.189787,8.761801 h -4.720078 l -5.285343,-7.998674 h -2.063293 v 7.998674 z m 6.274649,-16.449614 h -2.261154 v 5.11571 h 2.261154 c 1.695797,0 2.685013,-0.989215 2.685013,-2.600241 0,-1.498026 -0.989216,-2.515469 -2.685013,-2.515469 z"
fill="#ffffff"
id="path2"
style="stroke-width:0.89611" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,33 @@
<svg width="480" height="169" viewBox="0 0 480 169" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M41.6279 84.5078C41.6279 108.09 57.607 125.931 79.0224 125.931C79.9876 125.931 80.9404 125.894 81.8745 125.819V98.6872C75.0806 97.4729 70.3292 91.7874 70.3292 84.5078C70.3292 77.2281 75.0806 71.5365 81.8745 70.3222V43.1964C80.9404 43.1155 79.9876 43.0781 79.0224 43.0781C57.607 43.0781 41.6279 60.9253 41.6279 84.5078Z" fill="#000091"/>
<path d="M296.15 45.9783H268.221V125.108H296.15V45.9783Z" fill="#E1000F"/>
<path d="M119.531 43.0781H91.6016V125.107H119.531V43.0781Z" fill="#000091"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.8544 12.3591L27.9292 124.069H0V12.3591H27.8544Z" fill="#000091"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M164.459 61.6758C171.751 61.6758 177.798 64.3162 184.162 67.8844L196.112 50.8155C188.353 45.6968 178.109 40.7336 163.369 40.7336C161.028 40.7336 158.742 40.8644 156.532 41.126V64.3473C157.217 62.4293 160.579 61.6758 164.459 61.6758Z" fill="#E1000F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.019 72.5548C161.059 71.4214 156.706 70.4001 155.997 68.1023V92.7869C156.632 92.9177 157.261 93.0485 157.871 93.1731C164.696 94.568 169.348 95.6577 169.348 98.6032C169.348 99.0329 169.217 99.4126 169.03 99.7738C169.223 100.135 169.348 100.515 169.348 100.951C169.348 103.585 165.624 104.986 159.882 104.986C158.544 104.986 157.261 104.88 155.997 104.737V125.723C157.603 125.854 159.253 125.928 160.972 125.928C181.142 125.928 196.81 116.463 196.81 98.3105C196.81 78.7633 179.124 74.8837 167.019 72.5548Z" fill="#E1000F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M157.871 93.173C157.261 93.0485 156.632 92.9176 155.997 92.7869V95.1283C156.632 95.2653 157.261 95.3962 157.871 95.5207C163.693 96.7101 167.909 97.6877 169.03 99.7738C169.217 99.4126 169.348 99.0328 169.348 98.6031C169.348 95.6577 164.696 94.5679 157.871 93.173Z" fill="#E1000F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M440.715 102.34C435.203 102.34 430.763 100.21 427.843 96.4926V124.44C431.666 125.381 435.702 125.922 439.936 125.922C454.053 125.922 466.47 120.492 475.002 111.65L456.694 95.5149C453.748 98.616 448.007 102.34 440.715 102.34Z" fill="#E1000F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M437.919 44.5889C434.419 44.5889 431.062 44.9998 427.843 45.7035V73.1095C430.271 69.9959 433.628 68.1713 437.607 68.1713C443.193 68.1713 448.007 71.7395 450.024 77.948H427.843V93.9271H479.193C479.66 91.9157 479.815 88.8083 479.815 86.635C479.815 62.2804 461.812 44.5889 437.919 44.5889Z" fill="#E1000F"/>
<path d="M342.946 45.9783H315.017V125.108H342.946V45.9783Z" fill="#E1000F"/>
<path d="M342.946 0H315.017V27.8369H342.946V0Z" fill="#E1000F"/>
<path d="M390.218 27.8362H362.289V46.4555H362.127V71.9H362.289V97.1887C362.289 114.413 371.288 127.447 392.703 127.447C398.912 127.447 403.875 126.357 408.682 123.872V97.8115C405.886 99.2064 402.48 100.446 398.75 100.446C392.859 100.446 390.218 97.1888 390.218 92.5308V71.9H408.682V46.4555H390.218V27.8362Z" fill="#E1000F"/>
<path d="M251.305 98.9288C245.102 98.9288 240.912 94.7379 240.912 88.3799V44.9387H212.982V92.2594C212.982 112.585 228.189 125.93 251.305 125.93C254.026 125.93 256.629 125.724 259.12 125.37V95.6782C257.289 97.7269 254.593 98.9288 251.305 98.9288Z" fill="#E1000F"/>
<path d="M158.904 160.532C160.447 160.532 161.789 159.257 161.789 157.68C161.789 156.137 160.447 154.828 158.904 154.828C157.293 154.828 156.001 156.137 156.001 157.68C156.001 159.257 157.293 160.532 158.904 160.532Z" fill="#080605"/>
<path d="M167.443 160.147H171.604V149.342C172.224 148.285 173.348 146.876 175.446 146.876C177.492 146.876 178.734 148.251 178.734 150.449V160.147H182.945V150.298C182.945 145.399 180.076 142.933 176.503 142.933C174.305 142.933 172.761 143.789 171.604 144.913V143.587H167.443V160.147Z" fill="#080605"/>
<path d="M200.209 152.983C200.209 155.382 198.967 156.859 196.702 156.859C194.404 156.859 193.145 155.382 193.145 152.983V143.588H188.951V152.781C188.951 157.747 191.803 160.801 196.669 160.801C201.534 160.801 204.42 157.747 204.42 152.781V143.588H200.209V152.983Z" fill="#080605"/>
<path d="M210.694 160.147H214.855V149.342C215.476 148.285 216.466 146.876 218.378 146.876C220.24 146.876 221.331 148.134 221.331 150.13V160.147H225.525V149.962C225.525 149.694 225.525 149.476 225.508 149.241C226.163 148.134 227.136 146.876 228.948 146.876C230.827 146.876 231.901 148.134 231.901 150.13V160.147H236.111V149.962C236.111 145.298 233.41 142.933 230.005 142.933C227.203 142.933 225.626 144.191 224.451 145.634C223.361 143.855 221.532 142.933 219.418 142.933C217.388 142.933 215.979 143.755 214.855 144.846V143.587H210.694V160.147Z" fill="#080605"/>
<path d="M257.754 157.445L254.768 155.08C253.878 156.271 252.301 157.059 250.489 157.059C247.906 157.059 245.993 155.817 245.607 152.73H257.2C257.318 152.16 257.452 151.305 257.452 150.382C257.452 146.053 254.432 142.933 249.835 142.933C244.416 142.933 241.329 146.976 241.329 151.875C241.329 156.69 244.651 160.8 250.523 160.8C253.643 160.8 256.21 159.525 257.754 157.445ZM249.734 146.355C252.066 146.355 253.291 148.034 253.358 149.694H245.725C246.194 147.53 247.604 146.355 249.734 146.355Z" fill="#080605"/>
<path d="M263.173 160.147H267.334V149.543C267.971 148.487 269.28 147.363 271.31 147.363C272.065 147.363 272.618 147.464 273.155 147.598V143.504C272.753 143.369 272.266 143.269 271.713 143.269C269.833 143.269 268.424 144.125 267.334 145.232V143.588H263.173V160.147Z" fill="#080605"/>
<path d="M280.252 140.702C281.728 140.702 282.936 139.461 282.936 137.984C282.936 136.508 281.728 135.249 280.252 135.249C278.775 135.249 277.517 136.508 277.517 137.984C277.517 139.461 278.775 140.702 280.252 140.702ZM278.138 160.147H282.315V143.588H278.138V160.147Z" fill="#080605"/>
<path d="M287.768 151.875C287.768 156.69 290.922 160.8 296.173 160.8C298.404 160.8 300.015 160.079 301.29 158.804V168.216H305.501V143.587H301.29V144.946C300.015 143.655 298.404 142.933 296.173 142.933C290.922 142.933 287.768 147.043 287.768 151.875ZM292.096 151.875C292.096 148.973 293.908 146.876 296.726 146.876C298.605 146.876 300.183 147.698 301.29 149.241V154.492C300.283 156.002 298.673 156.858 296.726 156.858C293.908 156.858 292.096 154.761 292.096 151.875Z" fill="#080605"/>
<path d="M323.033 152.983C323.033 155.382 321.792 156.859 319.527 156.859C317.228 156.859 315.987 155.382 315.987 152.983V143.588H311.776V152.781C311.776 157.747 314.628 160.801 319.493 160.801C324.359 160.801 327.244 157.747 327.244 152.781V143.588H323.033V152.983Z" fill="#080605"/>
<path d="M348.752 157.445L345.766 155.08C344.877 156.271 343.3 157.059 341.488 157.059C338.904 157.059 336.992 155.817 336.606 152.73H348.199C348.333 152.16 348.45 151.305 348.45 150.382C348.45 146.053 345.43 142.933 340.833 142.933C335.415 142.933 332.328 146.976 332.328 151.875C332.328 156.69 335.649 160.8 341.521 160.8C344.642 160.8 347.209 159.525 348.752 157.445ZM340.733 146.355C343.065 146.355 344.29 148.034 344.357 149.694H336.74C337.193 147.53 338.602 146.355 340.733 146.355Z" fill="#080605"/>
<path d="M356.084 160.532C357.628 160.532 358.97 159.257 358.97 157.68C358.97 156.137 357.628 154.828 356.084 154.828C354.473 154.828 353.182 156.137 353.182 157.68C353.182 159.257 354.473 160.532 356.084 160.532Z" fill="#080605"/>
<path d="M362.812 163.032C362.812 166.521 365.932 168.35 371.217 168.35C376.367 168.35 379.891 165.532 379.891 161.858C379.891 158.771 377.626 156.539 373.448 156.539H369.287C368.532 156.539 368.164 156.204 368.164 155.549C368.164 155.18 368.365 154.828 368.717 154.493C369.22 154.593 369.741 154.627 370.294 154.627C374.572 154.627 376.988 151.993 376.988 148.755C376.988 148.184 376.938 147.664 376.77 147.144H379.689V143.587H373.65C372.693 143.168 371.569 142.933 370.294 142.933C366.15 142.933 363.667 145.534 363.667 148.755C363.667 150.584 364.456 152.227 365.899 153.318C364.657 154.241 364.087 155.281 364.087 156.606C364.087 157.445 364.456 158.335 365.11 158.989C363.634 160.08 362.812 161.388 362.812 163.032ZM370.361 151.472C368.549 151.472 367.476 150.382 367.476 148.755C367.476 147.144 368.549 146.053 370.361 146.053C372.106 146.053 373.213 147.111 373.213 148.755C373.213 150.382 372.106 151.472 370.361 151.472ZM366.754 162.444C366.754 161.421 367.241 160.7 367.861 160.113H372.995C374.992 160.113 375.747 161.002 375.747 162.244C375.747 163.888 374.203 165.012 371.183 165.012C368.264 165.012 366.754 164.021 366.754 162.444Z" fill="#080605"/>
<path d="M391.517 142.933C386.131 142.933 382.44 146.909 382.44 151.875C382.44 156.824 386.131 160.8 391.517 160.8C396.902 160.8 400.576 156.824 400.576 151.875C400.576 146.909 396.902 142.933 391.517 142.933ZM391.584 156.858C388.816 156.858 386.786 154.761 386.786 151.875C386.786 148.973 388.816 146.876 391.584 146.876C394.235 146.876 396.248 148.973 396.248 151.875C396.248 154.727 394.235 156.858 391.584 156.858Z" fill="#080605"/>
<path d="M416.934 152.983C416.934 155.382 415.693 156.859 413.411 156.859C411.113 156.859 409.871 155.382 409.871 152.983V143.588H405.66V152.781C405.66 157.747 408.529 160.801 413.378 160.801C418.243 160.801 421.129 157.747 421.129 152.781V143.588H416.934V152.983Z" fill="#080605"/>
<path d="M425.306 143.588L431.312 160.147H436.799L442.838 143.588H438.342L434.081 155.415L429.769 143.588H425.306Z" fill="#080605"/>
<path d="M447.116 160.532C448.66 160.532 450.002 159.257 450.002 157.68C450.002 156.137 448.66 154.828 447.116 154.828C445.505 154.828 444.23 156.137 444.23 157.68C444.23 159.257 445.505 160.532 447.116 160.532Z" fill="#080605"/>
<path d="M455.387 143.588L454.901 146.121H457.954L454.079 168.217H456.897L460.773 146.121H466.326L466.745 143.588H461.209L461.595 141.356C461.997 139.091 463.373 138.051 464.984 138.051C465.923 138.051 466.695 138.37 467.282 138.84L468.792 136.625C467.936 136.038 466.695 135.518 464.95 135.518C461.796 135.518 459.464 137.414 458.81 141.356L458.407 143.588H455.387Z" fill="#080605"/>
<path d="M468.037 160.147H470.822L472.802 149.041C474.11 147.346 475.687 145.987 477.852 145.987C478.439 145.987 478.875 146.087 479.328 146.188L479.814 143.454C479.395 143.336 478.875 143.269 478.305 143.269C476.14 143.269 474.697 144.242 473.355 145.802L473.741 143.588H470.956L468.037 160.147Z" fill="#080605"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,6 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.0156 0H8.98438C4.02344 0 0 4.02344 0 8.98438V31.0156C0 35.9766 4.02344 40 8.98438 40H31.0156C35.9766 40 40 35.9766 40 31.0156V8.98438C40 4.02344 35.9766 0 31.0156 0Z" fill="#000091"/>
<path d="M31.8816 26.4702L20 33.3201L8.11841 26.4702V12.7704L20 5.92053L31.8816 12.7704V26.4702Z" fill="white"/>
<path d="M21.1245 15.2365H19.0781V27.1463H21.1245V15.2365Z" fill="#E1000F"/>
<path d="M15.3506 19.1888H13.3042V14.212H27.3879V16.262H15.3506V19.1888Z" fill="#E1000F"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,76 @@
import { useTranslate } from "../i18n/useTranslate"
export type Props = {
/**
* URL de l'action du formulaire.
*
* @default "#"
*/
action?: string
/**
* attribut `name` à définir sur le champ de saisie de l'email.
*
* @default "email"
*/
inputName?: string
/**
* Méthode du formulaire.
*
* @default "post"
*/
method?: "get" | "post"
/**
* Fonction appelée lors de la soumission du formulaire.
*
* L'envoi du formulaire n'est jamais intercepté par le composant.
* À vous d'appeler `event.preventDefault()` si vous souhaitez empêcher l'envoi du formulaire.
*
*/
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void
/**
* Contenu du formulaire.
*
* À utiliser si vous voulez rajouter des champs supplémentaires.
*/
children?: React.ReactNode
}
export const EmailForm = ({
action = "#",
method = "post",
onSubmit,
inputName = "email",
children,
}: Props) => {
const { t } = useTranslate()
return (
<form
action={action}
method={method}
className="fr-mb-4w fr-mb-md-6w"
onSubmit={(event) => {
if (onSubmit) {
onSubmit(event)
}
}}
>
<div className="fr-mb-4w fr-mb-md-6w">
<input
className="fr-input lasuite-input"
name={inputName}
type="email"
aria-label={t("email.srLabel")}
placeholder={t("email.placeholder")}
/>
</div>
{children}
<div>
<button className="fr-btn lasuite-btn">{t("email.submit")}</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,215 @@
import { ReactNode } from "react"
import { useTranslate } from "../i18n/useTranslate"
export type Props = {
/**
* Nom de l'entité, affiché au niveau du logo Marianne.
*
* @example "Gouvernement"
* @example "<>Ministère de <br />l'Intérieur</>"
*/
entity: ReactNode
/**
* L'url vers la page d'accueil de votre service.
*
* @default "/"
*/
homepageUrl?: string
/**
* Nom du service, utilisé dans des libellés accessibles de liens.
*/
serviceName?: string
/**
* texte affiché sur la droite du footer, au dessus des liens du gouvernement.
*/
description?: string
/**
* bloc de contenu indiquant la licence utilisée pour le site. Mentionne la licence etalab par défaut.
*/
license?: ReactNode
/**
* URL de la page de plan du site.
*/
sitemapUrl?: string
/**
* URL de la page d'accessibilité.
*/
a11yUrl?: string
/**
* Niveau d'accessibilité du site.
*/
a11yLevel?: "non compliant" | "partially compliant" | "fully compliant"
/**
* URL de la page de mentions légales.
*/
termsUrl?: string
/**
* URL de la page de politique de confidentialité.
*/
privacyUrl?: string
/**
* Liens à afficher en bas de page, en supplément du plan du site, de l'accessibilité, des mentions légales et de la politique de confidentialité.
*
* Par défaut, on affiche un lien vers le site vitrine de la suite numérique
*/
links?: Array<{ label: string; url: string }>
/**
* Liens légaux à afficher en bas de page.
*
* Par défaut, on affiche les liens indiqués dans le système de design de l'État.
*/
legalLinks?: Array<{ label: string; url: string }>
}
export const Footer = ({
entity,
homepageUrl = "/",
serviceName,
description,
sitemapUrl,
a11yUrl,
a11yLevel,
termsUrl,
privacyUrl,
links = [
{
label: "La Suite Numérique",
url: "https://lasuite.numerique.gouv.fr/",
},
],
legalLinks = [
{
label: "legifrance.gouv.fr",
url: "https://legifrance.gouv.fr",
},
{
label: "info.gouv.fr",
url: "https://info.gouv.fr",
},
{
label: "service-public.fr",
url: "https://service-public.fr",
},
{
label: "data.gouv.fr",
url: "https://data.gouv.fr",
},
],
license,
}: Props) => {
const { t } = useTranslate()
const a11yLevelLabel =
a11yLevel === "fully compliant"
? t("footer.links.a11y.perfect")
: a11yLevel === "partially compliant"
? t("footer.links.a11y.partial")
: t("footer.links.a11y.bad")
return (
<footer className="fr-footer" role="contentinfo" id="footer-7127">
<div className="fr-container lasuite-container">
<div className="fr-footer__body">
<div className="fr-footer__brand fr-enlarge-link">
<a
id="footer-operator"
href={homepageUrl}
title={
serviceName
? t("footer.homepageLinkTitle.withService", { serviceName })
: t("footer.homepageLinkTitle.withoutService")
}
>
<p className="fr-logo">{entity}</p>
</a>
</div>
<div className="fr-footer__content">
{!!description && <p className="fr-footer__content-desc">{description}</p>}
<ul className="fr-footer__content-list">
{legalLinks.map((link) => (
<li key={link.url} className="fr-footer__content-item">
<a
target="_blank"
rel="noopener external"
title={t("links.newWindow", { title: link.label })}
className="fr-footer__content-link"
href={link.url}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</div>
<div className="fr-footer__bottom">
<ul className="fr-footer__bottom-list">
{!!sitemapUrl && (
<li className="fr-footer__bottom-item">
<a className="fr-footer__bottom-link" href={sitemapUrl}>
{t("footer.links.sitemap")}
</a>
</li>
)}
{!!a11yUrl && (
<li className="fr-footer__bottom-item">
<a className="fr-footer__bottom-link" href={a11yUrl}>
{a11yLevelLabel}
</a>
</li>
)}
{!!termsUrl && (
<li className="fr-footer__bottom-item">
<a className="fr-footer__bottom-link" href={termsUrl}>
{t("footer.links.terms")}
</a>
</li>
)}
{!!privacyUrl && (
<li className="fr-footer__bottom-item">
<a className="fr-footer__bottom-link" href={privacyUrl}>
{t("footer.links.privacy")}
</a>
</li>
)}
{links.map((link) => (
<li key={link.url} className="fr-footer__bottom-item">
<a className="fr-footer__bottom-link" href={link.url}>
{link.label}
</a>
</li>
))}
</ul>
<div className="fr-footer__bottom-copy">
{(license === undefined && (
<p>
{t("footer.license", {
license: (
<a
href="https://github.com/etalab/licence-ouverte/blob/master/LO.md"
target="_blank"
rel="noopener external"
title={t("links.newWindow", { title: "licence etalab-2.0" }) as string}
>
{t("footer.licenseEtalab")}
</a>
),
})}
</p>
)) ||
license}
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,14 @@
import { useTranslate } from "../i18n/useTranslate"
export const Gaufre = () => {
const { t } = useTranslate()
return (
<button
type="button"
className="lasuite-gaufre-btn lasuite-gaufre-btn--vanilla js-lasuite-gaufre-btn"
title={t("gaufre.label")}
>
{t("gaufre.label")}
</button>
)
}

View File

@@ -0,0 +1,111 @@
import { ReactNode } from "react"
import { Gaufre } from "./Gaufre"
import { useTranslate } from "../i18n/useTranslate"
export type Props = {
/**
* Nom de l'entité, affiché au niveau du logo Marianne.
*
* @example "Gouvernement"
* @example "<>Ministère de <br />l'Intérieur</>"
*/
entity: ReactNode
/**
* L'url vers la page d'accueil de votre service.
*
* @default "/"
*/
homepageUrl?: string
/**
* Nom du service, affiché dans le header et utilisé dans des libellés accessibles de liens.
*/
serviceName: string
/**
* Logo du service.
*
* Peut être une chaine de caractère vers un fichier, ou un élément React.
*/
logo: ReactNode
/**
* Afficher le nom du service à côté du logo ou non.
*
* Utile si votre logo à lui-seul est suffisamment explicite.
*
* @default true
*/
showServiceName?: boolean
/**
* liste des actions à afficher à droite du header.
*
* Si ceci est passé, vous devez passer manuellement le composant <Gaufre /> dans les actions
*/
actions?: ReactNode
}
export const Header = ({
entity,
serviceName,
logo,
homepageUrl = "/",
showServiceName = true,
actions,
}: Props) => {
const { t } = useTranslate()
return (
<header role="banner" className="fr-header">
<div className="fr-header__body">
<div className="fr-container lasuite-container">
<div className="fr-header__body-row">
<div className="fr-header__brand lasuite-header__brand fr-enlarge-link">
<div className="fr-header__brand-top lasuite-header__brand-top">
<div className="fr-header__logo">
<p className="fr-logo">{entity}</p>
</div>
</div>
<div className="fr-header__service lasuite-header__service">
<a
className="lasuite-header__service-link"
href={homepageUrl}
title={t("header.homepageLinkTitle", { serviceName })}
aria-label={t("header.homepageLinkTitle", { serviceName })}
>
{typeof logo === "string" ? (
<img
className="lasuite-header__service-logo fr-responsive-img"
src={logo}
alt={showServiceName ? "" : serviceName}
/>
) : (
logo
)}
{showServiceName && (
<p className="fr-header__service-title lasuite-header__service-title">
{serviceName}
</p>
)}
</a>
</div>
</div>
{typeof actions === "undefined" ? (
<div className="fr-header__tools">
<div
className="fr-header__tools-links lasuite-header__tools-links"
data-fr-js-header-links="true"
>
<Gaufre />
</div>
</div>
) : (
actions
)}
</div>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,94 @@
import { ReactNode } from "react"
import { Header, type Props as HeaderProps } from "./Header"
import { Footer, type Props as FooterProps } from "./Footer"
import { HomepageContent } from "./HomepageContent"
export type Props = {
/**
* Nom de l'entité, affiché au niveau du logo Marianne.
*
* @example "Gouvernement"
* @example "<>Ministère de <br />l'Intérieur</>"
*/
entity: ReactNode
/**
* Phrase d'accroche.
*
* Si vous passez une chaîne de caractères, vous pouvez facilement mettre du texte en gras en entourant le texte par `**`. Et vous pouvez ajouter des sauts de ligne en utilisant `<br>`.
*
* Sinon, vous pouvez passer directement un élément React.
*
* @example "**Tchap**, la messagerie <br>sécurisée de l'État"
*/
tagline?: ReactNode
/**
* url de l'API de lasuite-integration. Nécessaire pour afficher la photo d'arrière-plan.
*/
lasuiteApiUrl?: string
/**
* L'url vers la page d'accueil de votre service.
*
* @default "/"
*/
homepageUrl?: string
/**
* Nom du service, affiché dans le header et utilisé dans des libellés accessibles de liens.
*/
serviceName: string
/**
* Identifiant du service sur l'API de la suite-integration.
*
* Utilisé pour afficher la photo d'arrière-plan correspondant au service, via l'API.
*/
serviceId?: string
/**
* Logo du service.
*
* Peut être une chaine de caractère vers un fichier, ou un élément React.
*/
logo?: ReactNode
/**
* options passées au composant Header.
*/
headerOptions?: Omit<HeaderProps, "entity" | "serviceName" | "logo" | "homepageUrl">
/**
* options passées au composant Footer.
*/
footerOptions?: Omit<FooterProps, "entity" | "serviceName" | "homepageUrl">
/**
* Contenu de la page d'accueil affiché dans la partie droite de la page.
*
* Passez ici le formulaire de connexion au service. Vous pouvez utiliser pour vous aider les composants déjà existants 'HomepageEmail', 'HomepageProconnect' et 'HomepageEmailOrProconnect'.
*/
children: ReactNode
}
export const Homepage = ({
lasuiteApiUrl,
entity,
tagline,
serviceName,
serviceId,
logo,
homepageUrl,
headerOptions,
footerOptions,
children,
}: Props) => {
return (
<div className="lasuite lasuite-homepage">
<Header {...{ entity, serviceName, logo, homepageUrl, ...headerOptions }} />
<HomepageContent {...{ serviceId, tagline, lasuiteApiUrl }}>{children}</HomepageContent>
<Footer {...{ entity, serviceName, homepageUrl, ...footerOptions }} />
</div>
)
}

View File

@@ -0,0 +1,16 @@
import { EmailForm, type Props as EmailFormProps } from "../EmailForm"
import { useTranslate } from "../../i18n/useTranslate"
export type Props = EmailFormProps
export const Email = (emailFormProps: Props) => {
const { t } = useTranslate()
return (
<>
<h2 className="fr-h4 fr-mb-8w lasuite-text-center">{t("email.title")}</h2>
<div className="lasuite-input-width">
<EmailForm {...emailFormProps} />
</div>
</>
)
}

View File

@@ -0,0 +1,25 @@
import { ProconnectButton, type Props as ProconnectProps } from "../ProconnectButton"
import { EmailForm, type Props as EmailFormProps } from "../EmailForm"
import { useTranslate } from "../../i18n/useTranslate"
export type Props = {
emailForm?: EmailFormProps
proconnectUrl: ProconnectProps["url"]
}
export const EmailOrProconnect = ({ proconnectUrl, emailForm = {} }: Props) => {
const { t } = useTranslate()
return (
<>
<h2 className="fr-h4 fr-mb-4w fr-mb-md-8w lasuite-text-center">{t("email.title")}</h2>
<div className="lasuite-input-width">
<EmailForm {...emailForm} />
<p className="fr-hr-or lasuite-hr-or fr-mb-6w">{t("proconnect.or")}</p>
<h2 className="fr-sr-only">{t("proconnect.title")}</h2>
<ProconnectButton url={proconnectUrl} />
</div>
</>
)
}

View File

@@ -0,0 +1,16 @@
import { useTranslate } from "../../i18n/useTranslate"
import { ProconnectButton, type Props as ProconnectButtonProps } from "../ProconnectButton"
export type Props = ProconnectButtonProps
export const Proconnect = (proconnectButtonProps: Props) => {
const { t } = useTranslate()
return (
<>
<h2 className="fr-h4 fr-mb-4w fr-mb-md-8w lasuite-text-center">{t("proconnect.title")}</h2>
<div className="lasuite-input-width">
<ProconnectButton {...proconnectButtonProps} />
</div>
</>
)
}

View File

@@ -0,0 +1,101 @@
import { ReactNode, Fragment } from "react"
export type Props = {
/**
* Phrase d'accroche.
*
* Si vous passez une chaîne de caractères, vous pouvez facilement mettre du texte en gras en entourant le texte par `**`. Et vous pouvez ajouter des sauts de ligne en utilisant `<br>`.
*
* Sinon, vous pouvez passer directement un élément React.
*
* @example "**Tchap**, la messagerie <br>sécurisée de l'État"
*/
tagline: ReactNode
/**
* Contenu de la page d'accueil affiché dans la partie droite de la page.
*
* Passez ici le formulaire de connexion au service. Vous pouvez utiliser pour vous aider les composants déjà existants 'HomepageEmail', 'HomepageProconnect' et 'HomepageEmailOrProconnect'.
*/
children: ReactNode
/**
* Identifiant du service sur l'API de la suite-integration.
*
* Utilisé pour afficher la photo d'arrière-plan correspondant au service, via l'API.
*/
serviceId?: string
/**
* url de l'API de lasuite-integration. Nécessaire pour afficher la photo d'arrière-plan.
*/
lasuiteApiUrl?: string
}
export const HomepageContent = ({ tagline, lasuiteApiUrl, serviceId, children }: Props) => {
const parsedTagline =
typeof tagline === "string" ? (
<>
{tagline
.replace(/<br\/>/g, "<br>")
.replace(/<br \/>/g, "<br>")
.split("<br>")
.map((part) => {
return part.split(/(\*\*.*?\*\*)/g).map((part, index) => {
if (part.startsWith("**") && part.endsWith("**")) {
return (
<strong className="lasuite-homepage__tagline-strong" key={index}>
{part.slice(2, -2)}
</strong>
)
} else {
return <Fragment key={index}>{part}</Fragment>
}
})
})
.map((part, index) => (
<Fragment key={index}>
{part}
<br />
</Fragment>
))}
</>
) : (
tagline
)
return (
<div className="lasuite-homepage__wrapper">
<div className="fr-container fr-p-0 lasuite-container">
<div className="lasuite-homepage__content fr-pt-8w fr-pb-6w lasuite-py-lg-16w">
<div className="fr-container--fluid">
<div className="fr-grid-row">
<div className="fr-col-12 fr-col-xl-6 fr-mb-4w">
<h1 className="lasuite-homepage__tagline">{parsedTagline}</h1>
</div>
<div className="fr-mt-8w fr-mb-4w lasuite-mt-lg-0 fr-col-12 fr-col-xl-6 lasuite-homepage__form-container">
<div className="lasuite-homepage__form">
<div className="lasuite-homepage__form-inner">{children}</div>
</div>
</div>
</div>
</div>
</div>
</div>
{!!lasuiteApiUrl && (
<picture className="lasuite-homepage__bg">
<source
srcSet={`${lasuiteApiUrl}/api/backgrounds/v1/${serviceId || "default"}.avif`}
type="image/avif"
/>
<img
src={`${lasuiteApiUrl}/api/backgrounds/v1/${serviceId || "default"}.jpg`}
alt=""
width="1920"
height="1080"
/>
</picture>
)}
</div>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,188 @@
// main file used for development, used by the default vite config when running npm run dev
import React, { type ReactNode } from "react"
import ReactDOM from "react-dom/client"
import { Link, Route, Switch } from "wouter"
import { navigate } from "wouter/use-browser-location"
import { Homepage } from "./components/Homepage"
import { Gaufre } from "./components/Gaufre"
import { EmailOrProconnect } from "./components/Homepage/EmailOrProconnect"
import { Email } from "./components/Homepage/Email"
import { Proconnect } from "./components/Homepage/Proconnect"
import services from "../../../website/src/data/services.json"
import "./styles/dsfr.css"
import "./styles/homepage.css"
import "./styles/gaufre.css"
import "./styles/dev.css"
const serviceHomepage = ({ id, content }: { content?: ReactNode; id: string }) => {
const service = services.find(({ id: itemId }) => itemId === id)
if (!service) {
console.log(`Service ${id} not found, exiting`)
return null
}
const { name, tagline } = service
return {
path: `/homepage-template-${id}`,
label: `Homepage ${name}`,
component: (
<Homepage
lasuiteApiUrl={import.meta.env.VITE_LASUITE_API_URL}
entity="Gouvernement"
tagline={tagline}
serviceName={name}
serviceId={id}
logo={`/logos/${id}.svg`}
homepageUrl="/"
footerOptions={{
description: "Un service de la Direction interministérielle du numérique",
sitemapUrl: "/sitemap",
a11yUrl: "/accessibilite",
a11yLevel: "non compliant",
termsUrl: "/mentions-legales",
privacyUrl: "/donnees-personnelles",
}}
>
{content || <Proconnect url="#" />}
</Homepage>
),
}
}
const routes = [
serviceHomepage({
id: "resana",
content: <EmailOrProconnect proconnectUrl="#" />,
}),
serviceHomepage({
id: "messagerie",
content: <Email />,
}),
serviceHomepage({
id: "tchap",
content: <Email />,
}),
serviceHomepage({
id: "france-transfert",
content: (
<div className="fr-p-4w">
<h2>Contenu personnalisable</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis beatae, quia eius
voluptatem repudiandae quisquam a magnam obcaecati labore dolor ad vitae, omnis iusto
deleniti error eveniet maxime! Consectetur, sint?
</p>
</div>
),
}),
serviceHomepage({
id: "equipes",
}),
{
path: "/gaufre",
label: "Gaufre",
component: (
<div
style={{
margin: "2rem auto",
width: 800,
border: "1px solid #999",
padding: "2rem",
borderRadius: "0.5rem",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<h1>Test de la Gaufre</h1>
<Gaufre />
</div>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac diam a libero posuere
ornare facilisis in mi. Nullam eu vulputate augue, in auctor nibh. Praesent ac tempus dui.
Integer vel enim non purus facilisis mattis et vel dolor. Aliquam lacinia elit et massa
faucibus, at dictum risus ornare. Vivamus ultricies magna et gravida consequat. Donec ac
odio finibus, lobortis purus vel, consequat purus. Maecenas convallis vel enim eu
malesuada. Vestibulum elementum maximus massa, a porta erat congue quis. Nunc neque quam,
euismod et malesuada in, bibendum ac ex. Phasellus felis elit, egestas a convallis nec,
malesuada a est. Donec ac urna venenatis lorem aliquet rhoncus in accumsan ipsum.
</p>
<p>
Interdum et malesuada fames ac ante ipsum primis in faucibus. Morbi sed augue elementum,
tempus diam in, euismod purus. Fusce interdum, leo nec blandit eleifend, sapien ligula
egestas quam, quis aliquam ex turpis ut augue. Nullam a neque consectetur, feugiat eros a,
lacinia tortor. Proin imperdiet vehicula justo, eget bibendum tortor gravida a.
Pellentesque sit amet fermentum urna. Ut rutrum eros a ligula dapibus pharetra. In
porttitor arcu in euismod dictum. Aenean vestibulum mi et dignissim rutrum. Phasellus
ultrices ex justo, eu tincidunt metus efficitur non. Curabitur ac lorem ornare, aliquet
neque et, tristique elit. Donec quis turpis sodales, interdum massa fermentum, dictum
magna.
</p>
<p>
Mauris elit risus, facilisis at magna quis, interdum tempor nulla. Ut ac erat eget tellus
ultricies semper. Ut at dictum ante. Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Nam placerat lacinia eros ac convallis. Sed ultricies lectus et pharetra aliquet.
Vestibulum feugiat pulvinar fermentum. Vivamus imperdiet dapibus ornare. Donec venenatis,
lectus id faucibus tempus, sapien urna molestie augue, at egestas enim lectus quis nisi.
</p>
</div>
),
},
]
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<select
style={{
position: "fixed",
opacity: "0.75",
bottom: 5,
left: 5,
zIndex: 10000,
fontSize: "0.75rem",
backgroundColor: "pink",
}}
onChange={(e) => navigate(e.target.value)}
>
{routes.map(
(route) =>
route && (
<option key={route.path} value={route.path}>
{route.label}
</option>
),
)}
</select>
<Switch>
<Route path="/">
<style>{`
body {
font-family: sans;
margin: 1rem auto;
max-width: 800px;
}
`}</style>
<h1>Appli de développement des templates de La Suite.</h1>
<ul>
{routes.map(
(route) =>
route && (
<li key={route.path}>
<Link href={route.path}>{route.label}</Link>
</li>
),
)}
</ul>
</Route>
{routes.map(
(route) =>
route && (
<Route key={route.path} path={route.path}>
{route.component}
</Route>
),
)}
{/* Default route in a switch */}
<Route>Page introuvable</Route>
</Switch>
</React.StrictMode>,
)

View File

@@ -0,0 +1,69 @@
// this tsx file is not actually exposed in dist/. It's used to generate the HTML templates from our React components
// dotenv is used instead of vite import.meta.env because this file is processed by import-single-ts
import "dotenv/config"
import { Homepage } from "./components/Homepage"
import { Email } from "./components/Homepage/Email"
import { EmailOrProconnect } from "./components/Homepage/EmailOrProconnect"
import { Proconnect } from "./components/Homepage/Proconnect"
import { Header } from "./components/Header"
import { Gaufre } from "./components/Gaufre"
const customContent = (
<div className="fr-p-4w">
<h2>~~replace~~</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis beatae, quia eius
voluptatem repudiandae quisquam a magnam obcaecati labore dolor ad vitae, omnis iusto deleniti
error eveniet maxime! Consectetur, sint?
</p>
</div>
)
const templates = [
{
name: "homepage",
render: (
<Homepage
lasuiteApiUrl={process.env.VITE_LASUITE_API_URL}
entity="~~replace~~"
tagline="**Service**, un outil sécurisé <br>pour les agents de l'État"
serviceName="~~replace~~"
logo="~~replace~~"
homepageUrl="/"
footerOptions={{
description: "Un service de la Direction interministérielle du numérique",
sitemapUrl: "/sitemap",
a11yUrl: "/accessibilite",
a11yLevel: "non compliant",
termsUrl: "/mentions-legales",
privacyUrl: "/donnees-personnelles",
}}
>
{customContent}
</Homepage>
),
},
{
name: "gaufre",
render: <Gaufre />,
},
{
name: "email",
render: <Email action="~~replace~~" />,
},
{
name: "email-or-proconnect",
render: <EmailOrProconnect proconnectUrl="~~replace~~" emailForm={{ action: "~~replace~~" }} />,
},
{
name: "proconnect",
render: <Proconnect url="~~replace~~" />,
},
{
name: "header",
render: <Header entity="~~replace~~" serviceName="~~replace~~" logo="~~replace~~" />,
},
]
export { templates }

View File

@@ -0,0 +1,42 @@
{
"email": {
"placeholder": "Entrez votre adresse e-mail",
"srLabel": "Adresse e-mail",
"submit": "Suivant",
"title": "Connexion avec votre adresse e-mail"
},
"footer": {
"homepageLinkTitle": {
"withoutService": "Retour à l'accueil",
"withService": "Retour à l'accueil - {serviceName}"
},
"license": "Sauf mention explicite de propriété intellectuelle détenue par des tiers, les contenus de ce site sont proposés sous {license}",
"licenseEtalab": "licence etalab-2.0",
"links": {
"a11y": {
"perfect": "Accessibilité : conforme",
"partial": "Accessibilité : partiellement conforme",
"bad": "Accessibilité : non conforme"
},
"privacy": "Données personnelles",
"sitemap": "Plan du site",
"terms": "Mentions légales"
}
},
"gaufre": {
"label": "Les services de La Suite numérique"
},
"header": {
"homepageLinkTitle": "Accueil - {serviceName}"
},
"links": {
"newWindow": "{title} - nouvelle fenêtre"
},
"proconnect": {
"help": "Qu'est-ce que ProConnect ?",
"loginWith": "Sidentifier avec",
"loginWithProconnect": "{loginWith} {proconnect}",
"or": "ou",
"title": "Connexion avec ProConnect"
}
}

View File

@@ -0,0 +1,59 @@
import { createContext, useContext, Fragment, type ReactNode } from "react"
import { get } from "../utils/get"
import frTranslations from "./fr.json" with { type: "json" }
type Translations = Record<string, any>
const TranslationsContext = createContext<Translations>(frTranslations)
function LaSuiteTranslationsProvider({
translations,
children,
}: {
translations: Translations
children: ReactNode
}) {
return (
<TranslationsContext.Provider value={translations}>{children}</TranslationsContext.Provider>
)
}
function useTranslate() {
const context = useContext(TranslationsContext)
const translations = context || frTranslations
return {
t: function t<T = string>(id: string, params: Record<string, string | ReactNode> = {}): T {
const translation = get(translations, id) as string
if (!translation) {
console.warn(`Translation for key "${id}" not found`)
return id as T
}
if (params) {
const componentKeys: string[] = []
Object.keys(params).forEach((key) => {
if (typeof params[key] !== "string") {
componentKeys.push(key)
}
})
if (!componentKeys.length) {
// no component keys, just replace {key} with value
return translation.replace(/{(\w+)}/g, (_, key) => params[key] as string) as T
}
// we have component keys: render react components
const parts = translation.split(/{(.*?)}/)
return (
<>
{parts.map((part, i) => {
if (part in params) {
return <Fragment key={i}>{params[part]}</Fragment>
}
return part
})}
</>
) as T
}
return translation as T
},
}
}
export { LaSuiteTranslationsProvider, useTranslate }

View File

@@ -0,0 +1,12 @@
export { EmailForm } from "./components/EmailForm"
export { Footer } from "./components/Footer"
export { Gaufre } from "./components/Gaufre"
export { Header } from "./components/Header"
export { Homepage } from "./components/Homepage"
export { HomepageContent } from "./components/HomepageContent"
export { ProconnectButton } from "./components/ProconnectButton"
export { Email as HomepageEmail } from "./components/Homepage/Email"
export { EmailOrProconnect as HomepageEmailOrProconnect } from "./components/Homepage/EmailOrProconnect"
export { Proconnect as HomepageProconnect } from "./components/Homepage/Proconnect"
export { LaSuiteTranslationsProvider, useTranslate } from "./i18n/useTranslate"
export { default as frTranslations } from "./i18n/fr.json"

View File

@@ -0,0 +1,3 @@
body {
margin: 0;
}

View File

@@ -0,0 +1,6 @@
/* import with specific path so that our patch-package works (we remove all the font-face references to have a smaller build) */
@import "@gouvfr/dsfr/dist/dsfr.css";
.fr-icon-arrow-right-line::before,
.fr-icon-arrow-right-line::after {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath d='m16.172 11-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2h12.172Z'/%3E%3C/svg%3E");
}

View File

@@ -0,0 +1,23 @@
@font-face {
font-family: Marianne;
src: url(../assets/Marianne-Regular.woff2) format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Marianne;
src: url(../assets/Marianne-Medium.woff2) format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Marianne;
src: url(../assets/Marianne-Bold.woff2) format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}

View File

@@ -0,0 +1,72 @@
/**
* This is the DSFR-like styling for the Gaufre button.
* Styles are marked important to ensure they override correctly any other global styles.
*
* If they happen to be a nuisance, you can totally decide to *not* use this CSS and implement your own.
*/
/* this mask-image rule is set on additional classes, in case you don't want to apply
* the "lasuite-gaufre-btn--vanilla" class to your button, but still want to easily
* apply the mask on your own custom button.
*/
.lasuite-gaufre-btn--vanilla::before,
.lasuite-gaufre-btn--vanilla::after,
.lasuite-gaufre-mask-element,
.lasuite-gaufre-mask::before,
.lasuite-gaufre-mask::after {
mask-image: url("data:image/svg+xml,%3Csvg width%3D%2224%22 height%3D%2224%22 viewBox%3D%220 0 24 24%22 xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E %3Cpath d%3D%22m11.931261 8.1750088 3.362701 1.9413282v3.882529l-3.362701 1.941262-3.3627003-1.941262v-3.882529zm3.785275-6.8155706 3.362701 1.9412995v3.8825496l-3.362701 1.9412783-3.362701-1.9412783V3.3007377Zm0 13.5159968 3.362701 1.941263v3.882529l-3.362701 1.941335-3.362701-1.941335v-3.882529ZM4.3627012 8.1750088l3.3627014 1.9413282v3.882529l-3.3627014 1.941262L1 13.998866v-3.882529Zm3.7841385-6.8155706 3.3627023 1.9412995v3.8825496L8.1468397 9.1245656 4.7841172 7.1832873V3.3007377Zm0 13.5159968 3.3627023 1.941263v3.882529l-3.3627023 1.941335-3.3627225-1.941335v-3.882529ZM19.637299 8.1750088 23 10.116337v3.882529l-3.362701 1.941262-3.362702-1.941262v-3.882529z%22%2F%3E%3C%2Fsvg%3E") !important;
}
.lasuite-gaufre-btn--vanilla {
all: unset;
overflow-wrap: break-word !important;
box-sizing: border-box !important;
appearance: none !important;
border: none !important;
margin: 0 !important;
font-family: inherit !important;
cursor: pointer !important;
-webkit-tap-highlight-color: transparent !important;
display: inline-flex !important;
flex-direction: row !important;
align-items: center !important;
width: -moz-fit-content !important;
width: fit-content !important;
font-weight: 500 !important;
font-size: 1.125rem !important;
line-height: 1.75rem !important;
min-height: 3rem !important;
padding: 0.5rem 1.5rem !important;
background-color: transparent !important;
color: #000091 !important;
box-shadow: inset 0 0 0 1px #000091 !important;
overflow: hidden !important;
white-space: nowrap !important;
max-width: 3rem !important;
max-height: 3rem !important;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
.lasuite-gaufre-btn--vanilla::before {
content: "" !important;
flex: 0 0 auto !important;
display: block !important;
background-color: currentColor !important;
width: var(--icon-size) !important;
height: var(--icon-size) !important;
-webkit-mask-size: 100% 100% !important;
mask-size: 100% 100% !important;
margin-left: 0 !important;
margin-right: 0.5rem !important;
--icon-size: 2rem !important;
}
.lasuite-gaufre-btn--vanilla:hover,
.lasuite-gaufre-btn--vanilla:focus-visible {
background-color: #f6f6f6 !important;
}
.lasuite-gaufre-btn--vanilla:active {
background-color: #ededed !important;
}

View File

@@ -0,0 +1,4 @@
@import "./fonts.css";
@import "./dsfr.css";
@import "./homepage.css";
@import "./gaufre.css";

View File

@@ -0,0 +1,2 @@
@import "./homepage.css";
@import "./gaufre.css";

View File

@@ -0,0 +1,260 @@
:root {
--lasuite-primary: #000091;
}
@media (min-width: 78em) {
.lasuite-container,
.fr-container.lasuite-container {
max-width: 120rem;
}
}
.lasuite-homepage {
height: 100dvh;
display: flex;
flex-direction: column;
border-top: 5px solid #e1000f;
}
.lasuite-text-center {
text-align: center;
}
.lasuite-header__brand {
filter: none;
}
.lasuite-header__brand-top,
.lasuite-header__service {
width: fit-content;
}
.lasuite-header__service {
box-shadow: none;
padding-bottom: 0;
margin-left: 0;
}
.lasuite-header__service-title {
display: none;
margin: 0;
color: var(--lasuite-primary);
font-size: 1.25rem;
margin-left: 0.5rem;
}
@media screen and (min-width: 48em) {
.lasuite-header__service {
margin-left: 3.75rem;
}
.lasuite-header__service-title {
display: block;
font-size: 1.75rem;
margin-left: 1rem;
}
}
.lasuite-header__service-link {
display: flex;
align-items: center;
text-decoration: none;
}
.lasuite-header__service-logo {
width: 2.375rem;
height: 2.375rem;
}
.lasuite-header__tools-links {
display: block;
margin-right: 1.5rem;
}
@media screen and (min-width: 62em) {
.lasuite-header__tools-links {
margin-right: 0;
}
}
.lasuite-homepage__wrapper {
flex: 1;
background: #1c135e no-repeat center center;
background-image: linear-gradient(135deg, rgba(0, 0, 145, 0.6) 0%, rgba(225, 0, 15, 0.6) 100%);
background-size: cover;
position: relative;
}
/* background is set like this to profit from the <picture> element
* (the image-set css alternative has less browser support) */
.lasuite-homepage__bg img {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.lasuite-homepage__content {
position: relative;
z-index: 2;
}
.lasuite-homepage__tagline {
margin: 0;
font-weight: 400;
background-color: white;
color: var(--lasuite-primary);
margin-bottom: 1rem;
font-size: 1.375rem;
padding: 1rem 1.5rem;
}
.lasuite-homepage__tagline-strong {
font-weight: 700;
}
@media screen and (min-width: 36em) {
.lasuite-homepage__tagline {
width: fit-content;
}
}
@media screen and (min-width: 48em) {
.lasuite-homepage__tagline {
padding: 2rem;
font-size: 1.875rem;
}
.lasuite-homepage__tagline br {
display: block;
}
}
.lasuite-homepage__form-container {
display: flex;
}
.lasuite-homepage__form {
width: 100%;
}
.lasuite-homepage__form-inner {
background: white;
margin: auto;
max-width: 33rem;
padding: 4rem 1.5rem 3rem;
/* fr-px-4w fr-pt-8w fr-pb-6w fr-px-md-6w */
}
@media screen and (min-width: 48em) {
.lasuite-homepage__form-inner {
margin-left: auto;
margin-right: 1.5rem;
padding-left: 3rem;
padding-right: 3rem;
}
}
@media screen and (min-width: 90em) {
.lasuite-homepage__form {
margin-right: 7rem;
}
}
.lasuite-btn {
justify-content: center;
text-transform: uppercase;
width: 100%;
line-height: 1.75rem;
padding: 1rem;
display: block;
}
.lasuite-input-width {
width: 100%;
max-width: 23rem;
margin-left: auto;
margin-right: auto;
}
@media screen and (min-width: 36em) {
.lasuite-btn {
font-size: 1.125rem;
padding: 1.125rem 1.5rem;
min-height: 3rem;
}
}
.lasuite-hr-or {
color: var(--lasuite-primary);
}
.lasuite-hr-or::before,
.lasuite-hr-or::after {
height: 2px;
background-color: var(--lasuite-primary);
}
.lasuite-connect__brand {
font-weight: 700;
}
.lasuite-connect :is(.fr-connect__brand, .fr-connect__login) {
line-height: inherit;
}
.lasuite-connect__icon {
width: 48px;
height: 52px;
}
.lasuite-connect__inner {
display: inline-flex;
align-items: center;
}
.lasuite-connect__text {
margin-left: 1rem;
display: flex;
flex-direction: column;
}
@media screen and (min-width: 36em) {
.lasuite-connect__icon {
width: 62px;
height: 67px;
}
.lasuite-connect__text {
margin-left: 2rem;
display: flex;
flex-direction: column;
}
}
.fr-input.lasuite-input {
background-color: white;
border-radius: 0;
border: 1px solid #959595;
box-shadow: none;
padding: 0.75rem 1rem;
max-height: none;
width: 100%;
}
.fr-input.lasuite-input::placeholder {
font-style: normal;
}
.lasuite-link:not(:hover) {
background-image: none;
}
@media screen and (min-width: 62em) {
.lasuite-py-lg-16w {
padding-top: 8rem !important;
padding-bottom: 8rem !important;
}
.lasuite-mt-lg-0 {
margin-top: 0 !important;
}
}

View File

@@ -0,0 +1,31 @@
// https://gist.github.com/andrewchilds/30a7fb18981d413260c7a36428ed13da?permalink_comment_id=4433741#gistcomment-4433741
type GetReturnType<T> = T | undefined
type ValueType = Record<string | number, unknown>
export const get = <T>(
value: unknown,
query: string | Array<string | number>,
defaultVal: GetReturnType<T> = undefined,
): GetReturnType<T> => {
const splitQuery = Array.isArray(query)
? query
: query
.replace(/(\[(\d)\])/g, ".$2")
.replace(/^\./, "")
.split(".")
if (!splitQuery.length || splitQuery[0] === undefined) return value as T
const key = splitQuery[0]
if (
typeof value !== "object" ||
value === null ||
!(key in value) ||
(value as ValueType)[key] === undefined
) {
return defaultVal
}
return get((value as ValueType)[key], splitQuery.slice(1), defaultVal)
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,40 @@
import { resolve } from "path"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
/*
* this config file takes care of building the react components
*
* see vite.css-config.ts for the css files (vite doesn't handle css files very well in lib mode so we generate them differently)
* see vite.html-config.ts for the html template files (they are generated from our react components)
*/
export const reactConfig = {
plugins: [
react({
babel: {
plugins: ["@babel/plugin-syntax-import-attributes"],
},
}),
],
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "index",
fileName: "index",
},
rollupOptions: {
external: ["react", "react-dom", "react/jsx-runtime"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
},
},
}
export default defineConfig({
...reactConfig,
plugins: [...reactConfig.plugins, dts({ rollupTypes: true })],
})

View File

@@ -0,0 +1,33 @@
import { resolve } from "path"
import { defineConfig } from "vite"
/*
* this config file takes care of building the css files
*
* it is meant to be run just after the react build, as it doesn't empty the dist/ folder
* it uses the postcss config in postcss.config.mjs
*
* see vite.config.ts for the react files
*/
export default defineConfig({
base: "",
build: {
emptyOutDir: false,
rollupOptions: {
input: {
"css-homepage": resolve(__dirname, "src/styles/homepage.css"),
"css-gaufre": resolve(__dirname, "src/styles/gaufre.css"),
"css-homepage-gaufre-dsfr": resolve(__dirname, "src/styles/homepage-gaufre-dsfr.css"),
"css-homepage-gaufre": resolve(__dirname, "src/styles/homepage-gaufre.css"),
},
output: {
assetFileNames: (arg) => {
if (arg.name?.startsWith("css-") && arg.name.endsWith(".css")) {
return `css/${arg.name.substring(4)}`
}
return `css/assets/[name][extname]`
},
},
},
},
})

View File

@@ -0,0 +1,65 @@
import { resolve, basename, join, dirname } from "path"
import { defineConfig } from "vite"
import { renderToStaticMarkup } from "react-dom/server"
import { importSingleTs } from "import-single-ts"
import * as prettier from "prettier"
import { reactConfig } from "./vite.config"
import { ReactNode } from "react"
/*
* this config file takes care of building the HTML files
*
* it is meant to be run just after the react build, as it doesn't empty the dist/ folder
* it works by checking src/html.tsx and generate static HTML files from each exported component
*
* see vite.config.ts for the react files
*/
export default defineConfig({
...reactConfig,
plugins: [...reactConfig.plugins, htmlFromComponents()],
build: {
...reactConfig.build,
emptyOutDir: false,
lib: {
entry: resolve(__dirname, "src/html.tsx"),
name: "lasuite-empty-html-bundle",
fileName: "lasuite-empty-html-bundle",
formats: ["es"],
},
},
})
function htmlFromComponents() {
const reactHtmlComponentRegex = /\.?html\.tsx$/
return {
name: "transform-file",
async transform(_, id) {
if (reactHtmlComponentRegex.test(id)) {
const component = await importSingleTs(id)
component.templates.forEach(async (template: { name: string; render: ReactNode }) => {
const html = await prettier.format(renderToStaticMarkup(template.render), {
parser: "html",
printWidth: 100,
tabWidth: 4,
bracketSameLine: false,
htmlWhitespaceSensitivity: "ignore",
})
this.emitFile({
type: "asset",
fileName: `html/${template.name}.html`,
source: html,
})
})
return {
code: "",
map: null,
}
}
},
// in the end, don't generate the js bundle, it's empty, we only want the html files
generateBundle(_, bundle) {
delete bundle["lasuite-empty-html-bundle.js"]
},
}
}

1
website/.env.example Normal file
View File

@@ -0,0 +1 @@
PUBLIC_LASUITE_API_URL=https://integration.lasuite.numerique.gouv.fr

1
website/.prettierignore Normal file
View File

@@ -0,0 +1 @@
dist/

6
website/.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"plugins": ["prettier-plugin-astro"],
"semi": false,
"printWidth": 100,
"proseWrap": "always"
}

4
website/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
website/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

41
website/README.md Normal file
View File

@@ -0,0 +1,41 @@
# lasuite-integration docs website and API
This folder is the source of the La Suite technical center website. It has two purposes:
- documenting how to use the @gouvfr-lasuite/integration npm package
- serving a couple of API endpoints that help developers of _La Suite_ services integrate common
things
This is a classic Astro app with a Starlight template. Besides the static documentation, generated
through astro content collections, there are a couple notable things:
- the API is in `pages/api`. We also serve static images under the `public/api` folder that are
called by La Suite services,
- we have scripts to generate the rolling homepage backgrounds in `bin/`. Every two week the website
is generated again with new backgrounds located in `src/assets/backgrounds`. Doing that allows
every service to just call a static endpoint for the background.
## Development
This is a starlight-based Astro app. Follow the official docs if more info is needed.
```sh
npm install
cp .env.example .env
npm run dev
```
### Background photos of homepages
The background image of the service homepages are served through an API exposed by the website.
Source images are not tracked in the repo. To build images, you must:
- put images in `src/assets/backgrounds/sources`
- run `node ./bin/transform-source-backgrounds.mjs`: this takes every image in the sources dir and
for every image: it applies a linear gradient, resizes the image to 1920x1200 and saves the result
in both avif and jpeg formats in `src/assets/backgrounds`
- then you can run `node ./bin/build-services-backgrounds.mjs`: it regenerates the service-related
backgrounds in `public/api/backgrounds`. Depending on the offset passed, or the current week of
the month, the service gets a different background. This is meant to be run in a cronjob to
generate different backgrounds a few times a month.

84
website/astro.config.mjs Normal file
View File

@@ -0,0 +1,84 @@
import { defineConfig } from "astro/config"
import react from "@astrojs/react"
import starlight from "@astrojs/starlight"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import remarkTextr from "remark-textr"
// https://astro.build/config
export default defineConfig({
compressHTML: false,
devToolbar: {
enabled: false,
},
markdown: {
shikiConfig: {
// Choose from Shiki's built-in themes (or add your own)
// https://shiki.style/themes
themes: {
light: "github-light",
dark: "github-dark",
},
},
rehypePlugins: [
rehypeHeadingIds,
[
rehypeAutolinkHeadings,
{
// Wrap the heading text in a link.
behavior: "wrap",
},
],
],
remarkPlugins: [
[
remarkTextr,
{
plugins: [frenchPunctuation],
options: {
locale: "fr",
},
},
],
],
},
integrations: [
react(),
starlight({
title: "La Suite Integrations",
social: {
github: "https://github.com/withastro/starlight",
},
defaultLocale: "root",
locales: {
root: {
label: "Français",
lang: "fr",
},
},
sidebar: [
{
label: "Guide",
autogenerate: {
directory: "guides",
},
},
{
label: "Référence",
autogenerate: {
directory: "reference",
},
},
],
customCss: ["./src/styles/global.css"],
expressiveCode: {
themes: ["github-dark", "github-light"],
},
}),
],
})
// replace all occurences of " :", " !", " ?", " ; " with a non-breaking space
function frenchPunctuation(input) {
return input.replace(/ (\?|\!|:|;)(\s|$)/gim, "\u202F$1$2")
}

View File

@@ -0,0 +1,39 @@
import fs from "fs"
import path from "path"
import { promisify } from "util"
const copyFile = promisify(fs.copyFile)
import services from "../src/data/services.json" with { type: "json" }
const args = process.argv.slice(2)
const weekOffset = args[0] && !isNaN(args[0]) ? args[0] * 1 : Math.floor(new Date().getDate() / 7)
const backgroundsDir = path.join(import.meta.dirname, "..", "src", "assets", "backgrounds")
const outputDir = path.join(import.meta.dirname, "..", "public", "api", "backgrounds", "v1")
async function buildStaticBackgrounds() {
try {
console.log(`Building backgrounds with offset ${weekOffset}`)
services.forEach(async (service, i) => {
;[".avif", ".jpg"].forEach(async (ext) => {
const srcPath = path.join(backgroundsDir, `${weekOffset + i}${ext}`)
const destPath = path.join(outputDir, `${service.id}${ext}`)
await copyFile(srcPath, destPath)
console.log(`Copied ${getFilename(srcPath)} to ${getFilename(destPath)}`)
if (i === 0) {
await copyFile(srcPath, path.join(outputDir, `default${ext}`))
console.log(`Copied ${getFilename(srcPath)} to default${ext}`)
}
})
})
console.log("Backgrounds have been successfully built.")
} catch (error) {
console.error("Error building static backgrounds:", error)
}
}
function getFilename(path) {
return path.split("/").pop()
}
buildStaticBackgrounds()

View File

@@ -0,0 +1,49 @@
import fs from "fs"
import path from "path"
import { promisify } from "util"
import sharp from "sharp"
import svgGradient from "svg-gradient"
const readdir = promisify(fs.readdir)
const sourcesDir = path.join(import.meta.dirname, "..", "src", "assets", "backgrounds", "sources")
const outputDir = path.join(import.meta.dirname, "..", "src", "assets", "backgrounds")
async function resizeSourceBackgrounds() {
try {
console.log(`Resizing source background files…`)
const gradient = await sharp(
Buffer.from(
svgGradient(`linear-gradient(135deg, rgba(0, 0, 145, 0.6) 0%, rgba(225, 0, 15, 0.6) 100%)`),
),
)
.resize(1920, 1200, { fit: "cover" })
.toBuffer()
const backgrounds = await readdir(sourcesDir)
backgrounds.forEach((backgroundFile, i) => {
const srcPath = path.join(sourcesDir, backgroundFile)
const jpegPath = path.join(outputDir, `${i}.jpg`)
const avifPath = path.join(outputDir, `${i}.avif`)
const image = sharp(srcPath)
.resize(1920, 1200, { fit: "cover" })
.composite([{ input: gradient }])
image.toFile(jpegPath).then(() => {
console.log(`Resized ${getFilename(backgroundFile)} to ${getFilename(jpegPath)}`)
})
image.toFile(avifPath).then(() => {
console.log(`Resized ${getFilename(backgroundFile)} to ${getFilename(avifPath)}`)
})
})
} catch (error) {
console.error("Error:", error)
}
}
function getFilename(path) {
return path.split("/").pop()
}
resizeSourceBackgrounds()

8947
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
website/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "lasuite-integration-website",
"private": true,
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build-backgrounds": "node ./bin/build-services-backgrounds.mjs",
"build": "astro check && npm run build-backgrounds && astro build",
"copy-logos": "cp -r ./node_modules/@gouvfr-lasuite/integration/dist/logos/* ./src/assets/logos",
"preview": "astro preview",
"astro": "astro",
"postinstall": "npm run copy-logos"
},
"dependencies": {
"@astrojs/check": "^0.5.10",
"@astrojs/markdown-remark": "^5.1.0",
"@astrojs/react": "^3.3.1",
"@astrojs/starlight": "^0.21.5",
"@gouvfr-lasuite/integration": "file:../packages/integration",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.11",
"astro": "^4.3.5",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.3",
"react-syntax-highlighter": "^15.5.0",
"rehype-autolink-headings": "^7.1.0",
"remark-textr": "^6.1.0",
"sharp": "^0.33.3",
"svg-gradient": "^1.0.3",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,124 @@
;(function () {
const BUTTON_CLASS = "js-lasuite-gaufre-btn"
let lastFocusedButton = null
if ("requestIdleCallback" in window) {
requestIdleCallback(() => {
appendIframe()
})
}
document.body.addEventListener("click", (event) => {
if (!event.target.classList || !event.target.classList.contains(BUTTON_CLASS)) {
const buttons = document.querySelectorAll(`.${BUTTON_CLASS}`)
buttons.forEach((b) => b.classList.remove("lasuite--opened"))
hideIframe()
return
}
const button = event.target
button.classList.toggle("lasuite--opened")
if (button.classList.contains("lasuite--opened")) {
showIframe(button)
} else {
hideIframe()
}
})
document.addEventListener("keyup", (event) => {
if (event.key === "Escape") {
hideIframe()
}
})
window.addEventListener("message", (event) => {
if (event.data === "lasuite-close-services-iframe") {
hideIframe()
}
})
window.addEventListener("resize", () => {
const iframe = document.querySelector(`#lasuite-gaufre-iframe.lasuite--opened`)
if (!iframe) {
return
}
const button = document.querySelector(`.${BUTTON_CLASS}.lasuite--opened`)
if (!button) {
return
}
iframe.style.cssText = getIframePositionStyle(button)
})
const appendIframe = () => {
if (document.querySelector(`#lasuite-gaufre-iframe`)) {
return
}
const scriptTag = document.querySelector(`#lasuite-gaufre-script`)
if (!scriptTag) {
console.log(
"La Suite numérique: Gaufre script tag not found, please check out the documentation",
)
return
}
const iframe = document.createElement("iframe")
iframe.title = "Services de La Suite numérique"
iframe.id = "lasuite-gaufre-iframe"
iframe.width = "304"
iframe.height = "360"
iframe.style.cssText = "display: none !important"
const { host, protocol } = new URL(scriptTag.src)
const searchParams = new URLSearchParams(scriptTag.src)
const anct = searchParams.get("type") === "anct"
const lang = ["en"].includes(searchParams.get("lang")) ? searchParams.get("lang") : null
iframe.src = `${protocol}//${host}/api/v1/${(!!lang && `${lang}/`) || ""}gaufre${(!!anct && "?type=anct") || ""}`
document.body.appendChild(iframe)
}
const getIframePositionStyle = (button) => {
const buttonCoords = button.getBoundingClientRect()
const isSmallScreen = window.innerWidth <= 400
return `
position: absolute !important;
top: ${buttonCoords.top + buttonCoords.height + 8}px;
${
isSmallScreen
? `
left: 5px;
right: 5px;
margin: 0 auto;
`
: `
left: ${buttonCoords.right - 304 + document.documentElement.scrollLeft}px;`
}
border: 0 !important;
display: block !important;
z-index: 100000;
`
}
const showIframe = (button) => {
const iframe = document.querySelector(`#lasuite-gaufre-iframe`)
if (!iframe) {
appendIframe()
}
iframe.style.cssText = getIframePositionStyle(button)
iframe.classList.add("lasuite--opened")
lastFocusedButton = button
setTimeout(() => {
iframe.focus()
}, 0)
}
const hideIframe = () => {
const iframe = document.querySelector(`#lasuite-gaufre-iframe`)
if (iframe) {
iframe.style.cssText = "display: none !important"
iframe.classList.remove("lasuite--opened")
}
if (lastFocusedButton) {
lastFocusedButton.classList.remove("lasuite--opened")
lastFocusedButton.focus()
lastFocusedButton = null
}
}
})()

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

View File

@@ -0,0 +1,13 @@
We use a subset of Marianne font because we know we use just a few characters for our list. This
shrinks the font file size noticeably!
Example command to use to generate the subset with glyphhanger:
```
glyphhanger \
--subset="./Marianne-Regular.woff2" \
--formats=woff2 \
--whitelist="DeskLaSuiteNumériqueMessagerieTchapResanaFranceTransfertContactsGristLePadWebConférencedel'ÉtatWebinaire  "
```
Assuming you have the Marianne-Regular.woff2 file (you can take it from the DSFR).

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Some files were not shown because too many files have changed in this diff Show More