diff --git a/head.html b/head.html
index 76959fe1b..1c735b527 100644
--- a/head.html
+++ b/head.html
@@ -1,6 +1,6 @@
+
-
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant.css b/hlx_statics/blocks/ai-assistant/ai-assistant.css
index 8820c3e3e..9d590fb01 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant.css
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant.css
@@ -39,6 +39,7 @@
*/
.ai-assistant-panel .chat-button {
--background-color: #ffffff;
+ --transition-delay: calc(var(--opening-transition-duration) * 0.9);
position: relative;
width: var(--button-size);
@@ -54,9 +55,11 @@
var(--ai-border-linear-gradient) border-box;
align-self: flex-end;
pointer-events: auto;
+ transform: scale(1);
visibility: visible;
- transition: visibility 0s linear
- calc(var(--opening-transition-duration) * 0.9);
+ transition:
+ visibility 0s linear var(--transition-delay),
+ transform 0.15s cubic-bezier(0, 0, 0.4, 1) var(--transition-delay);
&:hover {
--background-color: #f0f0f0;
@@ -65,9 +68,36 @@
&.hidden {
visibility: hidden;
transition: visibility 0s linear;
+ transform: scale(0.9);
}
}
+.ai-assistant-panel .chat-button .chat-button-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ background: var(--ai-border-linear-gradient);
+ color: #fff;
+ font-size: 9px;
+ font-weight: 800;
+ letter-spacing: 0.3px;
+ padding: 2px 6px;
+ border-radius: 8px;
+ border: 2px solid #fff;
+ pointer-events: none;
+ line-height: 1.4;
+ opacity: 1;
+ transform: scale(1);
+ transition:
+ opacity 0.15s cubic-bezier(0, 0, 0.4, 1),
+ transform 0.15s cubic-bezier(0, 0, 0.4, 1);
+}
+
+.ai-assistant-panel .chat-button .chat-button-badge.hidden {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
/*
* MARK: Chat Window
*/
@@ -422,6 +452,7 @@
justify-content: center;
padding: 24px;
box-sizing: border-box;
+ z-index: 11;
}
.ai-assistant-panel .chat-window .chat-window-dialog-card {
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 1bfa0d076..ed7c7cb5f 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
@@ -396,7 +396,7 @@ export class AiApiClient {
callbacks = {},
}) {
const defaultSystemPrompt = `
- Use markdown formatting for the response.
+ Use markdown formatting and codeblocks for the response.
`;
/** @type {RequestBody} */
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 b16486dcf..edf349817 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
@@ -1,5 +1,7 @@
// @ts-check
import { createTag } from "../../scripts/lib-adobeio.js";
+import decoratePreformattedCode from "../../components/code.js";
+import { ensurePrismLoaded } from "../../scripts/prism-loader.js";
import { aiApiClient } from "./ai-assistant_api-client.js";
import { chatHistory } from "./ai-assistant_chat-history.js";
import { createAiAvatar } from "./ai-assistant_chat-ui.js";
@@ -74,6 +76,8 @@ export class ChatBubble {
bubble.dataset.messageId = this.id;
// If we have an ID already (e.g., restored from history), append the actions
contentElement.appendChild(this._actionsContainer);
+ // Restored-history bubbles never call completeBubble(), so decorate here.
+ this.#_decorateCodeBlocks(contentElement);
}
// Otherwise, the actions will be appended when completeBubble is called
}
@@ -301,7 +305,7 @@ export class ChatBubble {
}
/**
- * Adds the missing actions to an AI bubble
+ * Final processing that needs to be done after streaming finishes.
*/
completeBubble() {
if (this.source !== "ai" || !this._actionsContainer) return;
@@ -310,6 +314,54 @@ export class ChatBubble {
const contentElement = this.element.querySelector(".chat-bubble-content");
contentElement?.appendChild(this._actionsContainer);
+ if (contentElement) {
+ this.#_decorateCodeBlocks(contentElement);
+ }
+ }
+
+ /**
+ * @param {Element} contentElement - The `.chat-bubble-content` element
+ */
+ #_decorateCodeBlocks(contentElement) {
+ const preBlocks = contentElement.querySelectorAll("pre");
+ if (!preBlocks.length) return;
+
+ let decoratedAny = false;
+ preBlocks.forEach((pre) => {
+ // decoratePreformattedCode dereferences a child unconditionally.
+ if (!pre.querySelector("code")) return;
+ // The chat panel is narrow, so use the icon-only copy button.
+ pre.classList.add("copy-condensed");
+ decoratePreformattedCode(pre);
+ decoratedAny = true;
+ });
+
+ if (!decoratedAny) return;
+
+ ensurePrismLoaded().then(() => {
+ // @ts-expect-error - Prism is not on the Window type
+ window.Prism?.highlightAllUnder?.(contentElement);
+ });
+ }
+
+ /**
+ * Recomputes Prism's line-number row heights for every decorated code block
+ * inside a container.
+ * This is required to correctly align line numbers after restoring history.
+ * @param {Element | null | undefined} container
+ */
+ static resizeCodeBlockLineNumbers(container) {
+ const preBlocks = container?.querySelectorAll("pre.line-numbers");
+ if (!preBlocks?.length) return;
+
+ ensurePrismLoaded().then(() => {
+ // @ts-expect-error - Prism is not on the Window type
+ const resize = window.Prism?.plugins?.lineNumbers?.resize;
+ if (typeof resize !== "function") return;
+ preBlocks.forEach((pre) => {
+ resize(pre);
+ });
+ });
}
/**
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 91d42651c..e59fe4f23 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
@@ -7,11 +7,11 @@ import {
CHAT_BUTTON_LABEL_OPEN,
ELEMENTS,
GENERIC_ERROR_MESSAGE,
+ INITIAL_SUGGESTED_QUESTIONS,
SEND_ICON_SRC,
STOP_ICON_SRC,
} from "./ai-assistant_constants.js";
import {
- getCollectionsQuestions,
hideSuggestedQuestions,
parseAiSuggestedQuestions,
showSuggestedQuestions,
@@ -51,6 +51,7 @@ export const onUserScroll = (event) => {
*/
const sendInitialMessages = ({ delay = 250 } = {}) => {
hideSuggestedQuestions();
+ updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(() => {
sendMessage({
content: "Hello, welcome to Adobe Developer Website!",
@@ -81,6 +82,26 @@ export const openChatWindow = () => {
ELEMENTS.CHAT_BUTTON.ariaLabel = CHAT_BUTTON_LABEL_MINIMIZE;
ELEMENTS.CHAT_WINDOW.classList.add("show");
ELEMENTS.CHAT_BUTTON.classList.add("hidden");
+ ELEMENTS.CHAT_BUTTON_BADGE?.classList.add("hidden");
+
+ // Code blocks in restored-history bubbles are decorated while the window is
+ // hidden (scaled to 0), so Prism's line-number rows collapse onto line 1.
+ // Recompute them once the open transition settles and the window has real
+ // layout. transitionend is the precise signal; the timeout is a fallback in
+ // case the transition is skipped (e.g. reduced motion / 0s duration).
+ const chatWindow = ELEMENTS.CHAT_WINDOW;
+ let fixedLineNumbers = false;
+ const fixLineNumbers = (event) => {
+ // transitionend bubbles, so ignore descendant transitions and only react to
+ // the window's own transform settling (or the timeout, which has no event).
+ if (event && (event.target !== chatWindow || event.propertyName !== "transform")) return;
+ if (fixedLineNumbers) return;
+ fixedLineNumbers = true;
+ chatWindow.removeEventListener("transitionend", fixLineNumbers);
+ ChatBubble.resizeCodeBlockLineNumbers(ELEMENTS.CHAT_WINDOW_CONTENT);
+ };
+ chatWindow.addEventListener("transitionend", fixLineNumbers);
+ window.setTimeout(fixLineNumbers, 400);
// Initial messages
if (chatHistory.isEmpty()) {
@@ -96,6 +117,44 @@ export const minimizeChatWindow = () => {
ELEMENTS.CHAT_BUTTON.ariaLabel = CHAT_BUTTON_LABEL_OPEN;
ELEMENTS.CHAT_BUTTON?.classList.remove("hidden");
ELEMENTS.CHAT_WINDOW?.classList.remove("show");
+
+ restoreBadgeAfterClose();
+};
+
+/**
+ * Restores the beta badge after the chat window's collapse (transform) transition
+ * completes.
+ */
+const restoreBadgeAfterClose = () => {
+ const chatWindow = ELEMENTS.CHAT_WINDOW;
+ if (!chatWindow) return;
+
+ // With reduced motion the window collapses instantly (no transform transition),
+ // so `transitionend` may never fire — restore the badge right away instead.
+ const prefersReducedMotion = window.matchMedia?.(
+ "(prefers-reduced-motion: reduce)",
+ )?.matches;
+ if (prefersReducedMotion) {
+ if (!chatWindow.classList.contains("show")) {
+ ELEMENTS.CHAT_BUTTON_BADGE?.classList.remove("hidden");
+ }
+ return;
+ }
+
+ /** @param {TransitionEvent} event */
+ const onTransitionEnd = (event) => {
+ // The window animates both `transform` and `visibility`; only react to the
+ // transform transition, which is the visible collapse.
+ if (event.target !== chatWindow || event.propertyName !== "transform") {
+ return;
+ }
+ chatWindow.removeEventListener("transitionend", onTransitionEnd);
+ if (!chatWindow.classList.contains("show")) {
+ ELEMENTS.CHAT_BUTTON_BADGE?.classList.remove("hidden");
+ }
+ };
+
+ chatWindow.addEventListener("transitionend", onTransitionEnd);
};
export const clearConversation = () => {
@@ -122,7 +181,7 @@ export const toggleChatWindow = () => {
* Falls back to static questions on any error or parse failure.
*/
export const fetchAiSuggestedQuestions = async () => {
- const query = `Please suggest 2 follow-up questions based on our conversation to make the users.`;
+ const query = `Please suggest 2 follow-up questions based on our conversation to make the users happy.`;
const systemPrompt = `
Structured questions format:
---question---
@@ -142,14 +201,14 @@ export const fetchAiSuggestedQuestions = async () => {
if (parsed.length > 0) {
updateSuggestedQuestions(parsed);
} else {
- updateSuggestedQuestions(await getCollectionsQuestions());
+ updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
}
} catch (error) {
console.warn(
"[AI Assistant] Failed to fetch AI suggested questions:",
error,
);
- updateSuggestedQuestions(await getCollectionsQuestions());
+ updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
}
};
@@ -291,7 +350,7 @@ export const handleUserQuery = async (
targetBubble.hideThinking();
responseContent = "_Response stopped by user._";
targetBubble.updateContent(responseContent);
- updateSuggestedQuestions(await getCollectionsQuestions());
+ updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(
() =>
showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
@@ -322,7 +381,7 @@ export const handleUserQuery = async (
// TODO: Log error somehow somewhere?
console.error("[AI Assistant] Error:", error);
showErrorMessage();
- getCollectionsQuestions().then(updateSuggestedQuestions);
+ updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(
() =>
showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
@@ -415,9 +474,11 @@ export const restoreChatHistory = async () => {
}
const lastMessage = chatHistory.getAll().pop();
if (lastMessage?.source === "ai") {
- updateSuggestedQuestions(await getCollectionsQuestions());
+ updateSuggestedQuestions(
+ chatHistory.getSuggestedQuestions() ?? INITIAL_SUGGESTED_QUESTIONS,
+ );
showSuggestedQuestions();
} else {
hideSuggestedQuestions();
}
-};
\ No newline at end of file
+};
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-history.js b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-history.js
index de70cf4c3..f0b686d72 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-history.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-history.js
@@ -16,43 +16,81 @@
* @property {{type: 'THUMBS_UP_DOWN'; score: 0|1}} [feedback]
*/
+/**
+ * @typedef {Object} SuggestedQuestion
+ * @property {string} label
+ * @property {string} question
+ * @property {string|null} [id]
+ */
+
+/**
+ * A single conversation: its messages plus the suggested questions currently
+ * associated with it. Bundling them keeps suggestions tied to the conversation,
+ * which makes supporting multiple switchable conversations straightforward later.
+ * @typedef {Object} Conversation
+ * @property {ChatMessage[]} messages
+ * @property {SuggestedQuestion[]|null} suggestedQuestions
+ */
+
export class ChatHistory {
static STORAGE_KEY = "ai-assistant-chat-history";
- /** @type {ChatMessage[]|null} */
+ /** @type {Conversation|null} */
_cache = null;
/**
- * Gets all messages from history
- * @returns {ChatMessage[]}
+ * Loads the conversation from cache or sessionStorage.
+ * @returns {Conversation}
+ * @private
*/
- getAll() {
- if (this._cache) return [...this._cache]; // Return copy to prevent mutations
+ _getConversation() {
+ if (this._cache) return this._cache;
try {
const stored = sessionStorage.getItem(ChatHistory.STORAGE_KEY);
if (!stored) {
- this._cache = [];
- return [];
+ this._cache = this._emptyConversation();
+ return this._cache;
}
const parsed = JSON.parse(stored);
- this._cache = this._sanitizeMessages(parsed);
- return [...this._cache];
+ this._cache = {
+ messages: this._sanitizeMessages(parsed?.messages ?? []),
+ suggestedQuestions: parsed?.suggestedQuestions ?? null,
+ };
+ return this._cache;
} catch (error) {
console.error("Error retrieving chat history:", error);
- this._cache = [];
- return [];
+ this._cache = this._emptyConversation();
+ return this._cache;
}
}
+ /**
+ * @returns {Conversation}
+ * @private
+ */
+ _emptyConversation() {
+ return { messages: [], suggestedQuestions: null };
+ }
+
+ /**
+ * Gets all messages from history
+ * @returns {ChatMessage[]}
+ */
+ getAll() {
+ return [...this._getConversation().messages]; // Copy to prevent mutations
+ }
+
/**
* Adds a new message to history
* @param {ChatMessage} message
*/
add(message) {
- const history = this.getAll();
- history.push(message);
- this._save(history);
+ const conversation = this._getConversation();
+ this._save({
+ ...conversation,
+ messages: [...conversation.messages, message],
+ });
}
/**
@@ -60,13 +98,14 @@ export class ChatHistory {
* @param {Partial} updates - Properties to merge into last message
*/
updateLast(updates) {
- const history = this.getAll();
- if (history.length > 0) {
- history[history.length - 1] = {
- ...history[history.length - 1],
+ const conversation = this._getConversation();
+ const messages = [...conversation.messages];
+ if (messages.length > 0) {
+ messages[messages.length - 1] = {
+ ...messages[messages.length - 1],
...updates,
};
- this._save(history);
+ this._save({ ...conversation, messages });
}
}
@@ -77,11 +116,12 @@ export class ChatHistory {
* @returns {boolean} True if the message was found and updated
*/
updateById(id, updates) {
- const history = this.getAll();
- const index = history.findIndex((m) => m.id === id);
+ const conversation = this._getConversation();
+ const messages = [...conversation.messages];
+ const index = messages.findIndex((m) => m.id === id);
if (index === -1) return false;
- history[index] = { ...history[index], ...updates };
- this._save(history);
+ messages[index] = { ...messages[index], ...updates };
+ this._save({ ...conversation, messages });
return true;
}
@@ -94,6 +134,23 @@ export class ChatHistory {
return this.getAll().find((m) => m.id === id);
}
+ /**
+ * Gets the suggested questions associated with the current conversation.
+ * @returns {SuggestedQuestion[]|null}
+ */
+ getSuggestedQuestions() {
+ return this._getConversation().suggestedQuestions ?? null;
+ }
+
+ /**
+ * Sets the suggested questions associated with the current conversation.
+ * @param {SuggestedQuestion[]} questions
+ */
+ setSuggestedQuestions(questions) {
+ const conversation = this._getConversation();
+ this._save({ ...conversation, suggestedQuestions: questions });
+ }
+
/**
* Gets messages formatted for AI context
* @param {Object} [options]
@@ -114,7 +171,7 @@ export class ChatHistory {
clear() {
try {
sessionStorage.removeItem(ChatHistory.STORAGE_KEY);
- this._cache = [];
+ this._cache = this._emptyConversation();
} catch (error) {
console.error("Error clearing chat history:", error);
}
@@ -137,13 +194,16 @@ export class ChatHistory {
}
/**
- * Saves history to sessionStorage
- * @param {ChatMessage[]} history - The chat history array to save
+ * Saves the conversation to sessionStorage
+ * @param {Conversation} conversation - The conversation to save
* @private
*/
- _save(history) {
+ _save(conversation) {
try {
- const serializable = this._sanitizeMessages(history);
+ const serializable = {
+ messages: this._sanitizeMessages(conversation.messages),
+ suggestedQuestions: conversation.suggestedQuestions ?? null,
+ };
sessionStorage.setItem(
ChatHistory.STORAGE_KEY,
JSON.stringify(serializable),
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 9ca2b7e8e..853daeef2 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_chat-ui.js
@@ -158,7 +158,16 @@ export const createChatButton = () => {
"daa-ll": "DevsiteAI Assistant:Open",
});
chatButton.innerHTML = ``;
+
+ const badge = createTag("span", {
+ class: "chat-button-badge",
+ "aria-hidden": "true",
+ });
+ badge.textContent = "BETA";
+ chatButton.appendChild(badge);
+
ELEMENTS.CHAT_BUTTON = chatButton;
+ ELEMENTS.CHAT_BUTTON_BADGE = badge;
return chatButton;
};
diff --git a/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js b/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
index fb6090b1d..b37d7350f 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_constants.js
@@ -10,6 +10,7 @@ export const CHAT_WINDOW_LABEL_ID = "ai-assistant-label";
*/
export const ELEMENTS = {
CHAT_BUTTON: null,
+ CHAT_BUTTON_BADGE: null,
CHAT_WINDOW_CLOSE_BUTTON: null,
CHAT_WINDOW_CLEAR_BUTTON: null,
CHAT_SEND_BUTTON: null,
@@ -21,14 +22,30 @@ export const ELEMENTS = {
/**
* @type {Array<{id?: string | null; label: string; question: string;}>}
*/
-export const FALLBACK_SUGGESTED_QUESTIONS = [
+export const INITIAL_SUGGESTED_QUESTIONS = [
{
- label: "Express Add-ons",
- question: "What can I do with Express Add-ons?",
+ label: "Adobe APIs and SDKs",
+ question: "Tell me about what Adobe APIs and SDKs are available",
},
{
- label: "App Builder application",
- question: "How do I build an App Builder application?",
+ label: "How do I get credentials?",
+ question: "How do I get credentials?",
+ },
+ {
+ label: "Adobe Developer App Builder",
+ question: "What is Adobe Developer App Builder?",
+ },
+ {
+ label: "Firefly Services",
+ question: "What are Firefly Services and how can I use them?",
+ },
+ {
+ label: "Adobe Express",
+ question: "How do I develop Adobe Express addons?",
+ },
+ {
+ label: "Adobe for Creativity",
+ question: "How can I use Adobe for Creativity in Claude?",
},
];
export const GENERIC_ERROR_MESSAGE =
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 b9603e947..3f8757693 100644
--- a/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js
+++ b/hlx_statics/blocks/ai-assistant/ai-assistant_suggested-questions.js
@@ -1,10 +1,10 @@
// @ts-check
import { createTag } from "../../scripts/lib-adobeio.js";
-import { aiApiClient } from "./ai-assistant_api-client.js";
import { handleUserQuery } from "./ai-assistant_chat-controller.js";
+import { chatHistory } from "./ai-assistant_chat-history.js";
import {
ELEMENTS,
- FALLBACK_SUGGESTED_QUESTIONS,
+ INITIAL_SUGGESTED_QUESTIONS,
} from "./ai-assistant_constants.js";
/**
@@ -37,6 +37,10 @@ export const parseAiSuggestedQuestions = (responseText) => {
* @param {Array<{label: string, question: string, id?: string|null}>|null} questions - Questions to show, or null for skeleton
*/
export const updateSuggestedQuestions = (questions) => {
+ if (questions !== null) {
+ chatHistory.setSuggestedQuestions(questions);
+ }
+
const wrapper = ELEMENTS.CHAT_SUGGESTED_QUESTIONS;
if (!wrapper) return;
const list = wrapper.querySelector(".chat-suggested-questions-list");
@@ -73,23 +77,6 @@ export const updateSuggestedQuestions = (questions) => {
});
};
-/**
- * Fetches collections and returns them as suggestion question objects.
- * Falls back to SUGGESTED_QUESTIONS if the API returns no results.
- * @returns {Promise>}
- */
-export const getCollectionsQuestions = async () => {
- const rawCollections = await aiApiClient.getCollections();
- const questions = rawCollections
- .filter((c) => c.id !== "__all-collections__" && !c.referencedCollectionIds)
- .map((c) => ({
- id: c.id,
- label: c.name,
- question: `What can I learn about ${c.name}?`,
- }));
- return questions.length > 0 ? questions : FALLBACK_SUGGESTED_QUESTIONS;
-};
-
/**
* Creates the suggested questions section with topic buttons.
* @returns {HTMLElement} The suggested questions wrapper element
@@ -106,7 +93,10 @@ export const createSuggestedQuestionsSection = () => {
wrapper.appendChild(list);
ELEMENTS.CHAT_SUGGESTED_QUESTIONS = wrapper;
- getCollectionsQuestions().then(updateSuggestedQuestions);
+ // Restore if we have an existing convo
+ updateSuggestedQuestions(
+ chatHistory.getSuggestedQuestions() ?? INITIAL_SUGGESTED_QUESTIONS,
+ );
return wrapper;
};
diff --git a/hlx_statics/blocks/codeblock/codeblock.css b/hlx_statics/blocks/codeblock/codeblock.css
index 9d9333f87..55612eaae 100644
--- a/hlx_statics/blocks/codeblock/codeblock.css
+++ b/hlx_statics/blocks/codeblock/codeblock.css
@@ -29,6 +29,7 @@ main div.codeblock-wrapper div.codeblock .inline-code {
main div.codeblock-wrapper div.codeblock .control-bar {
display: flex;
+ align-items: center;
justify-content: space-between;
}
@@ -37,6 +38,7 @@ main div.codeblock-wrapper div.codeblock .control-bar {
main div.codeblock-wrapper div.codeblock .control-bar,
main div.codeblock-wrapper div.codeblock .control-bar .tabs-list {
display: flex;
+ align-items: center;
gap: 16px;
max-width: 100%;
overflow-x: auto;
@@ -59,6 +61,9 @@ main div.codeblock-wrapper div.codeblock .control-bar select {
white-space: unset;
cursor: pointer;
background-color: initial;
+}
+
+main div.codeblock-wrapper div.codeblock .control-bar .tabs-list button {
border-bottom: 3px solid transparent;
}
@@ -84,18 +89,30 @@ main div.codeblock-wrapper div.codeblock .control-bar select::picker(select) {
main div.codeblock-wrapper div.codeblock .control-bar select {
border: none;
+ display: flex;
+ align-items: center;
+}
+
+main div.codeblock-wrapper div.codeblock .control-bar select > button {
+ all: unset;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ color: rgb(209, 209, 209);
}
main div.codeblock-wrapper div.codeblock .control-bar select selectedcontent {
- margin: auto;
+ display: flex;
+ align-items: center;
}
main div.codeblock-wrapper div.codeblock .control-bar select::picker-icon {
content: "⌄";
- margin: auto;
- padding-bottom: 6px;
+ display: flex;
+ align-items: center;
font-weight: bold;
- transform: scaleX(1.5);
+ transform: scaleX(1.5) translateY(-1px);
}
main div.codeblock-wrapper div.codeblock .control-bar select::picker(select) {
@@ -123,6 +140,39 @@ main div.codeblock-wrapper div.codeblock .hidden {
display: none;
}
+main div.codeblock-wrapper div.codeblock .right-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: auto;
+ margin-right: 4px;
+ flex-shrink: 0;
+}
+
+main div.codeblock-wrapper div.codeblock .collapse-toggle {
+ flex: 0 0 auto;
+ margin-left: 8px;
+ padding-top: 2px;
+ font-size: 14px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: rgb(209, 209, 209);
+ display: flex;
+ align-items: center;
+ border-radius: 4px;
+ transition: color 0.15s, background-color 0.15s;
+}
+
+main div.codeblock-wrapper div.codeblock .collapse-toggle:hover {
+ color: rgb(255, 255, 255);
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+main div.codeblock-wrapper div.codeblock.collapsed .tabs-panel {
+ display: none;
+}
+
main div.codeblock-wrapper div.codeblock pre[class*=language-].no-line-numbers .line-highlight {
transform: translateY(-1.6em);
}
diff --git a/hlx_statics/blocks/codeblock/codeblock.js b/hlx_statics/blocks/codeblock/codeblock.js
index 1dd1d5fae..02c2c48b6 100644
--- a/hlx_statics/blocks/codeblock/codeblock.js
+++ b/hlx_statics/blocks/codeblock/codeblock.js
@@ -84,11 +84,15 @@ export default function decorate(block) {
}
});
+ const rightControls = document.createElement('div');
+ rightControls.className = 'right-controls';
+ controlBar.append(rightControls);
+
const select = document.createElement('select');
select.id = selectId;
select.classList.toggle('hidden', !areTabsGrouped);
select.addEventListener('change', handleSelectChange);
- controlBar.append(select);
+ rightControls.append(select);
// set up customizable select (as opposed to classic which can't be styled) as described in https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select
const selectButton = document.createElement('button');
@@ -113,6 +117,20 @@ export default function decorate(block) {
decoratePreformattedCode(panel);
});
+ const collapseToggle = document.createElement('button');
+ collapseToggle.className = 'collapse-toggle';
+ collapseToggle.setAttribute('type', 'button');
+ collapseToggle.setAttribute('aria-label', 'Collapse code');
+ collapseToggle.setAttribute('aria-expanded', 'true');
+ collapseToggle.textContent = 'Hide';
+ collapseToggle.addEventListener('click', () => {
+ const isCollapsed = block.classList.toggle('collapsed');
+ collapseToggle.setAttribute('aria-expanded', String(!isCollapsed));
+ collapseToggle.setAttribute('aria-label', isCollapsed ? 'Expand code' : 'Collapse code');
+ collapseToggle.textContent = isCollapsed ? 'Show' : 'Hide';
+ });
+ rightControls.append(collapseToggle);
+
// initialize by simulating a click on the first tab
const firstTab = block.querySelector('[role=tab]');
if (firstTab) {
diff --git a/hlx_statics/blocks/discoverblock/discoverblock.css b/hlx_statics/blocks/discoverblock/discoverblock.css
index bb0f352f9..762ff0078 100644
--- a/hlx_statics/blocks/discoverblock/discoverblock.css
+++ b/hlx_statics/blocks/discoverblock/discoverblock.css
@@ -23,31 +23,40 @@ main div.discoverblock-wrapper div.discoverblock {
margin-right: 32px;
}
+main div.discoverblock-wrapper div.discoverblock.has-image {
+ display: flex;
+ align-items: flex-start;
+ gap: 26px;
+}
+
+main div.discoverblock-wrapper div.discoverblock.has-image > img.discover-image {
+ /* width/height come from inline styles set by discoverblock.js */
+ align-self: flex-start;
+}
+
+main div.discoverblock-wrapper div.discoverblock.has-image > div {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
/* Wrapper display */
main div.discoverblock-wrapper {
display: inline-flex;
flex-direction: column;
width: 320px;
- padding-top:0px;
+ min-width: 280px;
+ padding-top: 0px;
+ vertical-align: top;
}
-/* Responsive design for mobile and tablet */
@media (max-width: 768px) {
main div.discoverblock-wrapper {
width: 100% !important;
+ min-width: 0;
}
}
-main div.discoverblock-container div.heading2-wrapper{
- margin-bottom: 5px;
-}
-
-main div.discoverblock-container div.heading2-wrapper h2 {
- border-bottom: 0px;
- padding-bottom: 3px;
-}
-
-main div.discoverblock-container h2 {
+main h2.discoverblock-heading {
border-bottom: 1px solid #e1e1e1;
margin-top: 40px;
padding-top: 20px;
@@ -55,7 +64,7 @@ main div.discoverblock-container h2 {
margin-bottom: 0px;
}
-main div.discoverblock-container h3 {
+main h3.discoverblock-heading {
width: 100%;
}
@@ -84,11 +93,8 @@ main div.discoverblock-wrapper p {
margin-bottom: 0;
}
-main div.discoverblock-container .discover-heading-with-image {
+main .discoverblock-heading.discover-heading-with-image {
padding-left: 126px;
margin-bottom: 5px;
}
-main div.discoverblock-wrapper div.discover-content-with-image {
- padding-left: 126px;
-}
diff --git a/hlx_statics/blocks/discoverblock/discoverblock.js b/hlx_statics/blocks/discoverblock/discoverblock.js
index d4c2b6dbe..26dd1a425 100644
--- a/hlx_statics/blocks/discoverblock/discoverblock.js
+++ b/hlx_statics/blocks/discoverblock/discoverblock.js
@@ -1,60 +1,49 @@
-import { createTag, decorateAnchorLink } from "../../scripts/lib-adobeio.js";
+import { decorateAnchorLink } from "../../scripts/lib-adobeio.js";
/**
* decorates the discover block
* @param {Element} block The discover block element
*/
export default async function decorate(block) {
- block.setAttribute('daa-lh', 'discover');
+ block.setAttribute('daa-lh', 'discover');
block.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((h) => {
decorateAnchorLink(h);
});
- // Set width based on data-width attribute
+
const width = block.getAttribute('data-width');
const wrapper = block.closest('.discoverblock-wrapper');
- // If there's an image, set the block width to 2*1280/12 and position image on the left
const hasImage = block.querySelector('img') !== null;
if (hasImage) {
let imageWidth = (2 * 1280) / 12;
imageWidth = imageWidth + 156;
wrapper.style.width = imageWidth + 'px';
- // Get the image
const image = block.querySelector('img');
- // Set image width to 100px and move it to the beginning
image.style.width = '100px';
image.style.height = 'auto';
image.style.flexShrink = '0';
- image.style.position = 'absolute';
- // Move the image to be the first child (leftmost position)
- block.insertBefore(image, block.firstChild);
+ block.classList.add('has-image');
+ image.classList.add('discover-image');
- // Find button-container's parent and give it a class name
- const buttonContainer = block.querySelector('.button-container');
- if (buttonContainer && buttonContainer.parentElement) {
- buttonContainer.parentElement.classList.add('discover-content-with-image');
- }
+ block.insertBefore(image, block.firstChild);
} else if (width) {
- // if data-width exists, override the default width with the data-width.
wrapper.style.width = width;
}
const heading = block.querySelector('h1, h2, h3, h4, h5, h6');
if (heading) {
- // Check if block has image and add class to heading if it does
if (hasImage) {
heading.classList.add('discover-heading-with-image');
}
const headingClone = heading.cloneNode(true);
- // Insert heading before the wrapper
+ headingClone.classList.add('discoverblock-heading');
wrapper.parentElement.insertBefore(headingClone, wrapper);
- // Remove the original heading from the block content
heading.remove();
}
}
diff --git a/hlx_statics/blocks/resources/resources.css b/hlx_statics/blocks/resources/resources.css
index aa6915e4a..2a8696882 100644
--- a/hlx_statics/blocks/resources/resources.css
+++ b/hlx_statics/blocks/resources/resources.css
@@ -1,5 +1,6 @@
-main div.resources-wrapper a{
+main div.resources-wrapper li > a{
font-size: 16px;
+ flex: 1;
}
main div.resources-wrapper ul{
@@ -9,6 +10,7 @@ main div.resources-wrapper ul{
main div.resources-wrapper li{
margin-top:15px;
display:flex;
+ align-items: center;
}
main div.resources-wrapper h3{
@@ -27,6 +29,5 @@ main div.resources-wrapper div.external-icon {
width: 16px;
height: 16px;
margin-left: 8px;
- margin-top:3px;
vertical-align: middle;
}
\ No newline at end of file
diff --git a/hlx_statics/blocks/resources/resources.js b/hlx_statics/blocks/resources/resources.js
index 94bd78662..a3bc5760c 100644
--- a/hlx_statics/blocks/resources/resources.js
+++ b/hlx_statics/blocks/resources/resources.js
@@ -13,7 +13,7 @@ export default async function decorate(block) {
const href = link.getAttribute('href');
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
const externalLink = createTag('div', {class: 'external-icon'});
- externalLink.innerHTML = `