✨(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:
@@ -1,5 +1,12 @@
|
||||
# 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
|
||||
|
||||
- new grist, resana, docs, visio, rdv logos
|
||||
|
||||
@@ -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 |
@@ -1,4 +1,5 @@
|
||||
import styles from "./styles.css?inline";
|
||||
import chevronDownwardSvg from "./chevron_downward.svg?raw";
|
||||
import { createShadowWidget } from "../../shared/shadow-dom";
|
||||
import { installHook } from "../../shared/script";
|
||||
import { listenEvent, triggerEvent } from "../../shared/events";
|
||||
@@ -41,11 +42,13 @@ type GaufreWidgetArgs = {
|
||||
label?: string;
|
||||
closeLabel?: string;
|
||||
headerLabel?: string;
|
||||
viewMoreLabel?: string;
|
||||
viewLessLabel?: string;
|
||||
loadingText?: string;
|
||||
newWindowLabelSuffix?: string;
|
||||
showFooter?: boolean;
|
||||
dialogElement?: HTMLElement;
|
||||
buttonElement?: HTMLElement;
|
||||
showMoreLimit?: number;
|
||||
};
|
||||
|
||||
let loaded = false;
|
||||
@@ -65,26 +68,44 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
|
||||
const listeners: (() => void)[] = [];
|
||||
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/
|
||||
/* prettier-ignore */
|
||||
const htmlContent =
|
||||
`<div id="wrapper" role="dialog" aria-modal="true" tabindex="-1">` +
|
||||
((args.headerLogo && args.headerUrl) ? (
|
||||
`<div id="header">` +
|
||||
`<a href="${args.headerUrl}" target="_blank">` +
|
||||
`<img src="${args.headerLogo}" id="header-logo">` +
|
||||
`</a>` +
|
||||
`<button type="button" id="close">×</button>` +
|
||||
`</div>`
|
||||
) : "") +
|
||||
`<div id="header">` +
|
||||
(args.headerLogo ? (
|
||||
args.headerUrl ? (
|
||||
`<a href="${args.headerUrl}" target="_blank">` +
|
||||
`<img src="${args.headerLogo}" id="header-logo">` +
|
||||
`</a>`
|
||||
) : (
|
||||
`<img src="${args.headerLogo}" id="header-logo">`
|
||||
)
|
||||
) : "") +
|
||||
`<button type="button" id="close">×</button>` +
|
||||
`</div>` +
|
||||
`<div id="content">` +
|
||||
`<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>` +
|
||||
(args.showFooter ? `<div id="footer">` +
|
||||
`<div id="footer">` +
|
||||
`<button id="ok-button">OK</button>` +
|
||||
`</div>` : "") +
|
||||
`</div>` +
|
||||
`</div>`;
|
||||
|
||||
// Create shadow DOM widget
|
||||
@@ -94,6 +115,11 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
|
||||
const wrapper = shadowRoot.querySelector<HTMLDivElement>("#wrapper")!;
|
||||
const loadingDiv = shadowRoot.querySelector<HTMLDivElement>("#loading")!;
|
||||
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 closeBtn = shadowRoot.querySelector<HTMLButtonElement>("#close");
|
||||
const okBtn = shadowRoot.querySelector<HTMLButtonElement>("#ok-button")!;
|
||||
@@ -153,6 +179,10 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
|
||||
listeners.push(
|
||||
listenEvent("", "resize", window, false, () => {
|
||||
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) => {
|
||||
// Clear previous content
|
||||
servicesGrid.innerHTML = "";
|
||||
moreServicesGrid.innerHTML = "";
|
||||
const maxInitialServices = showMoreLimit;
|
||||
const hasMoreServices = data.services.length > maxInitialServices;
|
||||
|
||||
|
||||
data.services.forEach((service) => {
|
||||
if (!service.logo) return;
|
||||
const createServiceCard = (service: Service) => {
|
||||
if (!service.logo) return null;
|
||||
if (service.maturity == "stable") delete service.maturity;
|
||||
|
||||
const serviceCard = document.createElement("li");
|
||||
@@ -205,9 +239,51 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
|
||||
img.src = service.logo;
|
||||
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";
|
||||
errorDiv.style.display = "none";
|
||||
servicesGrid.style.display = "grid";
|
||||
@@ -252,7 +328,7 @@ listenEvent(widgetName, "init", null, false, async (args: GaufreWidgetArgs) => {
|
||||
// Open widget (show the prepared shadow DOM)
|
||||
listeners.push(
|
||||
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.
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
display: none; /* Hidden by default on desktop */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 16px;
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
#footer {
|
||||
display: flex;
|
||||
display: none; /* Hidden by default on desktop */
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
border-top: 1px solid #dfe2ea;
|
||||
@@ -97,7 +97,6 @@
|
||||
|
||||
#content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
@@ -112,6 +111,7 @@
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
@@ -123,6 +123,7 @@
|
||||
color: #dc3545;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
/* Services grid */
|
||||
@@ -150,6 +151,78 @@
|
||||
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 {
|
||||
background-color: #eef1f4;
|
||||
border-radius: 6px;
|
||||
@@ -205,6 +278,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#more-services-grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#more-services-grid.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
#wrapper {
|
||||
@@ -218,9 +299,17 @@
|
||||
position: fixed !important;
|
||||
}
|
||||
|
||||
#more-services-grid {
|
||||
display: grid !important;
|
||||
}
|
||||
|
||||
#show-more-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
display: flex; /* Always show header on mobile */
|
||||
}
|
||||
|
||||
#header-logo {
|
||||
@@ -236,6 +325,11 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#more-services-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
width: 70px;
|
||||
padding: 6px;
|
||||
|
||||
5
website/public/widgets/dist/lagaufre.js
vendored
5
website/public/widgets/dist/lagaufre.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user