✨(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
|
# 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
|
||||||
|
|||||||
@@ -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 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">×</button>` +
|
`</a>`
|
||||||
`</div>`
|
) : (
|
||||||
) : "") +
|
`<img src="${args.headerLogo}" id="header-logo">`
|
||||||
|
)
|
||||||
|
) : "") +
|
||||||
|
`<button type="button" id="close">×</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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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