(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]
### Added
- ✨(frontend) integrate configurable Waffle #1795
### Fixed
- ✅(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
@@ -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
@@ -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.
@@ -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.
@@ -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.
@@ -140,4 +142,36 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
### 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();
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, {
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('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
header.getByRole('button', { name: 'Digital LaSuite services' }),
).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
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', {
name: 'Les services de La Suite numérique',
name: 'Digital LaSuite services',
})
.click();
await expect(
page.getByRole('link', { name: 'France Transfert' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Tchap' })).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.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(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -139,5 +139,30 @@
"src": "/assets/icon-docs.svg",
"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
}
}