Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
AkurAI-Framework / site/frontend/ai/components/content/item_carousel.js
Latest change 67a966d4abf972b300e8c6c214a30a0279103337 - feat(ai): Artifacts content components + geo map + item carousel (AB3-C) by Ólafur Búi Ólafsson
/**
* Item Carousel — Artifacts / content component.
* Payload: { items: Array<{ title: string, image?: string, subtitle?: string, href?: string }> }
* Horizontal scroll-snap carousel with prev/next buttons.
*/
function esc(str) {
return String(str ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function safeHref(href) {
if (!href) return null;
const str = String(href);
if (/^(https?:\/\/|\/)/i.test(str)) return str;
return null;
}
function renderItem(item) {
const { title, image, subtitle, href } = item;
const safe = safeHref(href);
const tag = safe ? "a" : "div";
const hrefAttr = safe ? ` href="${esc(safe)}"` : "";
const target = safe ? ` target="_blank" rel="noopener noreferrer"` : "";
const imgHtml = image
? `<img class="tc-content-carousel__img" src="${esc(image)}" alt="${esc(title)}" loading="lazy">`
: `<div class="tc-content-carousel__img tc-content-carousel__img--placeholder" aria-hidden="true">
<svg viewBox="0 0 80 56" fill="none" width="80" height="56">
<rect width="80" height="56" rx="6" fill="var(--panel-2)"/>
<path d="M26 42l10-13 7 9 5-6 10 10H18z" fill="var(--border-2)"/>
<circle cx="24" cy="22" r="5" fill="var(--border-2)"/>
</svg>
</div>`;
return `<${tag} class="tc-content-carousel__item"${hrefAttr}${target}>
${imgHtml}
<div class="tc-content-carousel__info">
<span class="tc-content-carousel__title">${esc(title)}</span>
${subtitle ? `<span class="tc-content-carousel__sub">${esc(subtitle)}</span>` : ""}
</div>
</${tag}>`;
}
export const def = {
name: "item_carousel",
category: "Artifacts",
title: "Item Carousel",
match(payload) {
return (
payload !== null &&
typeof payload === "object" &&
Array.isArray(payload.items) &&
payload.items.length > 0
);
},
render(payload) {
const { items } = payload;
const wrap = document.createElement("div");
wrap.className = "tc-content-carousel";
wrap.setAttribute("role", "region");
wrap.setAttribute("aria-label", "Item carousel");
wrap.innerHTML = `
<button class="tc-content-carousel__nav tc-content-carousel__nav--prev" aria-label="Previous" type="button" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="18" height="18" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<div class="tc-content-carousel__track" role="list">
${items.map((item) => `<div role="listitem">${renderItem(item)}</div>`).join("")}
</div>
<button class="tc-content-carousel__nav tc-content-carousel__nav--next" aria-label="Next" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="18" height="18" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
</button>
`;
const track = wrap.querySelector(".tc-content-carousel__track");
const prevBtn = wrap.querySelector(".tc-content-carousel__nav--prev");
const nextBtn = wrap.querySelector(".tc-content-carousel__nav--next");
function updateButtons() {
prevBtn.disabled = track.scrollLeft <= 4;
nextBtn.disabled = track.scrollLeft + track.clientWidth >= track.scrollWidth - 4;
}
function scrollBy(dir) {
const itemEl = track.querySelector(".tc-content-carousel__item");
const step = itemEl ? itemEl.offsetWidth + 12 : 200;
track.scrollBy({ left: dir * step, behavior: "smooth" });
}
prevBtn.addEventListener("click", () => scrollBy(-1));
nextBtn.addEventListener("click", () => scrollBy(1));
track.addEventListener("scroll", updateButtons, { passive: true });
// Initial state after paint
requestAnimationFrame(updateButtons);
return wrap;
},
example: {
items: [
{ title: "AkurAI Framework", subtitle: "Native ESM, zero deps", href: "https://github.com/olibuijr/AkurAI-Framework" },
{ title: "Streaming Chat", subtitle: "SSE-based token streaming" },
{ title: "Tool UI", subtitle: "30+ generative components" },
{ title: "Geo Map", subtitle: "No map library" },
{ title: "Item Carousel", subtitle: "This component" },
],
},
};