(widgets) import widgets code from Messages and setup Docker workflow (#33)

This adds Gaufre v2 with source, documentation, examples and built artefacts.
Also includes the feedback widget from Messages.
This commit is contained in:
Sylvain Zimmer
2025-11-19 15:18:21 +01:00
committed by GitHub
parent 3b2f083d3f
commit 720ee9f4f0
53 changed files with 5375 additions and 4 deletions

View File

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

10
website/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM node:22-slim AS website-deps
WORKDIR /home/website/
RUN npm install -g npm@11.3.0 && npm cache clean -f
ARG DOCKER_USER
USER ${DOCKER_USER}
ENV npm_config_cache=/tmp/npm-cache

View File

@@ -21,7 +21,6 @@ This is a starlight-based Astro app. Follow the official docs if more info is ne
```sh
npm install
cp .env.example .env
npm run dev
```

View File

@@ -7,12 +7,12 @@
"node": "20"
},
"scripts": {
"dev": "astro dev",
"dev": "astro dev --port 8930 --host 0.0.0.0",
"start": "node ./server.mjs",
"build-backgrounds": "node ./bin/build-services-backgrounds.mjs",
"gaufre-glyphhanger-cmd": "node ./bin/gaufre-font-cmd.mjs",
"build": "astro check && npm run build-backgrounds && astro build",
"preview": "astro preview",
"preview": "astro preview --port 8930 --host 0.0.0.0",
"astro": "astro"
},
"dependencies": {

View File

@@ -0,0 +1 @@
{"success": true, "config": {"captcha": false, "submitUrl": "/error"}}

View File

@@ -0,0 +1 @@
{"success": true, "config": {"captcha": false}}

View File

@@ -0,0 +1,57 @@
{
"organization": {
"name": "Example Organization",
"type": "Public Administration",
"siret": "12345678901234"
},
"services": [
{
"id": 1,
"name": "Authentication Service",
"url": "https://example.com/auth",
"maturity": "stable",
"logo": "/widgets/demo/logos/auth.svg",
"subscribed": true
},
{
"id": 2,
"name": "Document Portal",
"url": "https://example.com/docs",
"maturity": "stable",
"logo": "/widgets/demo/logos/docs.svg",
"subscribed": true
},
{
"id": 3,
"name": "Payment Gateway",
"url": "https://example.com/payments",
"maturity": "stable",
"logo": "/widgets/demo/logos/payment.svg",
"subscribed": false
},
{
"id": 4,
"name": "Analytics Dashboard",
"url": "https://example.com/analytics",
"maturity": "beta",
"logo": "/widgets/demo/logos/analytics.svg",
"subscribed": true
},
{
"id": 5,
"name": "Notification Center",
"url": "https://example.com/notifications",
"maturity": "alpha",
"logo": "/widgets/demo/logos/notifications.svg",
"subscribed": false
},
{
"id": 6,
"name": "File Storage",
"url": "https://example.com/storage",
"maturity": "stable",
"logo": "/widgets/demo/logos/storage.svg",
"subscribed": true
}
]
}

View File

@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#ffc107"/>
<path d="M20 12L24 20L20 28L16 20L20 12Z" fill="white"/>
<path d="M20 16L22 20L20 24L18 20L20 16Z" fill="#ffc107"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#000091"/>
<path d="M20 12C15.58 12 12 15.58 12 20C12 24.42 15.58 28 20 28C24.42 28 28 24.42 28 20C28 15.58 24.42 12 20 12ZM20 26C16.69 26 14 23.31 14 20C14 16.69 16.69 14 20 14C23.31 14 26 16.69 26 20C26 23.31 23.31 26 20 26Z" fill="white"/>
<path d="M20 16C17.79 16 16 17.79 16 20C16 22.21 17.79 24 20 24C22.21 24 24 22.21 24 20C24 17.79 22.21 16 20 16Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

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">
<rect width="40" height="40" rx="8" fill="#28a745"/>
<path d="M12 14H28V26H12V14ZM14 16V24H26V16H14ZM16 18H24V20H16V18ZM16 21H20V22H16V21Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#17a2b8"/>
<path d="M20 12C15.58 12 12 15.58 12 20C12 24.42 15.58 28 20 28C24.42 28 28 24.42 28 20C28 15.58 24.42 12 20 12ZM20 26C16.69 26 14 23.31 14 20C14 16.69 16.69 14 20 14C23.31 14 26 16.69 26 20C26 23.31 23.31 26 20 26Z" fill="white"/>
<path d="M18 18H22V20H18V18ZM18 21H22V22H18V21ZM18 24H22V25H18V24Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#dc3545"/>
<path d="M20 12C15.58 12 12 15.58 12 20C12 24.42 15.58 28 20 28C24.42 28 28 24.42 28 20C28 15.58 24.42 12 20 12ZM20 26C16.69 26 14 23.31 14 20C14 16.69 16.69 14 20 14C23.31 14 26 16.69 26 20C26 23.31 23.31 26 20 26Z" fill="white"/>
<path d="M18 18H22V22H18V18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

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">
<rect width="40" height="40" rx="8" fill="#6f42c1"/>
<path d="M14 16H26V24H14V16ZM16 18V22H24V18H16ZM18 20H22V21H18V20Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
website/public/widgets/dist/loader.js vendored Normal file
View File

@@ -0,0 +1 @@
(function(){"use strict";const m='div{position:fixed;bottom:20px;right:20px;z-index:1000}button{width:60px;height:60px;border-radius:50%;background:#000091;border:none;cursor:pointer;box-shadow:0 4px 12px #0000914d;transition:transform .2s ease,box-shadow .2s ease,background-color .2s ease;display:flex;align-items:center;justify-content:center;padding:0;margin:0}svg{width:35px;height:35px}button:focus-visible{outline:3px solid #0a76f6;outline-offset:2px}button:hover{background:#1212ff;transform:scale(1.1);box-shadow:0 6px 16px #1212ff66}button:active{transform:scale(.95)}button.loading{background:#2323ff}button.loading:after{content:"";width:25px;height:25px;border:3px solid transparent;border-top:3px solid white;border-radius:50%;animation:spin 1s linear infinite}button.loading svg{display:none}button.opened svg{display:none}button.opened:after{content:"+";font-size:50px;color:#fff;height:60px;line-height:60px;font-family:Arial;transform:rotate(45deg)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (max-width: 420px){button{width:40px;height:40px}svg{width:25px;height:25px}div{right:15px;bottom:15px}button.opened:after{font-size:40px}}';function x(e,t,n){const i=`lasuite-widget-${e}-shadow`,a=document.getElementById(i);a&&a.remove();const o=document.createElement("div");o.id=i;const s=o.attachShadow({mode:"open"}),d=document.createElement("style");d.textContent=n;const u=document.createElement("div");return u.innerHTML=t,s.appendChild(d),s.appendChild(u),o}const v='<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1, 0, 0, 1, 0, 0)"><g stroke-width="0"></g><g stroke-linecap="round" stroke-linejoin="round"></g><g><path d="M7 9H17M7 13H17M21 20L17.6757 18.3378C17.4237 18.2118 17.2977 18.1488 17.1656 18.1044C17.0484 18.065 16.9277 18.0365 16.8052 18.0193C16.6672 18 16.5263 18 16.2446 18H6.2C5.07989 18 4.51984 18 4.09202 17.782C3.71569 17.5903 3.40973 17.2843 3.21799 16.908C3 16.4802 3 15.9201 3 14.8V7.2C3 6.07989 3 5.51984 3.21799 5.09202C3.40973 4.71569 3.71569 4.40973 4.09202 4.21799C4.51984 4 5.0799 4 6.2 4H17.8C18.9201 4 19.4802 4 19.908 4.21799C20.2843 4.40973 20.5903 4.71569 20.782 5.09202C21 5.51984 21 6.0799 21 7.2V20Z" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>',f="lasuite-widget",p=(e,t,n,i)=>document.dispatchEvent(new CustomEvent(`${f}-${e}-${t}`,n?{detail:n}:void 0)),r=(e,t,n,i,a)=>{const o=d=>a(d.detail),s=e?`${f}-${e}-${t}`:t;return document.addEventListener(s,o,i?{once:!0}:void 0),()=>document.removeEventListener(s,o,i?{once:!0}:void 0)},C=1,l=2,c=e=>window._lasuite_widget?._loaded?.[e],h=(e,t)=>{window._lasuite_widget?._loaded&&(window._lasuite_widget._loaded[e]=t)},_=e=>{window._lasuite_widget||(window._lasuite_widget=[]);const t=window._lasuite_widget;if(t._loaded||(t._loaded={}),c(e)!==l){t.push=(...n)=>{for(const i of n)c(i[0])===l?p(i[0],i[1],i[2]):t[t.length]=i;return t.length},h(e,l);for(const n of t.splice(0,t.length))t.push(n)}p(e,"loaded")},E=(e,t="")=>{const n=document.createElement("script");n.src=e,n.type=t,n.defer=!0,document.body.appendChild(n)},g="loader";r(g,"init",null,!1,e=>{const t=e.widget||"feedback",n=`<div><button type="button">${v}</button></div>`,i=x(g,n,m),o=i.shadowRoot.querySelector("button"),s=()=>{o.setAttribute("aria-label",String(e.closeLabel||"Close widget")),o.setAttribute("aria-expanded","true")},d=()=>{o.setAttribute("aria-label",String(e.label||"Load widget")),o.setAttribute("aria-expanded","false")};d(),r(t,"closed",null,!1,()=>{o.classList.remove("opened"),d()}),r(t,"opened",null,!1,()=>{o.classList.add("opened"),s()}),o.addEventListener("click",()=>{if(o.classList.contains("opened")){p(t,"close");return}const u=setTimeout(()=>{o.classList.remove("loading")},1e4);o.classList.add("loading");const w=()=>{clearTimeout(u),o.classList.remove("loading");const b=Object.assign({},e.params);b.bottomOffset=o.offsetHeight+20,window._lasuite_widget.push([t,"init",b])};c(t)===l?w():(r(t,"loaded",null,!0,w),c(t)||(E(e.script,e.scriptType||""),h(t,C)))}),document.body.appendChild(i)}),_(g)})();

View File

@@ -0,0 +1,64 @@
---
title: Retours utilisateurs
sidebar:
order: 50
---
<div style="width:300px;margin:auto;">
![](./feedback.png)
</div>
Ce widget permet aux visiteurs de votre service d'envoyer des questions ou des retours à votre équipe.
Ces retours sont ensuite consultables dans une instance de [Messages](https://github.com/suitenumerique/messages),
qui permet d'y répondre collaborativement par email.
## Intégration
Il y a deux façons principales d'intégrer ce widget dans votre service:
### 1. Via le composant React
Ce composant sera bientôt disponible dans le [UI Kit](https://github.com/suitenumerique/ui-kit).
En attendant, vous pouvez :
* soit [copier ce composant](https://github.com/suitenumerique/messages/blob/main/src/frontend/src/features/ui/components/feedback-widget/index.tsx)
si vous souhaitez intégrer la pop-in de retour avec son bouton de chargement qui s'affiche en bas à droite des pages,
* soit [copier ce composant](https://github.com/suitenumerique/messages/blob/main/src/frontend/src/features/ui/components/feedback-button/index.tsx)
si vous souhaitez juste ouvrir la pop-in de retour dynamiquement, avec un bouton existant de votre application par exemple.
### 2. Via le script JavaScript
Cette option est à privilégier si vous n'utilisez pas React ou si vous ne souhaitez pas intégrer l'UI Kit dans votre application.
Voici comment l'intégrer avec le bouton de chargement :
```html
<script src="https://cdn.votresuite.fr/loader.js" async></script>
<script>
window._lasuite_widget = window._lasuite_widget || [];
_lasuite_widget.push(["loader", "init", {
"params": {
"api": "https://votre.instance.de.messages/api/",
"channel": "xyz-xyz-xyz"
},
"script": "https://cdn.votresuite.fr/feedback.js",
"widget": "feedback",
"label": "Poser une question"
}]);
</script>
```
D'autres exemples d'intégration sont disponibles ci-dessous.
## Exemples
Nous avons une documentation interactive avec plusieurs exemples de configuration pour :
* [version avec bouton de chargement](/widgets-demo/loader).
* [version sans bouton de chargement](/widgets-demo/feedback).
Vous pouvez aussi tester La Gaufre v2 en ligne sur ces services :
* [Suite territoriale - site vitrine](https://suiteterritoriale.anct.gouv.fr/)
* [Suite territoriale - Messages](https://messages.suite.anct.gouv.fr)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,87 @@
---
title: La Gaufre v2
sidebar:
order: 40
---
<div style="width:400px;margin:auto;">
![](./gaufre-v2-dinum.png)
</div>
La Gaufre v2 est une évolution de la [première version](/guides/gaufre) historique, qui ajoute plusieurs fonctionnalités :
* Un design en accord avec le [UI Kit](https://github.com/suitenumerique/ui-kit) de LaSuite.
* Un chargement des services à afficher en JSON, qui peut être effectué via une API dynamique ou fichier statique.
* L'utilisation du "Shadow DOM" qui permet aux styles du widget d'être indépendants de ceux de la page
En configurant la Gaufre v2, il est possible de changer son contenu ou même son apparence.
Voici par exemple une Gaufre pour la Suite territoriale :
<div style="width:400px;margin:30px auto;">
![](./gaufre-v2-anct.png)
</div>
## Intégration
Il y a deux façons principales d'intégrer La Gaufre v2 dans votre service:
### 1. Via le composant React
Ce composant, qui intègre le bouton "Gaufre" ainsi que la pop-in, sera bientôt disponible dans le [UI Kit](https://github.com/suitenumerique/ui-kit).
En attendant, vous pouvez [copier ce composant](https://github.com/suitenumerique/messages/blob/main/src/frontend/src/features/ui/components/lagaufre/index.tsx).
### 2. Via le script JavaScript
Cette option est à privilégier si vous n'utilisez pas React ou si vous ne souhaitez pas intégrer l'UI Kit dans votre application.
Dans ce cas, vous devez intégrer le bouton "Gaufre". Le script gérera la pop-in et s'occupera de mettre à jour les attributs nécessaires sur le bouton lors des interactions.
Voici un exemple complet d'intégration dans ce cas :
```html
<button type="button" id="gaufre_button" aria-label="Ouvrir la Gaufre" aria-expanded="false">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path fill="#000091" id="square" d="M2.7959 0.5C3.26483 0.5 3.49956 0.49985 3.68848 0.564453C4.03934 0.684581 4.31542 0.960658 4.43555 1.31152C4.50015 1.50044 4.5 1.73517 4.5 2.2041V2.7959C4.5 3.26483 4.50015 3.49956 4.43555 3.68848C4.31542 4.03934 4.03934 4.31542 3.68848 4.43555C3.49956 4.50015 3.26483 4.5 2.7959 4.5H2.2041C1.73517 4.5 1.50044 4.50015 1.31152 4.43555C0.960658 4.31542 0.684581 4.03934 0.564453 3.68848C0.49985 3.49956 0.5 3.26483 0.5 2.7959V2.2041C0.5 1.73517 0.49985 1.50044 0.564453 1.31152C0.684581 0.960658 0.960658 0.684581 1.31152 0.564453C1.50044 0.49985 1.73517 0.5 2.2041 0.5H2.7959Z" />
</defs>
<use href="#square" transform="translate(0, 0)"/><use href="#square" transform="translate(6.5, 0)"/><use href="#square" transform="translate(13, 0)"/><use href="#square" transform="translate(0, 6.5)"/><use href="#square" transform="translate(6.5, 6.5)"/><use href="#square" transform="translate(13, 6.5)"/><use href="#square" transform="translate(0, 13)"/><use href="#square" transform="translate(6.5, 13)"/><use href="#square" transform="translate(13, 13)"/>
</svg>
</button>
<script src="https://cdn.votresuite.fr/lagaufre.js" async></script>
<script>
const button = document.getElementById("gaufre_button");
window._lasuite_widget = window._lasuite_widget || [];
_lasuite_widget.push(['lagaufre', 'init', {
api: 'https://lasuite.numerique.gouv.fr/api/services',
label: "Services de la Suite numérique",
closeLabel: "Fermer le menu",
headerLabel: "À propos",
loadingText: "Chargement…"
newWindowLabelSuffix: " (nouvelle fenêtre)",
buttonElement: button,
position: () => {
return {
position: "absolute",
top: button.offsetTop + button.offsetHeight + 10,
right: window.innerWidth - button.offsetLeft - button.offsetWidth
}
}
}]);
</script>
```
Vous pouvez tester cet exemple sur cette [page dédiée](/widgets-demo/lagaufre-single).
Nous avons une documentation interactive avec plusieurs [autres exemples de configuration](/widgets-demo/lagaufre).
## Intégrations existantes
Vous pouvez tester La Gaufre v2 en ligne sur ces services :
* [Suite territoriale - Messages](https://messages.suite.anct.gouv.fr)

View File

@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feedback Widget Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
* {
color: #666;
text-align: center;
}
h1 {
color: #333;
text-align: center;
}
.controls {
text-align: center;
margin: 20px 0;
}
button {
margin: 0 10px;
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<a href="/">Back to index</a>
<h1>Feedback Widget Demo</h1>
<script type="module" src="/widgets/dist/feedback.js"></script>
<script>
window._lasuite_widget = window._lasuite_widget || [];
_lasuite_widget.push(["feedback", "init", {
"title": "Do you have any feedback?",
"api": "/widgets/demo/feedback-api.json?",
"channel": "xyz-xyz-xyz",
"successText2": "We will get back to you as soon as possible. Let's add some text too to test the widget behaving correctly."
}]);
</script>
<button onclick='_lasuite_widget.push(["feedback", "close"]);'>Close</button>
<button onclick='
_lasuite_widget.push(["feedback", "init", {
"email": "test@test.com",
"title": "Do you have any feedback?",
"api": "/widgets/demo/feedback-api.json?",
"channel": "xyz-xyz-xyz"
}]);
'>Re-init with email</button>
<br/><br/>
<button onclick='
_lasuite_widget.push(["feedback", "init", {
"email": "test@test.com",
"title": "Do you have any feedback?",
"api": "/error",
"channel": "xyz-xyz-xyz"
}]);
'>Re-init with loading error</button>
<button onclick='
_lasuite_widget.push(["feedback", "init", {
"email": "test@test.com",
"title": "Do you have any feedback?",
"api": "/widgets/demo/feedback-api-error.json?",
"channel": "xyz-xyz-xyz"
}]);
'>Re-init with submit error</button>
<br/><br/>
<button onclick='
_lasuite_widget.push(["feedback", "init", {
"title": "With offset",
"api": "/widgets/demo/feedback-api.json?",
"channel": "xyz-xyz-xyz",
"bottomOffset": 100,
"rightOffset": 50
}]);
'>Re-init with position offset</button>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>La Gaufre Widget Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.demo-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
button {
float:right;
cursor: pointer;
display:flex;
border: none;
background: none;
padding: 8px;
}
</style>
</head>
<body>
<div class="demo-container">
<p>
<a href="/">Back to index</a>
</p>
<script src="/widgets/dist/lagaufre.js" async type="module"></script>
<button type="button" id="gaufre_button" aria-label="Ouvrir la Gaufre" aria-expanded="false">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path fill="#000091" id="square" d="M2.7959 0.5C3.26483 0.5 3.49956 0.49985 3.68848 0.564453C4.03934 0.684581 4.31542 0.960658 4.43555 1.31152C4.50015 1.50044 4.5 1.73517 4.5 2.2041V2.7959C4.5 3.26483 4.50015 3.49956 4.43555 3.68848C4.31542 4.03934 4.03934 4.31542 3.68848 4.43555C3.49956 4.50015 3.26483 4.5 2.7959 4.5H2.2041C1.73517 4.5 1.50044 4.50015 1.31152 4.43555C0.960658 4.31542 0.684581 4.03934 0.564453 3.68848C0.49985 3.49956 0.5 3.26483 0.5 2.7959V2.2041C0.5 1.73517 0.49985 1.50044 0.564453 1.31152C0.684581 0.960658 0.960658 0.684581 1.31152 0.564453C1.50044 0.49985 1.73517 0.5 2.2041 0.5H2.7959Z" />
</defs>
<use href="#square" transform="translate(0, 0)"/><use href="#square" transform="translate(6.5, 0)"/><use href="#square" transform="translate(13, 0)"/><use href="#square" transform="translate(0, 6.5)"/><use href="#square" transform="translate(6.5, 6.5)"/><use href="#square" transform="translate(13, 6.5)"/><use href="#square" transform="translate(0, 13)"/><use href="#square" transform="translate(6.5, 13)"/><use href="#square" transform="translate(13, 13)"/>
</svg>
</button>
<script>
const button = document.getElementById("gaufre_button");
window._lasuite_widget = window._lasuite_widget || [];
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
label: "Services de la Suite numérique",
closeLabel: "Fermer le menu",
headerLabel: "À propos",
loadingText: "Chargement…",
newWindowLabelSuffix: " (nouvelle fenêtre)",
buttonElement: button,
position: () => {
return {
position: "absolute",
top: button.offsetTop + button.offsetHeight + 10,
right: window.innerWidth - button.offsetLeft - button.offsetWidth
}
}
}]);
</script>
<h1>La Gaufre - Single Page Demo</h1>
<p>
Try opening the Gaufre by clicking on the button on the top right! You can also use keyboard navigation.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</body>
</html>
</div>

View File

@@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>La Gaufre Widget Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.demo-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.demo-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.demo-section h3 {
margin-top: 0;
color: #333;
}
button {
background: #000091;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #1212ff;
}
.code-block {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
margin: 10px 0;
font-family: monospace;
font-size: 14px;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="demo-container">
<a href="/">Back to index</a>
<h1>La Gaufre Widget Demo</h1>
<p>This demo shows how to use the La Gaufre widget to display a list of services with subscription status.</p>
<div class="demo-section">
<h2>Basic Usage</h2>
<p>Open the widget with default settings:</p>
<button type="button" onclick="openBasicWidget(this)">Open La Gaufre Widget</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true
}]);</pre></div>
</div>
<div class="demo-section">
<h2>With ANCT Data</h2>
<p>Open the widget with ANCT services data:</p>
<button type="button" onclick="openBasicWidget(this)">Open with ANCT Data</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'init', {
api: 'https://operateurs.suite.anct.gouv.fr/api/v1.0/lagaufre/services/?operator=9f5624fc-ef99-4d10-ae3f-403a81eb16ef&siret=21870030000013',
open: true
}]);</pre></div>
</div>
<div class="demo-section">
<h2>With LaSuite data and style</h2>
<p>Open the widget with gradient background:</p>
<button type="button" onclick="openBasicWidget(this)">Open with LaSuite Data</button>
<div class="code-block"><pre>
// With gradient background
_lasuite_widget.push(['lagaufre', 'init', {
api: 'https://lasuite.numerique.gouv.fr/api/services',
open: true,
background: 'linear-gradient(180deg, #eceffd 0%, #FFFFFF 20%)',
headerLogo: 'https://lasuite.numerique.gouv.fr/_next/static/media/suite-numerique.ebdb6ce9.svg',
headerUrl: 'https://lasuite.numerique.gouv.fr',
showFooter: true
}]);</pre></div>
</div>
<div class="demo-section">
<h2>With Custom Font</h2>
<p>Open the widget with custom font family:</p>
<button type="button" onclick="openBasicWidget(this)">Open with Custom Font</button>
<div class="code-block"><pre>
// With custom font
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
fontFamily: 'Georgia, serif'
}]);</pre></div>
</div>
<div class="demo-section">
<h2>Positioning Options</h2>
<p>Control widget position and behavior:</p>
<button type="button" onclick="openBasicWidget(this)">Top Right</button>
<div class="code-block"><pre>
// Top Right positioning
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
label: 'La Gaufre - Top Right',
position: 'fixed',
top: 20,
right: 20
}]);</pre></div>
<button type="button" onclick="openBasicWidget(this)">Bottom Left</button>
<div class="code-block"><pre>
// Bottom Left positioning
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
label: 'La Gaufre - Bottom Left',
position: 'fixed',
bottom: 20,
left: 20
}]);</pre></div>
<button type="button" onclick="openBasicWidget(this)">Below button (absolute)</button>
<div class="code-block"><pre>
// Absolute & dynamic positioning, tied to a button
(button) => {
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
label: 'La Gaufre - Bottom Left',
position: () => {
return {
position: 'absolute',
top: button.offsetTop + button.offsetHeight + 10,
right: window.innerWidth - button.offsetLeft - button.offsetWidth
};
},
buttonElement: button
}]);
}</pre></div>
</div>
<div class="demo-section">
<h2>No dialog</h2>
<p>You can insert the widget into an existing container:</p>
<div id="custom-box" style="width: 100%; max-width: 500px; height: 300px; overflow: hidden; background-color: #f0f0f0; border: 1px solid blue; border-radius: 8px; padding: 10px;"></div>
<button type="button" onclick="openBasicWidget(this)">Open inside box</button>
<div class="code-block"><pre>
// No dialog
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
dialogElement: document.getElementById('custom-box')
}]);</pre></div>
<button type="button" onclick="openBasicWidget(this)">Close widget</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'close']);</pre></div>
</div>
<div class="demo-section">
<h2>With hardcoded data</h2>
<p>You can pass static data and avoid any API calls:</p>
<button type="button" onclick="openBasicWidget(this)">Open La Gaufre Widget</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'init', {
open: true,
data: {
"services": [
{
"name": "Authentication Service",
"url": "https://example.com/auth",
"logo": "/widgets/demo/logos/auth.svg",
},
{
"name": "Document Portal",
"url": "https://example.com/docs",
"logo": "/widgets/demo/logos/docs.svg",
},
]
}
}]);</pre></div>
</div>
<div class="demo-section">
<h2>Open and close Widget Programmatically</h2>
<p>Close the widget programmatically after 2 seconds:</p>
<button type="button" onclick="openBasicWidget(this)">Open Widget</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json'
}]);
setTimeout(() => {
_lasuite_widget.push(['lagaufre', 'open']);
}, 300);
setTimeout(() => {
_lasuite_widget.push(['lagaufre', 'close']);
}, 2000);</pre></div>
<button type="button" onclick="openBasicWidget(this)">Toggle Widget Programmatically</button>
<div class="code-block"><pre>
_lasuite_widget.push(['lagaufre', 'toggle']);</pre></div>
</div>
<div class="demo-section">
<h2>Full localization</h2>
<p>You can fully localize the widget:</p>
<button type="button" onclick="openBasicWidget(this)">Open Widget</button>
<div class="code-block"><pre>
// Full localization
_lasuite_widget.push(['lagaufre', 'init', {
api: '/widgets/demo/lagaufre-data.json',
open: true,
label: 'La Gaufre',
headerLogo: 'https://lasuite.numerique.gouv.fr/_next/static/media/suite-numerique.ebdb6ce9.svg',
headerUrl: 'https://lasuite.numerique.gouv.fr',
headerLabel: 'A propos de LaSuite',
closeLabel: 'Fermer la liste des services',
loadingText: 'Chargement des services...',
newWindowLabelSuffix: ' (nouvelle fenêtre)'
}]);</pre></div>
</div>
<div class="demo-section">
<h2>API Response Format</h2>
<p>The widget expects the following API response format:</p>
<div class="code-block"><pre>
{
"services": [
{
"name": "Service Name",
"url": "https://service-url.com",
"maturity": "stable",
"logo": "/path/to.svg"
}
]
}</pre></div>
</div>
</div>
<!-- Load the La Gaufre widget -->
<script type="module" src="/widgets/dist/lagaufre.js"></script>
<script>
// Initialize the widget system
window._lasuite_widget = window._lasuite_widget || [];
function openBasicWidget(button) {
// Find the code block that immediately follows this button
const codeBlock = button.nextElementSibling;
if (codeBlock && codeBlock.classList.contains('code-block')) {
// Extract the JavaScript code from the code block
const codeText = codeBlock.textContent.trim();
// Remove comments and clean up the code
const cleanCode = codeText.replace(/^\/\/.*$/gm, '').trim();
// Execute the code
try {
var ret = eval(cleanCode);
if (typeof ret === 'function') {
return ret(button);;
}
} catch (error) {
console.error('Error executing widget code:', error);
alert('Error executing widget code: ' + error.message);
}
} else {
console.error('No code block found after button');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loader Widget Demo</title>
<style>
* {
color: #666;
text-align: center;
}
html {
font-family: Arial, sans-serif;
padding: 0px;
background: #f5f5f5;
}
button {
margin: 0 10px;
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<a href="/">Back to index</a>
<h1>Loader Widget Demo</h1>
<button onclick="document.documentElement.style.backgroundColor = '#000'">Make background dark</button>
<button onclick="document.documentElement.style.backgroundColor = '#fff'">Make background light</button>
<button onclick='
_lasuite_widget.push(["loader", "init", {
"params": {
"title": "Do you have any feedback?",
"api": "/widgets/demo/feedback-api.json?",
"channel": "xyz-xyz-xyz"
},
"script": "/error",
"scriptType": "module",
"widget": "feedback",
"label": "Feedback Widget"
}]);
'>Re-init with target loading error</button>
<script type="module" src="/widgets/dist/loader.js"></script>
<script>
window._lasuite_widget = window._lasuite_widget || [];
_lasuite_widget.push(["loader", "init", {
"params": {
"title": "Do you have any feedback?",
"api": "/widgets/demo/feedback-api.json?",
"channel": "xyz-xyz-xyz",
"successText2": "We'll get back to you soon."
},
"script": "/widgets/dist/feedback.js",
"scriptType": "module",
"widget": "feedback",
"label": "Feedback Widget"
}]);
</script>
</body>
</html>