(widgets) update gaufre V2 ui/ux

- Header and footer are displayed by default in mobile mode, but never
in desktop mode.
- show only the first 6 services and add view_more button
This commit is contained in:
Nathan Panchout
2025-11-25 16:09:53 +01:00
parent 720ee9f4f0
commit 86392eb40b
5 changed files with 204 additions and 21 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## [unreleased]
- header and footer are displayed by default in mobile mode, but never in desktop mode.
- show only the first x (customizable) services and add view_more button
## 1.0.3 ## 1.0.3
- new grist, resana, docs, visio, rdv logos - new grist, resana, docs, visio, rdv logos

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8.40004L11.0667 5.33337L12 6.26671L8 10.2667L4 6.26671L4.93333 5.33337L8 8.40004Z" fill="#626A80"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -1,4 +1,5 @@
import styles from "./styles.css?inline"; import styles from "./styles.css?inline";
import chevronDownwardSvg from "./chevron_downward.svg?raw";
import { createShadowWidget } from "../../shared/shadow-dom"; import { createShadowWidget } from "../../shared/shadow-dom";
import { installHook } from "../../shared/script"; import { installHook } from "../../shared/script";
import { listenEvent, triggerEvent } from "../../shared/events"; import { listenEvent, triggerEvent } from "../../shared/events";
@@ -41,11 +42,13 @@ type GaufreWidgetArgs = {
label?: string; label?: string;
closeLabel?: string; closeLabel?: string;
headerLabel?: string; headerLabel?: string;
viewMoreLabel?: string;
viewLessLabel?: string;
loadingText?: string; loadingText?: string;
newWindowLabelSuffix?: string; newWindowLabelSuffix?: string;
showFooter?: boolean;
dialogElement?: HTMLElement; dialogElement?: HTMLElement;
buttonElement?: HTMLElement; buttonElement?: HTMLElement;
showMoreLimit?: number;
}; };
let loaded = false; let loaded = false;
@@ -65,26 +68,44 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
const listeners: (() => void)[] = []; const listeners: (() => void)[] = [];
let isVisible = false; let isVisible = false;
const viewMoreLabel = args.viewMoreLabel || "More apps";
const viewLessLabel = args.viewLessLabel || "Fewer apps";
const showMoreLimit = args.showMoreLimit || 6;
// https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ // https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
/* prettier-ignore */ /* prettier-ignore */
const htmlContent = const htmlContent =
`<div id="wrapper" role="dialog" aria-modal="true" tabindex="-1">` + `<div id="wrapper" role="dialog" aria-modal="true" tabindex="-1">` +
((args.headerLogo && args.headerUrl) ? ( `<div id="header">` +
`<div id="header">` + (args.headerLogo ? (
`<a href="${args.headerUrl}" target="_blank">` + args.headerUrl ? (
`<img src="${args.headerLogo}" id="header-logo">` + `<a href="${args.headerUrl}" target="_blank">` +
`</a>` + `<img src="${args.headerLogo}" id="header-logo">` +
`<button type="button" id="close">&times;</button>` + `</a>`
`</div>` ) : (
) : "") + `<img src="${args.headerLogo}" id="header-logo">`
)
) : "") +
`<button type="button" id="close">&times;</button>` +
`</div>` +
`<div id="content">` + `<div id="content">` +
`<div id="loading">Loading...</div>` + `<div id="loading">Loading...</div>` +
`<ul role="list" id="services-grid" style="display: none;"></ul>` + `<div id="main-apps">` +
`<ul role="list" id="services-grid" style="display: none;"></ul>` +
`</div>` +
`<div id="more-apps" style="display: none;">` +
`<div id="show-more-container">` +
`<button type="button" id="show-more-button">` +
`<span id="show-more-chevron" aria-hidden="true">${chevronDownwardSvg}</span>` +
`<span id="show-more-text">${viewMoreLabel}</span>` +
`</button>` +
`</div>` +
`<ul role="list" id="more-services-grid"></ul>` +
`</div>` +
`<div id="error" style="display: none;"></div>` + `<div id="error" style="display: none;"></div>` +
`</div>` + `</div>` +
(args.showFooter ? `<div id="footer">` + `<div id="footer">` +
`<button id="ok-button">OK</button>` + `<button id="ok-button">OK</button>` +
`</div>` : "") + `</div>` +
`</div>`; `</div>`;
// Create shadow DOM widget // Create shadow DOM widget
@@ -94,6 +115,11 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
const wrapper = shadowRoot.querySelector<HTMLDivElement>("#wrapper")!; const wrapper = shadowRoot.querySelector<HTMLDivElement>("#wrapper")!;
const loadingDiv = shadowRoot.querySelector<HTMLDivElement>("#loading")!; const loadingDiv = shadowRoot.querySelector<HTMLDivElement>("#loading")!;
const servicesGrid = shadowRoot.querySelector<HTMLDivElement>("#services-grid")!; const servicesGrid = shadowRoot.querySelector<HTMLDivElement>("#services-grid")!;
const moreAppsSection = shadowRoot.querySelector<HTMLDivElement>("#more-apps")!;
const moreServicesGrid = shadowRoot.querySelector<HTMLDivElement>("#more-services-grid")!;
const showMoreBtn = shadowRoot.querySelector<HTMLButtonElement>("#show-more-button")!;
const showMoreChevron = shadowRoot.querySelector<HTMLSpanElement>("#show-more-chevron")!;
const showMoreText = shadowRoot.querySelector<HTMLSpanElement>("#show-more-text")!;
const errorDiv = shadowRoot.querySelector<HTMLDivElement>("#error")!; const errorDiv = shadowRoot.querySelector<HTMLDivElement>("#error")!;
const closeBtn = shadowRoot.querySelector<HTMLButtonElement>("#close"); const closeBtn = shadowRoot.querySelector<HTMLButtonElement>("#close");
const okBtn = shadowRoot.querySelector<HTMLButtonElement>("#ok-button")!; const okBtn = shadowRoot.querySelector<HTMLButtonElement>("#ok-button")!;
@@ -153,6 +179,10 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
listeners.push( listeners.push(
listenEvent("", "resize", window, false, () => { listenEvent("", "resize", window, false, () => {
configure(args); configure(args);
// Re-render services on resize to handle mobile/desktop switch
if (args.data) {
renderServices(args.data);
}
}), }),
); );
@@ -169,9 +199,13 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
const renderServices = (data: ServicesResponse) => { const renderServices = (data: ServicesResponse) => {
// Clear previous content // Clear previous content
servicesGrid.innerHTML = ""; servicesGrid.innerHTML = "";
moreServicesGrid.innerHTML = "";
const maxInitialServices = showMoreLimit;
const hasMoreServices = data.services.length > maxInitialServices;
data.services.forEach((service) => { const createServiceCard = (service: Service) => {
if (!service.logo) return; if (!service.logo) return null;
if (service.maturity == "stable") delete service.maturity; if (service.maturity == "stable") delete service.maturity;
const serviceCard = document.createElement("li"); const serviceCard = document.createElement("li");
@@ -205,9 +239,51 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
img.src = service.logo; img.src = service.logo;
serviceName.textContent = service.name; serviceName.textContent = service.name;
servicesGrid.appendChild(serviceCard); return serviceCard;
};
// Render initial services (first 6)
const initialServices = data.services.slice(0, maxInitialServices);
initialServices.forEach((service) => {
const serviceCard = createServiceCard(service);
if (serviceCard) {
servicesGrid.appendChild(serviceCard);
}
}); });
// Handle additional services if any
if (hasMoreServices) {
const additionalServices = data.services.slice(maxInitialServices);
// Render additional services in the more services grid
additionalServices.forEach((service) => {
const serviceCard = createServiceCard(service);
if (serviceCard) {
moreServicesGrid.appendChild(serviceCard);
}
});
// Show the more apps section
moreAppsSection.style.display = "flex";
moreServicesGrid.classList.add("hidden");
showMoreChevron.classList.remove("opened");
// Update button text and handle click
const updateButton = () => {
moreServicesGrid.classList.toggle("hidden");
showMoreChevron.classList.toggle("opened");
const isOpened = showMoreChevron.classList.contains("opened");
showMoreText.textContent = !isOpened ? viewLessLabel : viewMoreLabel;
};
showMoreBtn.addEventListener("click", () => {
updateButton();
});
} else {
// Hide the more apps section if no additional services
moreAppsSection.style.display = "none";
}
loadingDiv.style.display = "none"; loadingDiv.style.display = "none";
errorDiv.style.display = "none"; errorDiv.style.display = "none";
servicesGrid.style.display = "grid"; servicesGrid.style.display = "grid";
@@ -252,7 +328,7 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
// Open widget (show the prepared shadow DOM) // Open widget (show the prepared shadow DOM)
listeners.push( listeners.push(
listenEvent(widgetName, "open", null, false, () => { listenEvent(widgetName, "open", null, false, () => {
wrapper.style.display = "block"; wrapper.style.display = "flex";
// Add click outside listener after a short delay to prevent immediate closing or double-clicks. // Add click outside listener after a short delay to prevent immediate closing or double-clicks.
setTimeout(() => { setTimeout(() => {

View File

@@ -20,7 +20,7 @@
} }
#header { #header {
display: flex; display: none; /* Hidden by default on desktop */
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 4px 16px; padding: 4px 16px;
@@ -67,7 +67,7 @@
} }
#footer { #footer {
display: flex; display: none; /* Hidden by default on desktop */
padding: 16px; padding: 16px;
background: transparent; background: transparent;
border-top: 1px solid #dfe2ea; border-top: 1px solid #dfe2ea;
@@ -97,7 +97,6 @@
#content { #content {
flex: 1; flex: 1;
padding: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
@@ -112,6 +111,7 @@
padding: 40px 20px; padding: 40px 20px;
color: #666; color: #666;
font-size: 14px; font-size: 14px;
margin: 16px;
} }
/* Error state */ /* Error state */
@@ -123,6 +123,7 @@
color: #dc3545; color: #dc3545;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
margin: 16px;
} }
/* Services grid */ /* Services grid */
@@ -150,6 +151,78 @@
margin: 0; margin: 0;
} }
/* Main apps section */
#main-apps {
padding: 16px;
}
/* More apps section */
#more-apps {
border-top: 1px solid #dfe2ea;
padding: 16px;
background: transparent;
display: flex;
flex-direction: column-reverse;
}
#more-services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
justify-items: center;
list-style: none;
padding: 0;
margin: 0 0 16px 0;
}
#show-more-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
#show-more-button {
background: none;
color: #64748b;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#show-more-button:hover {
background: #f1f5f9;
color: #475569;
}
#show-more-button:focus {
outline: none;
}
#show-more-button:focus-visible {
outline: 2px solid #0a76f6;
outline-offset: 2px;
}
#show-more-chevron {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
#show-more-chevron.opened {
transform: rotate(180deg);
}
.service-card:hover { .service-card:hover {
background-color: #eef1f4; background-color: #eef1f4;
border-radius: 6px; border-radius: 6px;
@@ -205,6 +278,14 @@
text-align: center; text-align: center;
} }
#more-services-grid {
display: grid;
}
#more-services-grid.hidden {
display: none;
}
/* Responsive design */ /* Responsive design */
@media (max-width: 480px) { @media (max-width: 480px) {
#wrapper { #wrapper {
@@ -218,9 +299,17 @@
position: fixed !important; position: fixed !important;
} }
#more-services-grid {
display: grid !important;
}
#show-more-button {
display: none;
}
#header { #header {
height: 40px; height: 40px;
display: flex; display: flex; /* Always show header on mobile */
} }
#header-logo { #header-logo {
@@ -236,6 +325,11 @@
gap: 12px; gap: 12px;
} }
#more-services-grid {
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.service-card { .service-card {
width: 70px; width: 70px;
padding: 6px; padding: 6px;

File diff suppressed because one or more lines are too long