collage fixes
This commit is contained in:
14
README.md
14
README.md
@@ -94,8 +94,9 @@ The template also accepts a raw URL string or standard CasparCG XML
|
||||
|
||||
## Collage
|
||||
|
||||
Use `/collage` to render multiple social posts as a vertically scrolling strip.
|
||||
Posts are repeated in the track so the motion keeps filling gaps.
|
||||
Use `/collage` to render multiple social posts as independently scrolling
|
||||
columns. The browser keeps only a small window of live posts per column, removes
|
||||
cards after they scroll off the top, and fills new random cards at the bottom.
|
||||
|
||||
Repeated `url` parameters:
|
||||
|
||||
@@ -114,12 +115,15 @@ Useful collage parameters:
|
||||
- `spacing`: pixels between a post and anything else, including screen edges and other posts, default `48`.
|
||||
- `fade`: optional pixels used to fade posts in/out at the top and bottom edges, default `0`.
|
||||
- `columns`: number of post columns, default `3`.
|
||||
- `repeatDistance`: minimum pixel distance before the same post can be reused near another live copy, default `900`.
|
||||
- `hydrateDelay`: milliseconds between hydrating newly inserted provider embeds, default `180`.
|
||||
- `duration`: seconds per scroll loop, default `360`.
|
||||
- `repeat`: number of times to repeat the post list in each half of the loop, default `4`.
|
||||
- `repeat`: virtual scroll buffer multiplier, default `2`.
|
||||
- `shuffle`: `1` to randomize post order on each page load, default `1`.
|
||||
|
||||
Collage card width is calculated from `width`, `spacing`, and `columns`,
|
||||
then capped at `500px`, so all columns fit within the configured screen width.
|
||||
Collage card width is calculated from `width`, `spacing`, and `columns` so the
|
||||
columns fill the configured screen width with only the chosen spacing at the
|
||||
edges.
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "node --watch src/server.js",
|
||||
"start": "node src/server.js",
|
||||
"typecheck": "node --check src/server.js && node --check src/oembed.js && node --check src/providers.js && node --check src/templates.js && node --check public/caspar.js",
|
||||
"typecheck": "node --check src/server.js && node --check src/oembed.js && node --check src/providers.js && node --check src/templates.js && node --check public/caspar.js && node --check public/collage.js",
|
||||
"build": "npm run typecheck",
|
||||
"test": "node --test"
|
||||
},
|
||||
|
||||
279
public/collage.js
Normal file
279
public/collage.js
Normal file
@@ -0,0 +1,279 @@
|
||||
const dataElement = document.getElementById("collage-data");
|
||||
const stage = document.querySelector(".collage-stage");
|
||||
const track = document.querySelector(".collage-track");
|
||||
const config = JSON.parse(dataElement.textContent);
|
||||
|
||||
let lastFrame = performance.now();
|
||||
let itemCursor = 0;
|
||||
const columns = [];
|
||||
const deviceScale = window.devicePixelRatio || 1;
|
||||
const hydrationQueue = [];
|
||||
const hydrateDelay = Math.max(Number(config.hydrateDelay) || 0, 0);
|
||||
const placeholderHeight = 160;
|
||||
const cardColumns = new WeakMap();
|
||||
let trackTop = 0;
|
||||
let hydrationTimer;
|
||||
const cardObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const card = entry.target;
|
||||
const column = cardColumns.get(card);
|
||||
const previousExtent = cardExtent(card);
|
||||
|
||||
card.dataset.height = String(Math.max(entry.contentRect.height, placeholderHeight));
|
||||
|
||||
if (column) {
|
||||
column.contentHeight += cardExtent(card) - previousExtent;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function snapPixel(value) {
|
||||
return Math.round(value * deviceScale) / deviceScale;
|
||||
}
|
||||
|
||||
function slugify(value = "") {
|
||||
return String(value)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "") || "unknown";
|
||||
}
|
||||
|
||||
function executeScripts(root) {
|
||||
for (const script of root.querySelectorAll("script")) {
|
||||
const replacement = document.createElement("script");
|
||||
|
||||
for (const attribute of script.attributes) {
|
||||
replacement.setAttribute(attribute.name, attribute.value);
|
||||
}
|
||||
|
||||
replacement.textContent = script.textContent;
|
||||
script.replaceWith(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleHydration(card, item) {
|
||||
if (!hydrateDelay) {
|
||||
hydrateCard(card, item);
|
||||
return;
|
||||
}
|
||||
|
||||
hydrationQueue.push({ card, item });
|
||||
|
||||
if (!hydrationTimer) {
|
||||
hydrationTimer = setInterval(hydrateNextCard, hydrateDelay);
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateNextCard() {
|
||||
let next = hydrationQueue.shift();
|
||||
|
||||
while (next && !next.card.isConnected) {
|
||||
next = hydrationQueue.shift();
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
clearInterval(hydrationTimer);
|
||||
hydrationTimer = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
hydrateCard(next.card, next.item);
|
||||
}
|
||||
|
||||
function nudgeProviderWidgets(root) {
|
||||
window.twttr?.widgets?.load?.(root);
|
||||
window.instgrm?.Embeds?.process?.();
|
||||
window.bluesky?.scan?.(root);
|
||||
}
|
||||
|
||||
function hydrateCard(card, item) {
|
||||
if (item.type === "photo" && item.url) {
|
||||
const image = document.createElement("img");
|
||||
image.src = item.url;
|
||||
image.alt = item.title || "";
|
||||
card.replaceChildren(image);
|
||||
} else {
|
||||
card.innerHTML = item.html || "";
|
||||
}
|
||||
|
||||
executeScripts(card);
|
||||
nudgeProviderWidgets(card);
|
||||
card.classList.add("is-hydrated");
|
||||
}
|
||||
|
||||
function createCard(column, item) {
|
||||
const card = document.createElement("section");
|
||||
card.className = `embed collage-card provider-${slugify(item.providerName)}`;
|
||||
card.dataset.source = item.targetUrl;
|
||||
card.dataset.height = String(placeholderHeight);
|
||||
card.style.setProperty("--embed-width", `${config.cardWidth}px`);
|
||||
cardColumns.set(card, column);
|
||||
column.contentHeight += cardExtent(card);
|
||||
cardObserver.observe(card);
|
||||
scheduleHydration(card, item);
|
||||
return card;
|
||||
}
|
||||
|
||||
function createColumn(index) {
|
||||
const element = document.createElement("div");
|
||||
element.className = "collage-column";
|
||||
element.dataset.column = String(index);
|
||||
track.append(element);
|
||||
|
||||
return {
|
||||
element,
|
||||
index,
|
||||
offset: 0,
|
||||
contentHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function cardExtent(card) {
|
||||
return cardHeight(card) + config.spacing;
|
||||
}
|
||||
|
||||
function cardHeight(card) {
|
||||
return Math.max(Number(card.dataset.height) || 0, placeholderHeight);
|
||||
}
|
||||
|
||||
function candidateY(column) {
|
||||
return column.contentHeight - column.offset;
|
||||
}
|
||||
|
||||
function isTooCloseToSamePost(item, column) {
|
||||
if (!config.repeatDistance || config.items.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const minDistanceSquared = config.repeatDistance * config.repeatDistance;
|
||||
const nextY = candidateY(column);
|
||||
|
||||
for (const otherColumn of columns) {
|
||||
const columnDistance = Math.abs(column.index - otherColumn.index) * (config.cardWidth + config.spacing);
|
||||
|
||||
for (const card of otherColumn.element.children) {
|
||||
if (card.dataset.source !== item.targetUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingY = card.offsetTop - otherColumn.offset;
|
||||
const yDistance = nextY - existingY;
|
||||
const distanceSquared = (columnDistance * columnDistance) + (yDistance * yDistance);
|
||||
|
||||
if (distanceSquared < minDistanceSquared) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function randomCandidate() {
|
||||
return config.items[Math.floor(Math.random() * config.items.length)];
|
||||
}
|
||||
|
||||
function sequentialCandidate(attempt) {
|
||||
return config.items[(itemCursor + attempt) % config.items.length];
|
||||
}
|
||||
|
||||
function nextItem(column) {
|
||||
const maxAttempts = Math.max(config.items.length * 2, 8);
|
||||
let fallback = config.items[0];
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
const item = config.shuffle ? randomCandidate() : sequentialCandidate(attempt);
|
||||
fallback = item;
|
||||
|
||||
if (!isTooCloseToSamePost(item, column)) {
|
||||
if (!config.shuffle) {
|
||||
itemCursor += attempt + 1;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.shuffle) {
|
||||
itemCursor += 1;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function fillColumn(column) {
|
||||
const targetHeight = stage.clientHeight + Math.max(config.cardWidth, 720) + config.spacing;
|
||||
let guard = 0;
|
||||
|
||||
while (column.contentHeight - column.offset < targetHeight && guard < 24) {
|
||||
column.element.append(createCard(column, nextItem(column)));
|
||||
guard += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function fillColumns() {
|
||||
for (const column of columns) {
|
||||
fillColumn(column);
|
||||
}
|
||||
}
|
||||
|
||||
function recycleColumn(column) {
|
||||
let firstCard = column.element.firstElementChild;
|
||||
|
||||
while (firstCard && isFullyAboveStage(firstCard, column)) {
|
||||
column.contentHeight -= cardExtent(firstCard);
|
||||
column.offset -= cardExtent(firstCard);
|
||||
cardObserver.unobserve(firstCard);
|
||||
cardColumns.delete(firstCard);
|
||||
firstCard.remove();
|
||||
column.element.append(createCard(column, nextItem(column)));
|
||||
firstCard = column.element.firstElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
function isFullyAboveStage(card, column) {
|
||||
const cardBottom = trackTop + cardHeight(card) - column.offset;
|
||||
|
||||
return cardBottom <= 0;
|
||||
}
|
||||
|
||||
function scrollSpeed() {
|
||||
const baseDistance = Math.max(stage.clientHeight, config.cardWidth * config.repeat);
|
||||
return baseDistance / config.duration;
|
||||
}
|
||||
|
||||
function frame(now) {
|
||||
const elapsedSeconds = Math.min((now - lastFrame) / 1000, 0.1);
|
||||
lastFrame = now;
|
||||
|
||||
for (const column of columns) {
|
||||
column.offset += scrollSpeed() * elapsedSeconds;
|
||||
recycleColumn(column);
|
||||
fillColumn(column);
|
||||
column.element.style.transform = `translate3d(0, ${snapPixel(-column.offset)}px, 0)`;
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
for (let index = 0; index < config.columns; index += 1) {
|
||||
columns.push(createColumn(index));
|
||||
}
|
||||
|
||||
fillColumns();
|
||||
trackTop = track.offsetTop;
|
||||
|
||||
for (const [index, column] of columns.entries()) {
|
||||
const firstCard = column.element.firstElementChild;
|
||||
const stagger = firstCard ? Math.min(index * config.spacing * 0.75, cardExtent(firstCard) * 0.4) : 0;
|
||||
column.offset = stagger;
|
||||
column.element.style.transform = `translate3d(0, ${snapPixel(-column.offset)}px, 0)`;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
trackTop = track.offsetTop;
|
||||
fillColumns();
|
||||
});
|
||||
observer.observe(stage);
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
@@ -215,6 +215,8 @@ code {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
padding: var(--collage-spacing);
|
||||
contain: layout paint;
|
||||
pointer-events: none;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
@@ -232,19 +234,23 @@ code {
|
||||
}
|
||||
|
||||
.collage-track {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
align-items: stretch;
|
||||
gap: var(--collage-spacing);
|
||||
animation: collage-scroll var(--collage-duration) linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.collage-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--collage-columns), minmax(0, var(--collage-card-width)));
|
||||
gap: var(--collage-spacing);
|
||||
width: max-content;
|
||||
backface-visibility: hidden;
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
.collage-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--collage-spacing);
|
||||
width: var(--collage-card-width);
|
||||
backface-visibility: hidden;
|
||||
contain: layout style;
|
||||
transform: translate3d(0, 0, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.collage-card {
|
||||
@@ -252,10 +258,16 @@ code {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: var(--collage-card-width);
|
||||
overflow: hidden;
|
||||
max-height: none;
|
||||
contain: layout style;
|
||||
overflow: visible;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.collage-card:not(.is-hydrated) {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.collage-card > iframe,
|
||||
.collage-card > blockquote,
|
||||
.collage-card > img,
|
||||
@@ -274,16 +286,11 @@ code {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@keyframes collage-scroll {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(calc(-50% - (var(--collage-spacing) / 2)));
|
||||
}
|
||||
.collage-card > * {
|
||||
margin-block: 0 !important;
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -84,6 +84,7 @@ async function serveStatic(requestUrl, response) {
|
||||
const staticPaths = new Map([
|
||||
["/styles.css", "styles.css"],
|
||||
["/caspar.js", "caspar.js"],
|
||||
["/collage.js", "collage.js"],
|
||||
]);
|
||||
const path = staticPaths.get(requestUrl.pathname) || "";
|
||||
|
||||
@@ -153,8 +154,10 @@ async function handleCollage(requestUrl, response) {
|
||||
const spacing = getNumber(requestUrl.searchParams, "spacing", Number(legacySpacing || 48), 0, 400);
|
||||
const fade = getNumber(requestUrl.searchParams, "fade", 0, 0, 600);
|
||||
const columns = getNumber(requestUrl.searchParams, "columns", 3, 1, 8);
|
||||
const repeatDistance = getNumber(requestUrl.searchParams, "repeatDistance", 900, 0, 4000);
|
||||
const hydrateDelay = getNumber(requestUrl.searchParams, "hydrateDelay", 180, 0, 2000);
|
||||
const duration = getNumber(requestUrl.searchParams, "duration", 360, 10, 1200);
|
||||
const repeat = getNumber(requestUrl.searchParams, "repeat", 4, 2, 12);
|
||||
const repeat = getNumber(requestUrl.searchParams, "repeat", 2, 1, 8);
|
||||
const shuffle = getBoolean(requestUrl.searchParams, "shuffle", true);
|
||||
const transparent = getBoolean(requestUrl.searchParams, "transparent", true);
|
||||
const autoplay = getBoolean(requestUrl.searchParams, "autoplay", true);
|
||||
@@ -189,6 +192,8 @@ async function handleCollage(requestUrl, response) {
|
||||
spacing,
|
||||
fade,
|
||||
columns,
|
||||
repeatDistance,
|
||||
hydrateDelay,
|
||||
duration,
|
||||
repeat,
|
||||
shuffle,
|
||||
|
||||
@@ -45,7 +45,7 @@ function collageCardWidth({ width, spacing, columns }) {
|
||||
const availableWidth = width - (spacing * 2) - (spacing * Math.max(columns - 1, 0));
|
||||
const columnWidth = Math.floor(availableWidth / columns);
|
||||
|
||||
return Math.max(Math.min(columnWidth, 500), 120);
|
||||
return Math.max(columnWidth, 120);
|
||||
}
|
||||
|
||||
function shuffleItems(items) {
|
||||
@@ -59,6 +59,10 @@ function shuffleItems(items) {
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
function safeJson(value) {
|
||||
return JSON.stringify(value).replaceAll("</", "<\\/");
|
||||
}
|
||||
|
||||
function addIframePermissions(tag) {
|
||||
const autoplayPermission = "autoplay";
|
||||
|
||||
@@ -350,8 +354,10 @@ export function homePage({ providersCount = 0 } = {}) {
|
||||
<label>Spacing <input name="spacing" type="number" value="48" min="0" max="400"></label>
|
||||
<label>Fade <input name="fade" type="number" value="0" min="0" max="600"></label>
|
||||
<label>Columns <input name="columns" type="number" value="3" min="1" max="8"></label>
|
||||
<label>Separation <input name="repeatDistance" type="number" value="900" min="0" max="4000"></label>
|
||||
<label>Hydrate <input name="hydrateDelay" type="number" value="180" min="0" max="2000"></label>
|
||||
<label>Duration <input name="duration" type="number" value="360" min="10" max="1200"></label>
|
||||
<label>Repeat <input name="repeat" type="number" value="4" min="2" max="12"></label>
|
||||
<label>Repeat <input name="repeat" type="number" value="2" min="1" max="8"></label>
|
||||
<label class="checkbox-label"><input name="shuffle" value="1" type="checkbox" checked> shuffle</label>
|
||||
<label class="checkbox-label"><input name="transparent" value="1" type="checkbox" checked> transparent</label>
|
||||
</div>
|
||||
@@ -435,35 +441,43 @@ export function collagePage({
|
||||
spacing = 48,
|
||||
fade = 0,
|
||||
columns = 3,
|
||||
repeatDistance = 900,
|
||||
hydrateDelay = 180,
|
||||
duration = 360,
|
||||
repeat = 4,
|
||||
repeat = 2,
|
||||
shuffle = true,
|
||||
}) {
|
||||
const cardWidth = collageCardWidth({ width, spacing, columns });
|
||||
const orderedItems = shuffle ? shuffleItems(items) : items;
|
||||
const groupItems = Array.from({ length: repeat }, () => orderedItems).flat();
|
||||
const groupCards = groupItems
|
||||
.map((item) => embedCardHtml({
|
||||
const orderedItems = shuffle ? shuffleItems(items) : [...items];
|
||||
const collageItems = orderedItems.map((item) => ({
|
||||
targetUrl: item.targetUrl,
|
||||
embed: item.embed,
|
||||
providerName: item.embed.provider_name || "",
|
||||
type: item.embed.type || "rich",
|
||||
title: item.embed.title || item.embed.provider_name || "",
|
||||
url: item.embed.url || "",
|
||||
html: prepareEmbedHtml(item.embed.html || "", { autoplay, muted }),
|
||||
}));
|
||||
const collageData = {
|
||||
items: collageItems,
|
||||
cardWidth,
|
||||
columns,
|
||||
repeatDistance,
|
||||
hydrateDelay,
|
||||
spacing,
|
||||
duration,
|
||||
repeat,
|
||||
shuffle,
|
||||
autoplay,
|
||||
muted,
|
||||
className: "embed collage-card",
|
||||
widthCap: cardWidth,
|
||||
includeHeight: false,
|
||||
}))
|
||||
.join("\n");
|
||||
const groups = [groupCards, groupCards]
|
||||
.map((cards) => `<div class="collage-group">${cards}</div>`)
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
return `${commonHead({ title: "oEmbed Collage", htmlClass: transparent ? "graphic-document transparent" : "graphic-document" })}
|
||||
<body class="graphic collage-page ${transparent ? "transparent" : ""} is-ready" style="--stage-width:${width}px; --stage-height:${height}px; --chroma:${escapeHtml(chroma)}; --collage-spacing:${spacing}px; --collage-fade:${fade}px; --collage-columns:${columns}; --collage-card-width:${cardWidth}px; --collage-duration:${duration}s;">
|
||||
<main class="collage-stage">
|
||||
<div class="collage-track">
|
||||
${groups}
|
||||
</div>
|
||||
<div class="collage-track"></div>
|
||||
</main>
|
||||
<script id="collage-data" type="application/json">${safeJson(collageData)}</script>
|
||||
<script src="/collage.js"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ test("home page includes single graphic and collage forms", () => {
|
||||
assert.match(page, /name="spacing" type="number" value="48"/);
|
||||
assert.match(page, /name="fade" type="number" value="0"/);
|
||||
assert.match(page, /name="columns" type="number" value="3"/);
|
||||
assert.match(page, /name="repeatDistance" type="number" value="900"/);
|
||||
assert.match(page, /name="hydrateDelay" type="number" value="180"/);
|
||||
assert.match(page, /name="repeat" type="number" value="2"/);
|
||||
assert.match(page, /name="shuffle" value="1" type="checkbox" checked/);
|
||||
assert.match(page, /Open collage/);
|
||||
});
|
||||
@@ -238,6 +241,7 @@ test("renders a vertical scrolling collage page with repeated embed cards", () =
|
||||
spacing: 48,
|
||||
fade: 0,
|
||||
columns: 3,
|
||||
repeatDistance: 900,
|
||||
duration: 360,
|
||||
repeat: 3,
|
||||
shuffle: false,
|
||||
@@ -247,12 +251,17 @@ test("renders a vertical scrolling collage page with repeated embed cards", () =
|
||||
assert.match(page, /--collage-spacing:48px/);
|
||||
assert.match(page, /--collage-fade:0px/);
|
||||
assert.match(page, /--collage-columns:3/);
|
||||
assert.match(page, /--collage-card-width:500px/);
|
||||
assert.match(page, /--collage-card-width:576px/);
|
||||
assert.match(page, /--collage-duration:360s/);
|
||||
assert.equal((page.match(/class="embed collage-card provider-example"/g) || []).length, 12);
|
||||
assert.equal((page.match(/class="collage-group"/g) || []).length, 2);
|
||||
assert.match(page, /--embed-width:500px/);
|
||||
assert.doesNotMatch(page, /collage-card provider-example"[^>]*--embed-height/);
|
||||
assert.match(page, /<div class="collage-track"><\/div>/);
|
||||
assert.match(page, /<script id="collage-data" type="application\/json">/);
|
||||
assert.match(page, /"cardWidth":576/);
|
||||
assert.match(page, /"columns":3/);
|
||||
assert.match(page, /"repeatDistance":900/);
|
||||
assert.match(page, /"hydrateDelay":180/);
|
||||
assert.match(page, /"repeat":3/);
|
||||
assert.match(page, /"shuffle":false/);
|
||||
assert.match(page, /<script src="\/collage\.js"><\/script>/);
|
||||
});
|
||||
|
||||
test("shrinks collage card width to fit the configured screen width", () => {
|
||||
@@ -281,6 +290,5 @@ test("shrinks collage card width to fit the configured screen width", () => {
|
||||
});
|
||||
|
||||
assert.match(page, /--collage-card-width:260px/);
|
||||
assert.match(page, /--embed-width:260px/);
|
||||
assert.doesNotMatch(page, /collage-card provider-example"[^>]*--embed-height/);
|
||||
assert.match(page, /"cardWidth":260/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user