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/code/code.css b/hlx_statics/blocks/code/code.css index 0cc3f08a9..d47e276e4 100644 --- a/hlx_statics/blocks/code/code.css +++ b/hlx_statics/blocks/code/code.css @@ -1,9 +1,15 @@ main div.code-wrapper pre[class*=language-], main div.nested-code-wrapper pre[class*=language-] { border-radius: 4px; + padding-top: 3.5em; +} + +main div.code-wrapper pre[class*=language-] .line-highlight, +main div.nested-code-wrapper pre[class*=language-] .line-highlight { + margin-top: 3.5em; } main div.code-wrapper pre[class*=language-].no-line-numbers .line-highlight, main div.nested-code-wrapper pre[class*=language-].no-line-numbers .line-highlight { transform: translateY(-1.6em); -} \ No newline at end of file +} diff --git a/hlx_statics/blocks/codeblock/codeblock.css b/hlx_statics/blocks/codeblock/codeblock.css index 352b8ed9d..9d9333f87 100644 --- a/hlx_statics/blocks/codeblock/codeblock.css +++ b/hlx_statics/blocks/codeblock/codeblock.css @@ -1,6 +1,11 @@ main div.codeblock-wrapper pre[class*="language-"] { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; + padding-top: 3.5em; +} + +main div.codeblock-wrapper pre[class*="language-"] .line-highlight { + margin-top: 3.5em; } main div.codeblock-wrapper { 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 = `
`; loadScript("https://www.instagram.com/embed.js"); @@ -97,7 +97,7 @@ const embedYTPlaylist = (url, loop, controls, vidTitle, isShort, autoplay) => { const embedHTML = `
`; return embedHTML; @@ -106,7 +106,7 @@ const embedTikTok = (url, loop, controls, vidTitle, isShort, autoplay) => { const [, vidID] = url.pathname.split('video/') return `
`; } @@ -170,7 +170,7 @@ const embedVimeo = (url, loop, controls, vidTitle, isShort, autoplay) => { style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;" frameborder="0" allow="fullscreen; encrypted-media; accelerometer; gyroscope; picture-in-picture" allowfullscreen - ${vidTitle ? `title=${vidTitle}` : `title="Content from" ${url.hostname}`} loading="lazy"> + 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..6373d5efc 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) => { 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..30f3729b1 100644 --- a/hlx_statics/blocks/tab/tab.css +++ b/hlx_statics/blocks/tab/tab.css @@ -32,6 +32,14 @@ main div.tab-wrapper > div:not(.background-color-navy) .tab-button:hover { color: white; } +main div.tab-wrapper div.code-toolbar pre.line-numbers { + padding-top: 3.5em; +} + +main div.tab-wrapper div.code-toolbar pre.line-numbers .line-highlight { + margin-top: 3.5em; +} + main div.tab-wrapper div.code-toolbar>.toolbar>.toolbar-item>button { padding: 12px; height: 32px; @@ -55,7 +63,6 @@ main div.tab-wrapper .line-numbers-rows>span { @media only screen and (max-width: 860px) { main div.tab-wrapper div.code-toolbar>.toolbar>.toolbar-item>button { - padding: 0; height: 25px; right: 0px; } @@ -123,6 +130,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 +141,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 +214,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; @@ -175,7 +230,7 @@ main div.tab-wrapper div.background-color-navy .code-toolbar pre { background-color: #111a35; border-radius: 2vh; } - + main div.sub-content-wrapper .code-toolbar pre { border-radius: 0%; } @@ -220,4 +275,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); -} \ No newline at end of file +} + + +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%; + } +} 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) {