(frontend) integrate configurable Waffle

Integrate Waffle component based on LaGaufreV2
from @gouvfr-lasuite/ui-kit.
Waffle will be fully configurable via the app config,
allowing to be set through environment variables
and api-provided configuration.
This commit is contained in:
Anthony LC
2026-01-14 17:26:23 +01:00
parent b1231cea7c
commit 2f52dddc84
16 changed files with 188 additions and 84 deletions

View File

@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Added
- ✨(frontend) integrate configurable Waffle #1795
### Fixed ### Fixed
- ✅(e2e) fix e2e test for other browsers #1799 - ✅(e2e) fix e2e test for other browsers #1799

BIN
docs/assets/waffle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,4 +1,6 @@
# Runtime Theming 🎨 # Customization Guide 🛠
## Runtime Theming 🎨
### How to Use ### How to Use
@@ -32,7 +34,7 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
---- ----
# Runtime JavaScript Injection 🚀 ## Runtime JavaScript Injection 🚀
### How to Use ### How to Use
@@ -87,7 +89,7 @@ Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom J
---- ----
# **Your Docs icon** 📝 ## **Your Docs icon** 📝
You can add your own Docs icon in the header from the theme customization file. You can add your own Docs icon in the header from the theme customization file.
@@ -105,7 +107,7 @@ This configuration is optional. If not set, the default icon will be used.
---- ----
# **Footer Configuration** 📝 ## **Footer Configuration** 📝
The footer is configurable from the theme customization file. The footer is configurable from the theme customization file.
@@ -128,7 +130,7 @@ Below is a visual example of a configured footer ⬇️:
---- ----
# **Custom Translations** 📝 ## **Custom Translations** 📝
The translations can be partially overridden from the theme customization file. The translations can be partially overridden from the theme customization file.
@@ -141,3 +143,35 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
### Example of JSON ### Example of JSON
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
----
## **Waffle Configuration** 🧇
The Waffle (La Gaufre) is a widget that displays a grid of services.
![Waffle Configuration Example](./assets/waffle.png)
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Configuration
The Waffle can be configured in the theme customization file with the `waffle` key.
### Available Properties
See: [LaGaufreV2Props](https://github.com/suitenumerique/ui-kit/blob/main/src/components/la-gaufre/LaGaufreV2.tsx#L49)
### Complete Example
From the theme customization file: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
### Behavior
- If `data.services` is provided, the Waffle will display those services statically
- If no data is provided, services can be fetched dynamically from an API endpoint thanks to the `apiUrl` property

View File

@@ -59,45 +59,90 @@ test.describe('Header', () => {
).toBeVisible(); ).toBeVisible();
await expect(header.getByText('English')).toBeVisible(); await expect(header.getByText('English')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
}); });
test('checks La Gauffre interaction', async ({ page }) => { test('checks a custom waffle', async ({ page }) => {
await overrideConfig(page, { await overrideConfig(page, {
FRONTEND_THEME: 'dsfr', theme_customization: {
waffle: {
data: {
services: [
{
name: 'Docs E2E Custom 1',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
{
name: 'Docs E2E Custom 2',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
],
},
showMoreLimit: 9,
},
},
}); });
await page.goto('/'); await page.goto('/');
const header = page.locator('header').first(); const header = page.locator('header').first();
await expect( await expect(
header.getByRole('button', { header.getByRole('button', { name: 'Digital LaSuite services' }),
name: 'Les services de La Suite numérique',
}),
).toBeVisible(); ).toBeVisible();
/** /**
* La gaufre load a js file from a remote server, * The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', { name: 'Digital LaSuite services' })
.click();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
).toBeVisible();
});
test('checks the waffle dsfr', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
showMoreLimit: 9,
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available * it takes some time to load the file and have the interaction available
*/ */
await page.waitForTimeout(1500); await page.waitForTimeout(1500);
await header await header
.getByRole('button', { .getByRole('button', {
name: 'Les services de La Suite numérique', name: 'Digital LaSuite services',
}) })
.click(); .click();
await expect( await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
page.getByRole('link', { name: 'France Transfert' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
}); });
}); });
@@ -124,11 +169,6 @@ test.describe('Header mobile', () => {
await expect(header.getByLabel('Open the header menu')).toBeVisible(); await expect(header.getByLabel('Open the header menu')).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible(); await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
}); });
}); });

View File

@@ -113,9 +113,6 @@ test.describe('Home page', () => {
}); });
await expect(languageButton).toBeVisible(); await expect(languageButton).toBeVisible();
await expect(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect( await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }), header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible(); ).toBeVisible();

View File

