diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant.css b/hlx_statics/blocks/ai-assistant/ai-assistant.css
index ec2a89801..8820c3e3e 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant.css
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant.css
@@ -25,6 +25,7 @@
);
--container-padding: 40px;
--button-size: 48px;
+ --opening-transition-duration: 0.25s;
display: flex;
flex-direction: column-reverse;
@@ -41,7 +42,9 @@
position: relative;
width: var(--button-size);
+ min-width: var(--button-size);
height: var(--button-size);
+ min-height: var(--button-size);
border-radius: 50%;
border: 2px solid transparent;
padding: 0;
@@ -51,6 +54,9 @@
var(--ai-border-linear-gradient) border-box;
align-self: flex-end;
pointer-events: auto;
+ visibility: visible;
+ transition: visibility 0s linear
+ calc(var(--opening-transition-duration) * 0.9);
&:hover {
--background-color: #f0f0f0;
@@ -58,6 +64,7 @@
&.hidden {
visibility: hidden;
+ transition: visibility 0s linear;
}
}
@@ -65,13 +72,11 @@
* MARK: Chat Window
*/
.ai-assistant-panel .chat-window {
- --transition-duration: 0.25s;
- width: var(--button-size);
- height: var(--button-size);
- border-radius: calc(var(--button-size) / 2);
- background:
- linear-gradient(#ffffff, #ffffff) padding-box,
- var(--ai-border-linear-gradient) border-box;
+ --chat-window-width: 460px;
+ width: var(--chat-window-width);
+ height: 100%;
+ border-radius: 8px;
+ background: #fff;
/* Drop shadow/elevated (pretty much copy pasta from Figma) */
box-shadow:
0 0 2px 0 rgba(0, 0, 0, 0.12),
@@ -80,48 +85,25 @@
display: flex;
flex-direction: column;
overflow: hidden;
- transform: translateY(var(--button-size));
+ transform: scale(0);
+ transform-origin: calc(100% - var(--button-size) / 2)
+ calc(100% - var(--button-size) / 2);
+ translate: 0 var(--button-size);
visibility: hidden;
- border: solid transparent;
- border-width: 2px;
+ border: 0;
transition:
- width var(--transition-duration) ease-out var(--transition-duration),
- height var(--transition-duration) ease-out var(--transition-duration),
- border-radius var(--transition-duration) ease-out,
- border-width var(--transition-duration) linear,
- visibility 0s linear calc(var(--transition-duration) * 2);
-
- .chat-window-header,
- .chat-window-content,
- .chat-window-input-section {
- opacity: 0;
- transition: opacity var(--transition-duration) ease-out 0s;
- }
+ transform var(--opening-transition-duration) cubic-bezier(0.5, 0, 1, 1),
+ visibility 0s linear var(--opening-transition-duration);
&.show {
- width: 400px;
- height: 100%;
- border-radius: 8px;
visibility: visible;
transition:
- width var(--transition-duration) ease-out,
- height var(--transition-duration) ease-out,
- border-radius var(--transition-duration) ease-out
- var(--transition-duration),
- border-width var(--transition-duration) linear
- var(--transition-duration),
- visibility 0s linear 0s;
+ transform var(--opening-transition-duration)
+ cubic-bezier(0, 0, 0.4, 1),
+ visibility 0s linear;
pointer-events: auto;
- border-width: 0px;
-
- .chat-window-header,
- .chat-window-content,
- .chat-window-input-section {
- opacity: 1;
- transition: opacity var(--transition-duration) ease-out
- calc(var(--transition-duration) * 2);
- }
+ transform: scale(1);
}
.chat-ai-avatar {
@@ -151,8 +133,21 @@
margin: 0;
font-size: 18px;
font-weight: 700;
- flex: 1;
- margin-left: 12px;
+ margin-inline: 12px;
+ }
+
+ .chat-window-badge {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px 11px;
+ background-color: rgb(255, 232, 240);
+ color: black;
+ border-radius: 7px;
+ font-size: 12px;
+ min-height: 24px;
+ line-height: 14px;
}
.chat-window-close,
@@ -734,4 +729,4 @@
max-width: 400px;
}
}
-}
\ No newline at end of file
+}
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant.js b/hlx_statics/blocks/ai-assistant/ai-assistant.js
index 959ff83b7..2d940d45e 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant.js
@@ -6,6 +6,7 @@ import {
import { aiApiClient } from "./ai-assistant_api-client.js";
import {
+ onUserScroll,
restoreChatHistory,
toggleChatWindow,
} from "./ai-assistant_chat-controller.js";
@@ -34,6 +35,20 @@ export default async function decorate(block) {
document.body,
"https://unpkg.com/marked@^17/lib/marked.umd.js",
() => {
+ // @ts-expect-error - marked is not on the Window object
+ window.marked.use({
+ renderer: {
+ /**
+ * @param {Object} options
+ * @param {string} options.href
+ * @param {string} options.title
+ * @param {string} options.text
+ */
+ link({ href, title, text }) {
+ return `${text}`;
+ },
+ },
+ });
addExtraScriptWithLoad(
document.body,
"https://unpkg.com/dompurify@^3/dist/purify.min.js",
@@ -67,6 +82,7 @@ export default async function decorate(block) {
block.appendChild(panel);
+ ELEMENTS.CHAT_WINDOW_CONTENT?.addEventListener("scroll", onUserScroll);
ELEMENTS.CHAT_BUTTON?.addEventListener("click", toggleChatWindow);
ELEMENTS.CHAT_WINDOW_CLEAR_BUTTON?.addEventListener("click", () =>
chatWindow.appendChild(createClearDialog()),
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js b/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
index b8ce6def3..1bfa0d076 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
@@ -1,5 +1,6 @@
// @ts-check
-import { AI_API_BASE_URL, AI_API_KEY } from "./ai-assistant_constants.js";
+
+import { isProdEnvironment } from "../../scripts/lib-adobeio.js";
/**
* @typedef {Object} RequestBody
@@ -87,11 +88,19 @@ import { AI_API_BASE_URL, AI_API_KEY } from "./ai-assistant_constants.js";
* @property {(error: unknown) => void} onError
*/
+const STAGE_API_URL =
+ "https://devsite-rag.stg.app-builder.corp.adp.adobe.io/v1/inference";
+const PROD_API_URL =
+ "https://devsite-rag.app-builder.adp.adobe.io/v1/inference";
+const STAGE_API_KEY = "ai-assistant-devsite-rag-demo-01";
+const PROD_API_KEY = "devsite-rag";
+const IS_PROD = isProdEnvironment(window.location.host);
+
export class AiApiClient {
- static STREAMING_ENDPOINT = "/v1/inference/retrieve/generate/stream";
- static NON_STREAMING_ENDPOINT = "/v1/inference/retrieve/generate";
- static COLLECTIONS_ENDPOINT = "/v1/inference/collections";
- static FEEDBACK_ENDPOINT = "/v1/inference/feedback";
+ static STREAMING_ENDPOINT = "/retrieve/generate/stream";
+ static NON_STREAMING_ENDPOINT = "/retrieve/generate";
+ static COLLECTIONS_ENDPOINT = "/collections";
+ static FEEDBACK_ENDPOINT = "/feedback";
static LOCAL_STORAGE_COLLECTIONS_KEY = "ai-assistant__collections";
static LOCAL_STORAGE_COLLECTION_TTL = 1 * 24 * 60 * 60 * 1000; // 1 day
@@ -414,6 +423,6 @@ export class AiApiClient {
}
export const aiApiClient = new AiApiClient({
- baseUrl: AI_API_BASE_URL,
- apiKey: AI_API_KEY,
+ baseUrl: IS_PROD ? PROD_API_URL : STAGE_API_URL,
+ apiKey: IS_PROD ? PROD_API_KEY : STAGE_API_KEY,
});
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
index face1a4e9..b16486dcf 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
@@ -40,7 +40,9 @@ export class ChatBubble {
*/
#_init() {
const bubble = createTag("div", { class: "chat-bubble" });
- const contentElement = createTag("div", { class: "chat-bubble-content" });
+ const contentElement = createTag("div", {
+ class: "chat-bubble-content",
+ });
if (this.source === "ai") {
if (!this.isContinuingConversation) {
@@ -111,6 +113,7 @@ export class ChatBubble {
class: "chat-bubble-copy",
type: "button",
"aria-label": COPY_BUTTON_LABEL,
+ "daa-ll": "DevsiteAI Assistant:Message:Button:Copy",
})
);
button.innerHTML = COPY_ICON_SVG;
@@ -180,6 +183,7 @@ export class ChatBubble {
class: "chat-bubble-feedback",
type: "button",
"aria-label": THUMB_UP_LABEL,
+ "daa-ll": "DevsiteAI Assistant:Message:Button:Upvote",
})
);
thumbUpButton.innerHTML = THUMB_UP_ICON_SVG;
@@ -192,6 +196,7 @@ export class ChatBubble {
class: "chat-bubble-feedback",
type: "button",
"aria-label": THUMB_DOWN_LABEL,
+ "daa-ll": "DevsiteAI Assistant:Message:Button:Downvote",
})
);
thumbDownButton.innerHTML = THUMB_DOWN_ICON_SVG;
@@ -324,7 +329,9 @@ export class ChatBubble {
this.references = references;
- const wrapper = createTag("div", { class: "chat-bubble-sources" });
+ const wrapper = createTag("div", {
+ class: "chat-bubble-sources",
+ });
const heading = createTag("p", { class: "chat-bubble-sources-heading" });
heading.textContent = "Sources:";
wrapper.appendChild(heading);
@@ -338,10 +345,14 @@ export class ChatBubble {
rel: "noopener noreferrer",
});
a.textContent = title || url;
+ a.setAttribute(
+ "daa-ll",
+ `DevsiteAI Assistant:Message:Sources:Link:${a.textContent}|${url}`,
+ );
li.appendChild(a);
list.appendChild(li);
});
wrapper.appendChild(list);
this.element.appendChild(wrapper);
}
-}
\ No newline at end of file
+}
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
index 2d81fd163..91d42651c 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
@@ -18,6 +18,34 @@ import {
updateSuggestedQuestions,
} from "./ai-assistant_suggested-questions.js";
+let userScrolledUp = false;
+let lastScrollTop = 0;
+
+/**
+ * Handles scroll events in the chat window to detect when the user scrolls up or back to the bottom.
+ * Sets/resets a flag to pause or resume auto-scrolling during streaming.
+ * Uses scroll direction rather than absolute position so programmatic downward scrolls
+ * during streaming don't mask the user's upward intent.
+ *
+ * @param {Event} event - The scroll event from the chat container element
+ */
+export const onUserScroll = (event) => {
+ if (event.type !== "scroll" || !event.target) {
+ return;
+ }
+ const container = /** @type {HTMLDivElement} */ (event.target);
+ const distanceFromBottom =
+ container.scrollHeight - container.clientHeight - container.scrollTop;
+ const scrolledUp = container.scrollTop < lastScrollTop;
+ lastScrollTop = container.scrollTop;
+
+ if (userScrolledUp && distanceFromBottom < 10) {
+ userScrolledUp = false;
+ } else if (!userScrolledUp && scrolledUp && distanceFromBottom > 100) {
+ userScrolledUp = true;
+ }
+};
+
/**
* @param {Partial<{delay: number}>} [options]
*/
@@ -166,6 +194,8 @@ export const handleUserQuery = async (
messageContentOverride,
collectionId = null,
) => {
+ userScrolledUp = false;
+ lastScrollTop = 0;
let messageContent = messageContentOverride;
const textarea = /** @type {HTMLTextAreaElement} */ (ELEMENTS.CHAT_TEXTAREA);
@@ -222,7 +252,10 @@ export const handleUserQuery = async (
targetBubble.hideThinking();
targetBubble.showStreamingCursor();
targetBubble.updateContent(responseContent);
- targetBubble.scrollIntoView();
+ if (!userScrolledUp && ELEMENTS.CHAT_WINDOW_CONTENT) {
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollTop =
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollHeight;
+ }
}
},
onCitation: (data) => {
@@ -245,7 +278,10 @@ export const handleUserQuery = async (
content: responseContent,
references,
});
- targetBubble.scrollIntoView();
+ if (!userScrolledUp && ELEMENTS.CHAT_WINDOW_CONTENT) {
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollTop =
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollHeight;
+ }
}
}
},
@@ -256,7 +292,11 @@ export const handleUserQuery = async (
responseContent = "_Response stopped by user._";
targetBubble.updateContent(responseContent);
updateSuggestedQuestions(await getCollectionsQuestions());
- window.setTimeout(showSuggestedQuestions, suggestedQuestionsDelayMs);
+ window.setTimeout(
+ () =>
+ showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
+ suggestedQuestionsDelayMs,
+ );
return;
}
targetBubble.completeBubble();
@@ -264,10 +304,17 @@ export const handleUserQuery = async (
content: responseContent,
references: accumulatedReferences,
});
- targetBubble.scrollIntoView();
+ if (!userScrolledUp && ELEMENTS.CHAT_WINDOW_CONTENT) {
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollTop =
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollHeight;
+ }
updateSuggestedQuestions(null);
- window.setTimeout(showSuggestedQuestions, suggestedQuestionsDelayMs);
+ window.setTimeout(
+ () =>
+ showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
+ suggestedQuestionsDelayMs,
+ );
await fetchAiSuggestedQuestions();
},
onError: (error) => {
@@ -276,7 +323,11 @@ export const handleUserQuery = async (
console.error("[AI Assistant] Error:", error);
showErrorMessage();
getCollectionsQuestions().then(updateSuggestedQuestions);
- window.setTimeout(showSuggestedQuestions, suggestedQuestionsDelayMs);
+ window.setTimeout(
+ () =>
+ showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
+ suggestedQuestionsDelayMs,
+ );
},
},
});
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js
index 997bcc138..9ca2b7e8e 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js
@@ -29,17 +29,24 @@ export const createAiAvatar = () => {
* Creates the chat window header
*/
export const createChatWindowHeader = () => {
- const chatWindowHeader = createTag("header", { class: "chat-window-header" });
+ const chatWindowHeader = createTag("header", {
+ class: "chat-window-header",
+ });
chatWindowHeader.appendChild(createAiAvatar());
const label = createTag("h2", {
class: "chat-window-label",
id: CHAT_WINDOW_LABEL_ID,
});
label.textContent = "Adobe Developer AI assistant";
+
+ const betaBadge = createTag("div", { class: "chat-window-badge" });
+ betaBadge.textContent = "Beta";
+
const clearButton = createTag("button", {
class: "chat-window-clear",
type: "button",
"aria-label": CHAT_BUTTON_LABEL_CLEAR,
+ "daa-ll": "DevsiteAI Assistant:Clear dialog:Open",
});
const clearButtonIcon = createTag("img", {
src: "/hlx_statics/icons/delete.svg",
@@ -52,6 +59,7 @@ export const createChatWindowHeader = () => {
class: "chat-window-close",
type: "button",
"aria-label": CHAT_BUTTON_LABEL_CLOSE,
+ "daa-ll": "DevsiteAI Assistant:Close",
});
const closeButtonIcon = createTag("img", {
src: "/hlx_statics/icons/dismiss.svg",
@@ -61,6 +69,8 @@ export const createChatWindowHeader = () => {
closeButton.appendChild(closeButtonIcon);
chatWindowHeader.appendChild(label);
+ chatWindowHeader.append(betaBadge);
+ chatWindowHeader.appendChild(createTag("div", { style: "flex: 1;" }));
chatWindowHeader.appendChild(clearButton);
chatWindowHeader.appendChild(closeButton);
ELEMENTS.CHAT_WINDOW_CLEAR_BUTTON = clearButton;
@@ -72,7 +82,9 @@ export const createChatWindowHeader = () => {
* Creates the input section
*/
export const createInputSection = () => {
- const inputSection = createTag("div", { class: "chat-window-input-section" });
+ const inputSection = createTag("div", {
+ class: "chat-window-input-section",
+ });
const textarea = /** @type {HTMLTextAreaElement} */ (
createTag("textarea", {
placeholder: "Type your message...",
@@ -81,13 +93,14 @@ export const createInputSection = () => {
})
);
const disclaimerText = createTag("div", { class: "chat-disclaimer-text" });
- disclaimerText.innerHTML = `By using AI Assistant, you agree to the Generative AI User Guidelines.`;
+ disclaimerText.innerHTML = `By using AI Assistant, you agree to the Generative AI User Guidelines.`;
const sendButton = /** @type {HTMLButtonElement} */ (
createTag("button", {
class: "chat-send-button",
type: "button",
"aria-label": "Send message",
+ "daa-ll": "DevsiteAI Assistant:Send message",
})
);
const sendButtonIcon = createTag("img", {
@@ -142,6 +155,7 @@ export const createChatButton = () => {
"aria-expanded": "false",
"aria-haspopup": "dialog",
"aria-label": CHAT_BUTTON_LABEL_OPEN,
+ "daa-ll": "DevsiteAI Assistant:Open",
});
chatButton.innerHTML = ``;
ELEMENTS.CHAT_BUTTON = chatButton;
@@ -151,7 +165,9 @@ export const createChatButton = () => {
export const createClearDialog = () => {
const dialog = createTag("div", { class: "chat-window-dialog" });
- const card = createTag("section", { class: "chat-window-dialog-card" });
+ const card = createTag("section", {
+ class: "chat-window-dialog-card",
+ });
const heading = createTag("h2", { class: "chat-window-dialog-heading" });
heading.textContent = "Clear conversation";
@@ -167,6 +183,7 @@ export const createClearDialog = () => {
const cancelButton = createTag("button", {
class: "chat-window-dialog-cancel",
type: "button",
+ "daa-ll": "DevsiteAI Assistant:Clear dialog:Cancel",
});
cancelButton.textContent = "Cancel";
cancelButton.addEventListener("click", () => dialog.remove());
@@ -174,6 +191,7 @@ export const createClearDialog = () => {
const clearButton = createTag("button", {
class: "chat-window-dialog-clear",
type: "button",
+ "daa-ll": "DevsiteAI Assistant:Clear dialog:Clear",
});
clearButton.textContent = "Clear";
clearButton.addEventListener("click", () => {
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js b/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
index 5095d4fc2..fb6090b1d 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
@@ -1,8 +1,4 @@
// @ts-check
-/** TODO: This should be different based on the environment */
-export const AI_API_BASE_URL =
- "https://devsite-rag.stg.app-builder.corp.adp.adobe.io";
-export const AI_API_KEY = "ai-assistant-devsite-rag-demo-01";
export const CHAT_BUTTON_LABEL_OPEN = "Open AI Assistant";
export const CHAT_BUTTON_LABEL_CLOSE = "Close AI Assistant";
export const CHAT_BUTTON_LABEL_MINIMIZE = "Minimize AI Assistant";
@@ -38,4 +34,4 @@ export const FALLBACK_SUGGESTED_QUESTIONS = [
export const GENERIC_ERROR_MESSAGE =
"Sorry, I encountered an error. Please try again later.";
export const SEND_ICON_SRC = "/hlx_statics/icons/send-message.svg";
-export const STOP_ICON_SRC = "/hlx_statics/icons/stop-response.svg";
\ No newline at end of file
+export const STOP_ICON_SRC = "/hlx_statics/icons/stop-response.svg";
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js b/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js
index 23cb262f0..b9603e947 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js
@@ -57,6 +57,7 @@ export const updateSuggestedQuestions = (questions) => {
const button = createTag("button", {
type: "button",
class: "chat-suggested-questions-button",
+ "daa-ll": `DevsiteAI Assistant:Suggested questions:${label}`,
});
const icon = createTag("img", {
src: "/hlx_statics/icons/arrow-curved.svg",
@@ -97,7 +98,9 @@ export const createSuggestedQuestionsSection = () => {
const wrapper = createTag("div", { class: "chat-suggested-questions" });
const title = createTag("p", { class: "chat-suggested-questions-title" });
title.textContent = "or choose from the following:";
- const list = createTag("div", { class: "chat-suggested-questions-list" });
+ const list = createTag("div", {
+ class: "chat-suggested-questions-list",
+ });
wrapper.appendChild(title);
wrapper.appendChild(list);
@@ -108,14 +111,24 @@ export const createSuggestedQuestionsSection = () => {
return wrapper;
};
-export const showSuggestedQuestions = () => {
+/**
+ * Shows the suggested questions section with optional scroll behavior.
+ * @param {Object} [options={}] - Options object
+ * @param {boolean} [options.shouldScrollIntoView=true] - Whether to scroll the element into view
+ */
+export const showSuggestedQuestions = ({
+ shouldScrollIntoView = true,
+} = {}) => {
const el = ELEMENTS.CHAT_SUGGESTED_QUESTIONS;
if (el) {
el.classList.remove("hidden");
el.classList.remove("animate-fade-in");
requestAnimationFrame(() => {
el.classList.add("animate-fade-in");
- el.scrollIntoView({ behavior: "smooth" });
+ if (shouldScrollIntoView && ELEMENTS.CHAT_WINDOW_CONTENT) {
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollTop =
+ ELEMENTS.CHAT_WINDOW_CONTENT.scrollHeight;
+ }
});
}
};
@@ -126,4 +139,4 @@ export const hideSuggestedQuestions = () => {
el.classList.remove("animate-fade-in");
el.classList.add("hidden");
}
-};
\ No newline at end of file
+};
diff --git a/hlx_statics/blocks/edition/edition.js b/hlx_statics/blocks/edition/edition.js
index 0bc4f687f..6a44dc806 100644
--- a/hlx_statics/blocks/edition/edition.js
+++ b/hlx_statics/blocks/edition/edition.js
@@ -7,12 +7,13 @@ export default async function decorate(block) {
const colorMap = {
'red': 'rgb(187, 2, 2)',
'green': 'rgb(0, 128, 0)',
- 'blue': 'rgb(4, 105, 227)'
+ 'blue': 'rgb(4, 105, 227)',
+ 'gray': 'rgb(71,71,71)'
};
// Get background color from data attribute or class name
let requestedColor = block.getAttribute('data-backgroundcolor')?.toLowerCase();
-
+
// If no data attribute, check for class name like 'background-color-blue'
if (!requestedColor) {
const classList = Array.from(block.classList);
@@ -21,7 +22,7 @@ export default async function decorate(block) {
requestedColor = colorClass.replace('background-color-', '');
}
}
-
+
const backgroundColor = colorMap[requestedColor] || colorMap['red'];
block.querySelectorAll('.edition > div > div').forEach((div) => {
diff --git a/hlx_statics/blocks/embed/embed.js b/hlx_statics/blocks/embed/embed.js
index 170614bf8..7b978c82f 100644
--- a/hlx_statics/blocks/embed/embed.js
+++ b/hlx_statics/blocks/embed/embed.js
@@ -35,7 +35,7 @@ const getDefaultEmbed = (url, loop, controls, vidTitle, isShort, autoplay) => {
params.push('mute=1');
}
const query = params.length ? `?${params.join('&')}` : '';
- const titleAttr = vidTitle ? `title="${vidTitle}"` : `title="Content from ${url.hostname}"`;
+ const titleAttr = `title="${vidTitle ? vidTitle : `Content from ${url.hostname}`}"`;
const embedHTML = `
+ title="${vidTitle ? vidTitle : `Content from ${url.hostname}`}" loading="lazy">
`;
return embedHTML;
};
diff --git a/hlx_statics/blocks/header/header.js b/hlx_statics/blocks/header/header.js
index 0fb4842ea..8c22d6d22 100644
--- a/hlx_statics/blocks/header/header.js
+++ b/hlx_statics/blocks/header/header.js
@@ -70,11 +70,25 @@ async function initSearch() {
const { connectAutocomplete } = instantsearch.connectors;
- const searchClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY);
+ const algoliaClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY);
const SUGGESTION_MAX_RESULTS = 50;
const SEARCH_MAX_RESULTS = 100;
- const SEARCH_MIN_QUERY_LENGTH = 3;
- const isSearchableQuery = (q) => q.trim().length >= SEARCH_MIN_QUERY_LENGTH;
+ const isSearchableQuery = (q) => q.trim() !== '';
+ const searchClient = { // "To prevent the initial empty query, you must wrap a custom search client around..." source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/js
+ ...algoliaClient,
+ search(requests) {
+ if (requests.every(({ params }) => !isSearchableQuery(params.query ?? ''))) {
+ return Promise.resolve({
+ results: requests.map(() => ({
+ hits: [], nbHits: 0, nbPages: 0, page: 0,
+ processingTimeMS: 0, hitsPerPage: 0,
+ exhaustiveNbHits: false, query: '', params: '',
+ })),
+ });
+ }
+ return algoliaClient.search(requests);
+ },
+ };
const indices = window.adp_search.indices
const indexToProduct = window.adp_search.index_to_product;
@@ -125,9 +139,17 @@ async function initSearch() {
let results = new Map();
search.start();
+
+ let currentDynamicWidgets = []; // Widgets that change on each call
+ let staticWidgetsAdded = false; // One-time widgets
// Function to initialize or update the search
function updateSearch() {
+ // Remove widgets from the previous call before adding new ones
+ if (currentDynamicWidgets.length) {
+ search.removeWidgets(currentDynamicWidgets);
+ currentDynamicWidgets = [];
+ }
// Get indices corresponding to selected products
const selectedIndices = indices.filter((indexName) => {
const product = indexToProduct[indexName];
@@ -143,14 +165,14 @@ async function initSearch() {
// Calculate hits dynamically based number of selected indices
const hits = Math.min(15, Math.max(4, Math.floor(SUGGESTION_MAX_RESULTS / selectedIndices.length)));
- // Add common widgets like hits per index and how long results are (content)
- search.addWidgets([
- instantsearch.widgets.configure({
- hitsPerPage: hits,
- attributesToHighlight: ['title', 'content'],
- attributesToSnippet: ['content:50'],
- }),
- ]);
+ // Add common widgets like hits per index and how long results are (content) - and save reference so it can be removed on the next call
+ const configureWidget = instantsearch.widgets.configure({
+ hitsPerPage: hits,
+ attributesToHighlight: ['title', 'content'],
+ attributesToSnippet: ['content:50'],
+ });
+ currentDynamicWidgets.push(configureWidget);
+ search.addWidgets([configureWidget]);
// Custom InstantSearch search box to deal with suggestions and full results which depends on user input
function customSearchBox() { return { init({ helper }) {
@@ -230,8 +252,12 @@ async function initSearch() {
if (event.key === 'Enter') {
searchCleared = false; // Reset cleared flag when user presses Enter
const trimmed = searchInput.value.trim();
- if (trimmed !== '' && !isSearchableQuery(trimmed)) {
- event.preventDefault();
+ if (trimmed === '') { // If users presses enter with an empty query while in a full search, clear results and show suggestions again
+ searchResults.classList.remove('has-results');
+ searchResults.style.visibility = 'hidden';
+ outerSearchSuggestions.style.display = 'flex';
+ suggestionsFlag = true;
+ searchExecuted = false;
return;
}
helper.setQuery(searchInput.value).search();
@@ -277,7 +303,8 @@ async function initSearch() {
// Process each hit
//
results.set(instantsearch.highlight({ hit, attribute: "title" }), {
- url: hit.url,
+ // Add anchor link if it exists to URL
+ url: hit.fragment ? `${hit.url}${hit.fragment}` : hit.url,
product: hit.product,
content: instantsearch.snippet({ hit, attribute: 'content' }),
});
@@ -298,21 +325,25 @@ async function initSearch() {
}
const customMergedHits = connectAutocomplete(mergedHits);
- search.addWidgets([
- customSearchBox(),
- customMergedHits({
- container: document.querySelector(searchBoxContainer)
- }),
- ]);
-
- // Loop through rest of indices
- selectedIndices.slice(1).forEach((indexName) => {
+ // Only add the search box and hits renderer once — fixes event listeners duplicates
+ if (!staticWidgetsAdded) {
search.addWidgets([
- instantsearch.widgets.index({
- indexName: indexName,
+ customSearchBox(),
+ customMergedHits({
+ container: document.querySelector(searchBoxContainer)
}),
]);
- });
+ staticWidgetsAdded = true;
+ }
+
+ // Instead of looping through other indices - add a child index widget for rest of indices (the main index is always searched, so it doesn't need a widget)
+ const indexWidgets = selectedIndices
+ .filter((indexName) => indexName !== initialIndex)
+ .map((indexName) => instantsearch.widgets.index({ indexName }));
+ if (indexWidgets.length) {
+ currentDynamicWidgets.push(...indexWidgets);
+ search.addWidgets(indexWidgets);
+ }
search.refresh();
}
@@ -405,24 +436,7 @@ async function initSearch() {
});
}
- // Function that is called after each search render to hide/show product checkboxes
- function updateCheckboxVisibility(productsWithResults) {
- const allSelected = selectedProducts.length === allProducts.length;
-
- // loop over each product‐wrapper
- document.querySelectorAll('.search-checkbox-div[data-product]').forEach((div) => {
- const product = div.dataset.product;
- if (allSelected) {
- // only show those with results
- div.style.display = productsWithResults.has(product) ? '' : 'none';
- } else {
- // specific‐product mode: show all product checkboxes
- div.style.display = '';
- }
- });
- }
-
- // Function that attaches event listeners to each checkbox
+// Function that attaches event listeners to each checkbox
function attachCheckboxEventListeners() {
const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productCheckboxes = document.querySelectorAll('.filters input[type="checkbox"]:not(#checkbox-all-products)');
@@ -447,6 +461,9 @@ async function initSearch() {
selectedProducts = Array.from(productCheckboxes)
.filter((cb) => cb.checked) // Get checked product checkboxes
.map((cb) => cb.value);
+ if (checkbox.checked) { // Add new selected product to the beginning of the list to prioritize it in results
+ selectedProducts = [checkbox.value, ...selectedProducts.filter((p) => p !== checkbox.value)];
+ }
if (selectedProducts.length === 0) {
// If no products selected, revert to "All Products"
@@ -476,18 +493,20 @@ async function initSearch() {
// figure out which products have at least one hit
const productsWithResults = new Set(productGroupedResults.keys());
- // hide/show checkboxes based on current results + mode
- updateCheckboxVisibility(productsWithResults);
-
// determine display order
const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productsToShow = allProductsCheckbox.checked ? allProducts : selectedProducts;
- const productsWith = [], productsWithout = [];
- productsToShow.forEach((p) =>
- productsWithResults.has(p) ? productsWith.push(p) : productsWithout.push(p)
- );
- const sorted = [...productsWith, ...productsWithout];
+ let sorted;
+ if (allProductsCheckbox.checked) {
+ const productsWith = [], productsWithout = [];
+ productsToShow.forEach((p) =>
+ productsWithResults.has(p) ? productsWith.push(p) : productsWithout.push(p)
+ );
+ sorted = [...productsWith, ...productsWithout];
+ } else {
+ sorted = productsToShow; // preserve selected order regardless of results
+ }
// render each group
sorted.forEach((product) => {
@@ -536,18 +555,22 @@ async function initSearch() {
// compute who has any suggestions
const productsWithResults = new Set(productGroupedResults.keys());
- updateCheckboxVisibility(productsWithResults);
const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productsToShow = allProductsCheckbox.checked
? allProducts
: selectedProducts;
- const withHits = [], withoutHits = [];
- productsToShow.forEach((p) =>
- productsWithResults.has(p) ? withHits.push(p) : withoutHits.push(p)
- );
- const sorted = [...withHits, ...withoutHits];
+ let sorted;
+ if (allProductsCheckbox.checked) {
+ const withHits = [], withoutHits = [];
+ productsToShow.forEach((p) =>
+ productsWithResults.has(p) ? withHits.push(p) : withoutHits.push(p)
+ );
+ sorted = [...withHits, ...withoutHits];
+ } else {
+ sorted = productsToShow; // preserve selected order regardless of results
+ }
// render each section
sorted.forEach((product) => {
@@ -1118,33 +1141,33 @@ export default async function decorate(block) {
// check if documentation template then retrieve from config otherwise default back to google drive path
let navPath;
- if (IS_DEV_DOCS) {
- const topNavHtml = await fetchTopNavHtml();
- if (topNavHtml) {
- navigationLinks.innerHTML += topNavHtml;
- }
- } else {
- navPath = cfg.nav || getClosestFranklinSubfolder(window.location.origin,'nav');
- let fragment = await loadFragment(navPath);
- if (fragment == null) {
- // load the default nav in franklin_assets folder nav
- fragment = await loadFragment(getClosestFranklinSubfolder(window.location.origin, 'nav', true));
- }
- const ul = fragment.querySelector("ul");
- ul.classList.add("menu");
- ul.setAttribute("id", "navigation-links");
- fragment.querySelectorAll("li").forEach((li, index) => {
- if (index == 0) {
- if (isTopLevelNav(window.location.pathname)) {
- const homeLink = ul.querySelector('li:nth-child(1)');
- homeLink.className = 'navigation-home';
- } else {
- li.classList.add("navigation-products");
- }
- }
- });
- navigationLinks = ul;
- }
+ // if (IS_DEV_DOCS) {
+ // const topNavHtml = await fetchTopNavHtml();
+ // if (topNavHtml) {
+ // navigationLinks.innerHTML += topNavHtml;
+ // }
+ // } else {
+ // navPath = cfg.nav || getClosestFranklinSubfolder(window.location.origin,'nav');
+ // let fragment = await loadFragment(navPath);
+ // if (fragment == null) {
+ // // load the default nav in franklin_assets folder nav
+ // fragment = await loadFragment(getClosestFranklinSubfolder(window.location.origin, 'nav', true));
+ // }
+ // const ul = fragment.querySelector("ul");
+ // ul.classList.add("menu");
+ // ul.setAttribute("id", "navigation-links");
+ // fragment.querySelectorAll("li").forEach((li, index) => {
+ // if (index == 0) {
+ // if (isTopLevelNav(window.location.pathname)) {
+ // const homeLink = ul.querySelector('li:nth-child(1)');
+ // homeLink.className = 'navigation-home';
+ // } else {
+ // li.classList.add("navigation-products");
+ // }
+ // }
+ // });
+ // navigationLinks = ul;
+ // }
navigationLinks.querySelectorAll('li > ul').forEach((dropDownList, index) => {
let dropdownLinkDropdownHTML = '';
diff --git a/hlx_statics/blocks/info-card/info-card.css b/hlx_statics/blocks/info-card/info-card.css
index 710be2862..566cb821c 100644
--- a/hlx_statics/blocks/info-card/info-card.css
+++ b/hlx_statics/blocks/info-card/info-card.css
@@ -36,6 +36,14 @@ main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard)
background-color: white;
}
+main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard) .cards-card-body > :is(h1, h2, h3, h4, h5, h6) {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ line-clamp: 2;
+ -webkit-line-clamp: 2;
+}
+
main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard) .cards-card-image {
line-height: 0;
}
@@ -73,6 +81,14 @@ main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard)
color: rgb(0, 0, 0);
}
+main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard) .cards-card-body > p {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ line-clamp: 3;
+ -webkit-line-clamp: 3;
+}
+
main :is(div.info-card-wrapper div.info-card, div.infocard-wrapper div.infocard) p {
color: rgb(71, 71, 71) !important;
}
diff --git a/hlx_statics/blocks/onthispage/onthispage.js b/hlx_statics/blocks/onthispage/onthispage.js
index 6ada92ebe..1eea26d55 100644
--- a/hlx_statics/blocks/onthispage/onthispage.js
+++ b/hlx_statics/blocks/onthispage/onthispage.js
@@ -16,7 +16,7 @@ export default async function decorate(block) {
block.append(aside);
const mainContainer = document.querySelector('main');
- const headings = mainContainer.querySelectorAll('h2:not(.side-nav h2):not(footer h2), h3:not(.side-nav h3):not(footer h3)');
+ const headings = mainContainer.querySelectorAll('.heading2:not(.side-nav .heading2):not(footer .heading2) h2, .heading3:not(.side-nav .heading3):not(footer .heading3) h3');
Object.assign(aside.style, {
display: 'flex',
diff --git a/hlx_statics/blocks/superhero/superhero.css b/hlx_statics/blocks/superhero/superhero.css
index 05e5911e1..9a685b9d1 100644
--- a/hlx_statics/blocks/superhero/superhero.css
+++ b/hlx_statics/blocks/superhero/superhero.css
@@ -1,3 +1,28 @@
+main div.superhero-wrapper div.superhero a:not(.spectrum-Button) {
+ color: inherit !important;
+ text-decoration: underline;
+}
+
+main div.superhero-wrapper div.superhero.text-color-white,
+main div.superhero-wrapper div.superhero.text-color-white :is(h1, h2, h3, h4, h5, h6, p) {
+ color: rgb(255, 255, 255) !important;
+}
+
+main div.superhero-wrapper div.superhero.text-color-black,
+main div.superhero-wrapper div.superhero.text-color-black :is(h1, h2, h3, h4, h5, h6, p) {
+ color: rgb(0, 0, 0) !important;
+}
+
+main div.superhero-wrapper div.superhero.text-color-gray,
+main div.superhero-wrapper div.superhero.text-color-gray :is(h1, h2, h3, h4, h5, h6, p) {
+ color: rgb(110, 110, 110) !important;
+}
+
+main div.superhero-wrapper div.superhero.text-color-navy,
+main div.superhero-wrapper div.superhero.text-color-navy :is(h1, h2, h3, h4, h5, h6, p) {
+ color: rgb(15, 55, 95) !important;
+}
+
main div.superhero-wrapper div.superhero span.icon {
display: inline-block;
}
@@ -159,7 +184,6 @@ main div.superhero-wrapper div.superhero.half-width > div:last-child video {
main div.superhero-wrapper div.superhero.half-width p.spectrum-Body--sizeL {
margin-top: 0 !important;
- color: rgb(80, 80, 80) !important;
}
main div.superhero-wrapper:has(div.superhero.half-width) {
@@ -208,20 +232,6 @@ main div.superhero-wrapper div.superhero.half-width h1 + p.last-of-type {
margin-bottom: 0 !important;
}
-main div.superhero-wrapper div.superhero.half-width a:not(.spectrum-Button) {
- text-decoration: underline;
-}
-
-main div.superhero-wrapper div.superhero.half-width.text-color-white h1,
-main div.superhero-wrapper div.superhero.half-width.text-color-white a,
-main div.superhero-wrapper div.superhero.half-width.text-color-white p {
- color: white !important;
-}
-
-main div.superhero-wrapper div.superhero.half-width.text-color-black {
- color: black;
-}
-
main div.superhero-wrapper div.superhero.half-width.over-gradient p.button-container:not(strong) a:not(.spectrum-Button--accent) {
border-color: white;
}
@@ -296,22 +306,6 @@ main div.superhero-wrapper div.superhero.default .all-button-container {
gap: 16px;
}
-main div.superhero-wrapper div.superhero.default.text-color-white {
- color: rgb(255, 255, 255);
-}
-
-main div.superhero-wrapper div.superhero.default.text-color-black {
- color: rgb(0, 0, 0);
-}
-
-main div.superhero-wrapper div.superhero.default.text-color-gray {
- color: rgb(110, 110, 110);
-}
-
-main div.superhero-wrapper div.superhero.default.text-color-navy {
- color: rgb(15, 55, 95);
-}
-
@media screen and (max-width: 1280px) {
main div.superhero-wrapper div.superhero.default > div > div {
width: 100%;
diff --git a/hlx_statics/blocks/superhero/superhero.js b/hlx_statics/blocks/superhero/superhero.js
index e01a9b3f2..34c2dfba3 100644
--- a/hlx_statics/blocks/superhero/superhero.js
+++ b/hlx_statics/blocks/superhero/superhero.js
@@ -45,6 +45,10 @@ function hasAnyClass(block, classes) {
return classes.some((c) => block.classList.contains(c));
}
+function getTextColorModifier(block) {
+ return Object.values(TEXT_COLORS).find((color) => block.classList.contains(`text-color-${color}`));
+}
+
function unwrapIcons(block) {
block.querySelectorAll('span.icon').forEach((span) => {
span.textContent = '';
@@ -56,6 +60,7 @@ function unwrapIcons(block) {
async function decorateDevBizCentered(block) {
const defaultTextColor = TEXT_COLORS.white;
+ const textColor = getTextColorModifier(block);
removeEmptyPTags(block);
decorateButtons(block);
@@ -69,7 +74,7 @@ async function decorateDevBizCentered(block) {
block.classList.add('spectrum--dark');
block.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((h) => {
h.classList.add('spectrum-Heading', 'spectrum-Heading--sizeXXL');
- h.style.color = defaultTextColor;
+ if (!textColor) h.style.color = defaultTextColor;
h.parentElement.classList.add('superhero-content');
h.parentElement.append(button_div);
});
@@ -77,7 +82,7 @@ async function decorateDevBizCentered(block) {
block.querySelectorAll('p').forEach((p) => {
if (!p.classList.contains('icon-container')) {
p.classList.add('spectrum-Body', 'spectrum-Body--sizeL');
- p.style.color = defaultTextColor;
+ if (!textColor) p.style.color = defaultTextColor;
}
if (p.classList.contains('button-container')) {
button_div.append(p);
@@ -136,7 +141,7 @@ async function decorateDevBizHalfWidth(block) {
const muted = !isControl || wantAutoplay;
const videoContainer = createTag('div', { class: 'superhero-video-container' });
- const videoTag = ``;
+ const videoTag = ``;
videoContainer.innerHTML = videoTag;
block.lastElementChild.replaceWith(videoContainer);
}
@@ -189,9 +194,8 @@ async function decorateDevBizDefault(block) {
div.append(...newChildren);
block.replaceChildren(div);
- const defaultTextColor = TEXT_COLORS.white;
- let textColor = Object.values(TEXT_COLORS).find((color) => block.classList.contains(`text-color-${color}`)) ?? defaultTextColor;
- block.classList.add(`text-color-${defaultTextColor}`);
+ const textColor = getTextColorModifier(block) ?? TEXT_COLORS.white;
+ block.classList.add(`text-color-${textColor}`);
block.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((h) => {
h.style.color = textColor;
@@ -356,14 +360,6 @@ function applyDataAttributeStyles(block) {
} else {
block.style.background = background;
}
-
- const defaultTextColor = variant === VARIANTS.halfWidth ? TEXT_COLORS.black : TEXT_COLORS.white;
- const textColor = block.getAttribute('data-textcolor') || defaultTextColor;
- if (Object.keys(TEXT_COLORS).includes(textColor)) {
- block.querySelectorAll('h1, h2, h3, h4, h5, h6, p').forEach((el) => {
- el.style.color = textColor;
- });
- }
}
/**
diff --git a/hlx_statics/blocks/tab/tab.css b/hlx_statics/blocks/tab/tab.css
index 812fd96b2..69c2d663e 100644
--- a/hlx_statics/blocks/tab/tab.css
+++ b/hlx_statics/blocks/tab/tab.css
@@ -123,6 +123,10 @@ main div.tab-wrapper .code-toolbar {
main div.tab-wrapper .tab.vertical {
flex-direction: row;
+ align-items: stretch;
+ width: 100%;
+ margin: 0;
+ gap: 0;
}
main div.tab-wrapper .tab.vertical .tabs-wrapper,
@@ -130,14 +134,55 @@ main div.tab-wrapper .tab.horizontal {
flex-direction: column;
}
-main div.tab-wrapper .tab.vertical>.tabs-wrapper {
+main div.tab-wrapper .tab.vertical > .tabs-wrapper {
display: flex;
flex-direction: column;
- width: 350px;
+ width: 240px;
+ min-width: 180px;
+ flex-shrink: 0;
+ gap: 4px;
+ padding: 16px 12px;
+ margin-bottom: 0;
+ box-sizing: border-box;
+ border-right: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+main div.tab-wrapper .tab.vertical > .content-wrapper {
+ flex: 1;
+ min-width: 0;
+ padding: 16px 24px;
+ width: auto;
+}
+
+main div.tab-wrapper .tab.vertical .tab-button {
+ width: 100%;
+ height: auto;
+ min-height: 54px;
+ margin: 0;
+ padding: 10px 14px;
+ border-radius: 8px;
+ text-align: left;
+ white-space: normal;
+ line-height: 1.35;
+ font-size: 14px;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 12px;
+}
+
+main div.tab-wrapper .tab.vertical .tab-icon {
+ width: 36px;
+ height: 36px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-main div.tab-wrapper .tab.vertical>.content-wrapper {
- width: 80%;
+main div.tab-wrapper .tab.vertical .tab-icon img {
+ width: 36px;
+ height: 36px;
+ object-fit: contain;
}
main div.tab-wrapper .tab {
@@ -162,10 +207,13 @@ main div.tab-wrapper > div.background-color-navy .tab-button:not(.active) .tab-t
}
main div.tab-wrapper > div.background-color-navy .tab-button:hover .tab-title {
- color: #000;
+ color: white;
+}
+
+main div.tab-wrapper > div:not(.background-color-navy) .tab-button {
+ background: #eee;
}
-main div.tab-wrapper > div:not(.background-color-navy) .tab-button ,
main div.tab-wrapper > div.background-color-navy .tab-button:hover,
main div.tab-wrapper > div.background-color-navy .tab-button.active {
background: #eee;
@@ -220,4 +268,77 @@ main div.tab-wrapper .sub-tabs-wrapper>pre[class*=language-].line-numbers {
main div.tab-wrapper div.tab pre[class*=language-].no-line-numbers .line-highlight {
transform: translateY(-1.6em);
+}
+
+
+main div.tab-wrapper:has(.tab.vertical.background-color-navy) {
+ background-color: rgb(15, 55, 95);
+ padding: 0;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy > .tabs-wrapper {
+ background-color: rgb(15, 55, 95);
+ border-right-color: rgba(255, 255, 255, 0.15);
+ padding: 20px 12px;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-title {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-button {
+ background: transparent;
+ border: none;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-button:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ color: white;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-button.active {
+ background-color: rgba(255, 255, 255, 0.18);
+ color: white;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-button.active .tab-title,
+main div.tab-wrapper .tab.vertical.background-color-navy .tab-button:hover .tab-title {
+ color: white;
+}
+
+main div.tab-wrapper .tab.vertical.background-color-navy > .content-wrapper {
+ background-color: #111a35;
+ padding: 20px 24px;
+}
+
+@media only screen and (max-width: 860px) {
+ main div.tab-wrapper .tab.vertical {
+ flex-direction: column;
+ }
+
+ main div.tab-wrapper .tab.vertical > .tabs-wrapper {
+ width: 100%;
+ min-width: unset;
+ flex-direction: row;
+ flex-wrap: wrap;
+ border-right: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 8px;
+ }
+
+ main div.tab-wrapper .tab.vertical.background-color-navy > .tabs-wrapper {
+ border-bottom-color: rgba(255, 255, 255, 0.15);
+ border-right: none;
+ }
+
+ main div.tab-wrapper .tab.vertical .tab-button {
+ width: auto;
+ min-height: unset;
+ white-space: nowrap;
+ }
+
+ main div.tab-wrapper .tab.vertical > .content-wrapper {
+ padding: 12px 16px;
+ width: 100%;
+ }
}
\ No newline at end of file
diff --git a/hlx_statics/blocks/tab/tab.js b/hlx_statics/blocks/tab/tab.js
index 22549b1cf..38216e799 100644
--- a/hlx_statics/blocks/tab/tab.js
+++ b/hlx_statics/blocks/tab/tab.js
@@ -86,11 +86,22 @@ const createSubTabs = (table) => {
}
export default async function decorate(block) {
- let orientation;
- if (IS_DEV_DOCS) {
- orientation = block.getAttribute('data-orientation') || 'horizontal';
+ block.querySelectorAll(':scope > div > div > pre > code').forEach((code) => {
+ const match = code.textContent.trim().match(/^(data-[^=]+)=(.*)$/);
+ if (!match) return;
+ const [, attr, value] = match;
+ if (attr === 'data-orientation') {
+ block.setAttribute('data-orientation', value.trim());
+ } else if (attr === 'data-classname') {
+ value.trim().split(/\s+/).filter(Boolean).forEach((cls) => block.classList.add(cls));
+ }
+ });
+
+ const dataOrientation = block.getAttribute('data-orientation');
+ const orientation = dataOrientation || (block.classList.contains('vertical') ? 'vertical' : 'horizontal');
+ if (!block.classList.contains(orientation)) {
+ block.classList.add(orientation);
}
- block.classList.add(orientation);
block.setAttribute('daa-lh', 'tab');
const tabsWrapper = document.createElement('div');
@@ -112,7 +123,7 @@ export default async function decorate(block) {
const tabButton = document.createElement('button');
tabButton.className = 'tab-button';
tabButton.innerHTML = `
- ${tabImage}
+ ${tabImage ? `${tabImage}
` : ''}
${tabTitle}
`;
tabButton.setAttribute('data-tab', `tab${tabCount}`);
diff --git a/hlx_statics/components/code.js b/hlx_statics/components/code.js
index 9033e010f..caf41b7b9 100644
--- a/hlx_statics/components/code.js
+++ b/hlx_statics/components/code.js
@@ -106,7 +106,7 @@ function applyDataAttributesFromCodeClasses(pre, code) {
parts.forEach((item) => {
if (!item.includes('language-') && item.includes('=')) {
- const match = item.match(/^-?([^=]+)="?([^"]*)"?$/);
+ const match = item.match(/^-?([^=]+)="([^"]*)"/);
if (match) {
const attrName = `data-${match[1]}`;
const attrValue = match[2];
@@ -119,7 +119,10 @@ function applyDataAttributesFromCodeClasses(pre, code) {
code.classList.remove(cls);
pre.classList.remove(cls);
if (languagePart) {
- const cleanClass = languagePart.trim();
+ let cleanClass = languagePart.trim();
+ if (cls.includes('disableLineNumbers') && !cleanClass.includes('disableLineNumbers')) {
+ cleanClass += '-disableLineNumbers';
+ }
code.classList.add(cleanClass);
pre.classList.add(cleanClass);
}
diff --git a/hlx_statics/scripts/scripts.js b/hlx_statics/scripts/scripts.js
index 0358d0009..11de7d00f 100644
--- a/hlx_statics/scripts/scripts.js
+++ b/hlx_statics/scripts/scripts.js
@@ -770,7 +770,8 @@ async function loadLazy(doc) {
const hasResources = Boolean(document.querySelector('.resources-wrapper'));
const hasCredential = Boolean(document.querySelector('.getcredential-wrapper'));
const hasHeading = main.querySelectorAll('h2:not(.side-nav h2):not(footer h2), h3:not(.side-nav h3):not(footer h3)').length !== 0;
- const hasOnThisPage = !hasHero && hasHeading && !hasCredential;
+ const hideOnThisPage = document.querySelector('meta[name="hideonthispage"]')?.content === 'true';
+ const hasOnThisPage = !hasHero && hasHeading && !hasCredential && !hideOnThisPage;
const hasAside = hasOnThisPage || hasResources;
if (hasAside) {