Use style, hoverStyle and activeStyle to craft distinct visuals.
Style any article via timeline-level defaults or per-article options. Hover and active styles inherit from the normal style, letting you override only what is needed. See the Article Style cascade for how layout, timeline, lane, and per-article values combine.
To clear system defaults for active and/or hover styles after initialization, delete individual properties from the style object or reset all by setting to an empty object, for example:
myTimeline.options.article.defaultActiveStyle = {}myTimeline.setOption("article.defaultActiveStyle", myActiveStyle)/* Select an article in the timeline to see its style here */
/* Select an article in the timeline to see its hoverStyle here */
/* Select an article in the timeline to see its activeStyle here */
Toggle between the built-in landscape and portrait layouts and a registered compact layout. The custom renderer keeps its measured size, clickable rectangle, and star interaction aligned through getWidth(), getHeight(), and getIconBox().
The same Article Style cascade applies to every layout. See the Custom Card Layouts guide for registration timing, drawing, responsive selection, aliases, caching, TypeScript, and troubleshooting.
function drawCustomLayoutStar(ctx, box, isStarred) {
const centreX = box.left + box.width / 2;
const centreY = box.top + box.height / 2;
ctx.save();
ctx.fillStyle = isStarred ? "#f59e0b" : "#2563eb";
ctx.font = "18px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(isStarred ? "★" : "☆", centreX, centreY);
ctx.restore();
}
Histropedia.Timeline.registerCardLayout({
name: "compact",
getWidth() {
const style = this.getCurrentStyle();
const key = `compact-width:${this.title}:${style.header.text.font}:${style.width}`;
return this.memo(key, () => {
const measureContext = this.owner.canvasContext;
measureContext.save();
measureContext.font = style.header.text.font;
const measuredWidth = measureContext.measureText(this.title).width + 54;
measureContext.restore();
return Math.min(280, Math.max(style.width, measuredWidth));
});
},
getHeight() {
const style = this.getCurrentStyle();
return this.subtitle ? Math.max(style.height, 60) : style.height;
},
getIconBox() {
if (this.owner.options.article.star.visible === false) return false;
const width = 18;
const height = 18;
const left = this.position.left + this.getWidth() - width - 8;
const top = this.position.top + 8;
return { left, top, width, height };
},
draw(ctx) {
const style = this.getCurrentStyle();
const left = this.position.left;
const top = this.position.top;
const width = this.getWidth();
const height = this.getHeight();
ctx.save();
ctx.fillStyle = style.backgroundColor;
ctx.fillRect(left, top, width, height);
ctx.strokeStyle = style.border.color;
const borderWidth = style.border.width;
ctx.lineWidth = borderWidth;
ctx.strokeRect(
left + borderWidth / 2,
top + borderWidth / 2,
width - borderWidth,
height - borderWidth
);
ctx.fillStyle = style.header.text.color;
ctx.font = style.header.text.font;
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText(this.title, left + 10, top + 9, width - 46);
if (this.subtitle) {
ctx.font = "12px sans-serif";
ctx.fillText(this.subtitle, left + 10, top + 34, width - 20);
}
ctx.restore();
const iconBox = this.getIconBox();
if (iconBox && (this.isActive || this.isStarred)) {
drawCustomLayoutStar(ctx, iconBox, this.isStarred);
}
},
defaultStyle: {
width: 190,
height: 48,
backgroundColor: "#eff6ff",
border: { color: "#2563eb", width: 1 },
header: { text: { color: "#1e3a8a", font: "600 14px sans-serif" } }
},
defaultHoverStyle: { backgroundColor: "#dbeafe" },
defaultActiveStyle: { backgroundColor: "#bfdbfe", border: { width: 2 } }
});