@@ -26,7 +26,6 @@ const themeWhiteLabelLight = getUIKitThemesFromGlobals(whiteLabelGlobals, {
widthHeader: '', widthHeader: '',
widthFooter: '', widthFooter: '',
}, },
'la-gaufre': false,
'home-proconnect': false, 'home-proconnect': false,
icon: { icon: {
src: '/assets/icon-docs.svg', src: '/assets/icon-docs.svg',
@@ -64,7 +63,6 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
widthFooter: '220px', widthFooter: '220px',
alt: 'Gouvernement Logo', alt: 'Gouvernement Logo',
}, },
'la-gaufre': true,
'home-proconnect': true, 'home-proconnect': true,
icon: { icon: {
src: '/assets/icon-docs-dsfr.svg', src: '/assets/icon-docs-dsfr.svg',

View File

@@ -4,13 +4,14 @@ import { Resource } from 'i18next';
import { APIError, errorCauses, fetchAPI } from '@/api'; import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/'; import { Theme } from '@/cunningham/';
import { FooterType } from '@/features/footer'; import { FooterType } from '@/features/footer';
import { HeaderType } from '@/features/header'; import { HeaderType, WaffleType } from '@/features/header';
import { PostHogConf } from '@/services'; import { PostHogConf } from '@/services';
interface ThemeCustomization { interface ThemeCustomization {
footer?: FooterType; footer?: FooterType;
translations?: Resource; translations?: Resource;
header?: HeaderType; header?: HeaderType;
waffle?: WaffleType;
} }
export interface ConfigResponse { export interface ConfigResponse {

View File

@@ -5,7 +5,7 @@ describe('<useCunninghamTheme />', () => {
expect(useCunninghamTheme.getState().componentTokens.logo?.src).toBe(''); expect(useCunninghamTheme.getState().componentTokens.logo?.src).toBe('');
// Change theme // Change theme
useCunninghamTheme.getState().setTheme('dsfr-light'); useCunninghamTheme.getState().setTheme('dsfr');
const { componentTokens } = useCunninghamTheme.getState(); const { componentTokens } = useCunninghamTheme.getState();
const logo = componentTokens.logo; const logo = componentTokens.logo;

View File

@@ -893,7 +893,6 @@
--c--components--logo--alt: ; --c--components--logo--alt: ;
--c--components--logo--widthheader: ; --c--components--logo--widthheader: ;
--c--components--logo--widthfooter: ; --c--components--logo--widthfooter: ;
--c--components--la-gaufre: false;
--c--components--home-proconnect: false; --c--components--home-proconnect: false;
--c--components--icon--src: /assets/icon-docs.svg; --c--components--icon--src: /assets/icon-docs.svg;
--c--components--icon--width: 32px; --c--components--icon--width: 32px;
@@ -2594,7 +2593,6 @@
--c--components--logo--alt: gouvernement logo; --c--components--logo--alt: gouvernement logo;
--c--components--logo--widthHeader: 110px; --c--components--logo--widthHeader: 110px;
--c--components--logo--widthFooter: 220px; --c--components--logo--widthFooter: 220px;
--c--components--la-gaufre: true;
--c--components--home-proconnect: true; --c--components--home-proconnect: true;
--c--components--icon--src: /assets/icon-docs-dsfr.svg; --c--components--icon--src: /assets/icon-docs-dsfr.svg;
--c--components--icon--width: 32px; --c--components--icon--width: 32px;

View File

@@ -677,7 +677,6 @@ export const tokens = {
info: { 'background-color': '#D5E4F3', color: '#005BC0' }, info: { 'background-color': '#D5E4F3', color: '#005BC0' },
}, },
logo: { src: '', alt: '', widthHeader: '', widthFooter: '' }, logo: { src: '', alt: '', widthHeader: '', widthFooter: '' },
'la-gaufre': false,
'home-proconnect': false, 'home-proconnect': false,
icon: { src: '/assets/icon-docs.svg', width: '32px', height: 'auto' }, icon: { src: '/assets/icon-docs.svg', width: '32px', height: 'auto' },
favicon: { favicon: {
@@ -1973,7 +1972,6 @@ export const tokens = {
widthHeader: '110px', widthHeader: '110px',
widthFooter: '220px', widthFooter: '220px',
}, },
'la-gaufre': true,
'home-proconnect': true, 'home-proconnect': true,
icon: { icon: {
src: '/assets/icon-docs-dsfr.svg', src: '/assets/icon-docs-dsfr.svg',

View File

@@ -12,8 +12,8 @@ import { useResponsiveStore } from '@/stores';
import { HEADER_HEIGHT } from '../conf'; import { HEADER_HEIGHT } from '../conf';
import { ButtonTogglePanel } from './ButtonTogglePanel'; import { ButtonTogglePanel } from './ButtonTogglePanel';
import { LaGaufre } from './LaGaufre';
import { Title } from './Title'; import { Title } from './Title';
import { Waffle } from './Waffle';
export const Header = () => { export const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -85,7 +85,7 @@ export const Header = () => {
</StyledLink> </StyledLink>
{!isDesktop ? ( {!isDesktop ? (
<Box $direction="row" $gap={spacingsTokens['sm']}> <Box $direction="row" $gap={spacingsTokens['sm']}>
<LaGaufre /> <Waffle />
</Box> </Box>
) : ( ) : (
<Box <Box
@@ -96,7 +96,7 @@ export const Header = () => {
> >
<ButtonLogin /> <ButtonLogin />
<LanguagePicker /> <LanguagePicker />
<LaGaufre /> <Waffle />
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -1,39 +0,0 @@
import { Gaufre } from '@gouvfr-lasuite/integration';
import '@gouvfr-lasuite/integration/dist/css/gaufre.css';
import Script from 'next/script';
import React from 'react';
import { createGlobalStyle } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
const GaufreStyle = createGlobalStyle`
.lasuite-gaufre-btn{
box-shadow: inset 0 0 0 0 !important;
border-radius: var(--c--components--button--border-radius) !important;
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out) !important;
&:hover, &:focus-visible {
background: var(--c--contextuals--background--semantic--contextual--primary) !important;
}
color: var(--c--contextuals--content--semantic--brand--tertiary) !important;
}
`;
export const LaGaufre = () => {
const { componentTokens } = useCunninghamTheme();
if (!componentTokens['la-gaufre']) {
return null;
}
return (
<>
<Script
src="https://integration.lasuite.numerique.gouv.fr/api/v1/gaufre.js"
strategy="lazyOnload"
id="lasuite-gaufre-script"
/>
<GaufreStyle />
<Gaufre variant="small" />
</>
);
};

View File

@@ -0,0 +1,48 @@
import { LaGaufreV2, LaGaufreV2Props } from '@gouvfr-lasuite/ui-kit';
import React from 'react';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useConfig } from '@/core';
type WaffleAPIType = {
apiUrl: LaGaufreV2Props['apiUrl'];
data?: never;
};
type WaffleDataType = {
apiUrl?: never;
data?: LaGaufreV2Props['data'];
};
export type WaffleType = Omit<
LaGaufreV2Props,
'apiUrl' | 'data' | 'widgetPath'
> &
(WaffleAPIType | WaffleDataType) & {
widgetPath?: string;
};
const LaGaufreV2Fixed = LaGaufreV2 as React.ComponentType<WaffleType>;
export const Waffle = () => {
const { data: conf } = useConfig();
const waffleConfig = conf?.theme_customization?.waffle;
if (!waffleConfig?.apiUrl && !waffleConfig?.data) {
return null;
}
return (
<Box
$css={css`
& > div {
display: flex;
}
`}
>
<LaGaufreV2Fixed {...waffleConfig} />
</Box>
);
};

View File

@@ -1,4 +1,4 @@
export * from './ButtonTogglePanel'; export * from './ButtonTogglePanel';
export * from './Header'; export * from './Header';
export * from './LaGaufre'; export * from './Waffle';
export * from './Title'; export * from './Title';

View File

@@ -4,7 +4,7 @@ import { Box } from '@/components';
import { useConfig } from '@/core'; import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { ButtonTogglePanel, Title } from '@/features/header/'; import { ButtonTogglePanel, Title } from '@/features/header/';
import { LaGaufre } from '@/features/header/components/LaGaufre'; import { Waffle } from '@/features/header/components/Waffle';
import { LanguagePicker } from '@/features/language'; import { LanguagePicker } from '@/features/language';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
@@ -81,7 +81,7 @@ export const HomeHeader = () => {
{!isSmallMobile && ( {!isSmallMobile && (
<Box $direction="row" $gap="1rem" $align="center"> <Box $direction="row" $gap="1rem" $align="center">
<LanguagePicker /> <LanguagePicker />
<LaGaufre /> <Waffle />
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -139,5 +139,30 @@
"src": "/assets/icon-docs.svg", "src": "/assets/icon-docs.svg",
"width": "32px" "width": "32px"
} }
},
"waffle": {
"data": {
"services": [
{
"name": "Docs",
"url": "https://docs.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/docs.svg"
},
{
"name": "Visio",
"url": "https://visio.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/visio.svg"
},
{
"name": "Fichiers",
"url": "https://fichiers.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/fichiers.svg"
}
]
},
"showMoreLimit": 9
} }
} }