import { attachFooterText } from "./common.js" ;
import type {
Action,
CardAction,
FlexBox,
FlexBubble,
FlexButton,
FlexCarousel,
FlexComponent,
FlexImage,
FlexText,
ListItem,
} from "./types.js" ;
/**
* Create an info card with title, body, and optional footer
*
* Editorial design: Clean hierarchy with accent bar, generous spacing,
* and subtle background zones for visual separation.
*/
export function createInfoCard(title: string, body: string, footer?: string): FlexBubble {
const bubble: FlexBubble = {
type: "bubble" ,
size: "mega" ,
body: {
type: "box" ,
layout: "vertical" ,
contents: [
// Title with accent bar
{
type: "box" ,
layout: "horizontal" ,
contents: [
{
type: "box" ,
layout: "vertical" ,
contents: [],
width: "4px" ,
backgroundColor: "#06C755" ,
cornerRadius: "2px" ,
} as FlexBox,
{
type: "text" ,
text: title,
weight: "bold" ,
size: "xl" ,
color: "#111111" ,
wrap: true ,
flex: 1 ,
margin: "lg" ,
} as FlexText,
],
} as FlexBox,
// Body text in subtle container
{
type: "box" ,
layout: "vertical" ,
contents: [
{
type: "text" ,
text: body,
size: "md" ,
color: "#444444" ,
wrap: true ,
lineSpacing: "6px" ,
} as FlexText,
],
margin: "xl" ,
paddingAll: "lg" ,
backgroundColor: "#F8F9FA" ,
cornerRadius: "lg" ,
} as FlexBox,
],
paddingAll: "xl" ,
backgroundColor: "#FFFFFF" ,
},
};
if (footer) {
attachFooterText(bubble, footer);
}
return bubble;
}
/**
* Create a list card with title and multiple items
*
* Editorial design: Numbered/bulleted list with clear visual hierarchy,
* accent dots for each item, and generous spacing.
*/
export function createListCard(title: string, items: ListItem[]): FlexBubble {
const itemContents: FlexComponent[] = items.slice(0 , 8 ).map((item, index) => {
const itemContents: FlexComponent[] = [
{
type: "text" ,
text: item.title,
size: "md" ,
weight: "bold" ,
color: "#1a1a1a" ,
wrap: true ,
} as FlexText,
];
if (item.subtitle) {
itemContents.push({
type: "text" ,
text: item.subtitle,
size: "sm" ,
color: "#888888" ,
wrap: true ,
margin: "xs" ,
} as FlexText);
}
const itemBox: FlexBox = {
type: "box" ,
layout: "horizontal" ,
contents: [
// Accent dot
{
type: "box" ,
layout: "vertical" ,
contents: [
{
type: "box" ,
layout: "vertical" ,
contents: [],
width: "8px" ,
height: "8px" ,
backgroundColor: index === 0 ? "#06C755" : "#DDDDDD" ,
cornerRadius: "4px" ,
} as FlexBox,
],
width: "20px" ,
alignItems: "center" ,
paddingTop: "sm" ,
} as FlexBox,
// Item content
{
type: "box" ,
layout: "vertical" ,
contents: itemContents,
flex: 1 ,
} as FlexBox,
],
margin: index > 0 ? "lg" : undefined,
};
if (item.action) {
itemBox.action = item.action;
}
return itemBox;
});
return {
type: "bubble" ,
size: "mega" ,
body: {
type: "box" ,
layout: "vertical" ,
contents: [
{
type: "text" ,
text: title,
weight: "bold" ,
size: "xl" ,
color: "#111111" ,
wrap: true ,
} as FlexText,
{
type: "separator" ,
margin: "lg" ,
color: "#EEEEEE" ,
},
{
type: "box" ,
layout: "vertical" ,
contents: itemContents,
margin: "lg" ,
} as FlexBox,
],
paddingAll: "xl" ,
backgroundColor: "#FFFFFF" ,
},
};
}
/**
* Create an image card with image, title, and optional body text
*/
export function createImageCard(
imageUrl: string,
title: string,
body?: string,
options?: {
aspectRatio?: "1:1" | "1.51:1" | "1.91:1" | "4:3" | "16:9" | "20:13" | "2:1" | "3:1" ;
aspectMode?: "cover" | "fit" ;
action?: Action;
},
): FlexBubble {
const bubble: FlexBubble = {
type: "bubble" ,
hero: {
type: "image" ,
url: imageUrl,
size: "full" ,
aspectRatio: options?.aspectRatio ?? "20:13" ,
aspectMode: options?.aspectMode ?? "cover" ,
action: options?.action,
} as FlexImage,
body: {
type: "box" ,
layout: "vertical" ,
contents: [
{
type: "text" ,
text: title,
weight: "bold" ,
size: "xl" ,
wrap: true ,
} as FlexText,
],
paddingAll: "lg" ,
},
};
if (body && bubble.body) {
bubble.body.contents.push({
type: "text" ,
text: body,
size: "md" ,
wrap: true ,
margin: "md" ,
color: "#666666" ,
} as FlexText);
}
return bubble;
}
/**
* Create an action card with title, body, and action buttons
*/
export function createActionCard(
title: string,
body: string,
actions: CardAction[],
options?: {
imageUrl?: string;
aspectRatio?: "1:1" | "1.51:1" | "1.91:1" | "4:3" | "16:9" | "20:13" | "2:1" | "3:1" ;
},
): FlexBubble {
const bubble: FlexBubble = {
type: "bubble" ,
body: {
type: "box" ,
layout: "vertical" ,
contents: [
{
type: "text" ,
text: title,
weight: "bold" ,
size: "xl" ,
wrap: true ,
} as FlexText,
{
type: "text" ,
text: body,
size: "md" ,
wrap: true ,
margin: "md" ,
color: "#666666" ,
} as FlexText,
],
paddingAll: "lg" ,
},
footer: {
type: "box" ,
layout: "vertical" ,
contents: actions.slice(0 , 4 ).map(
(action, index) =>
({
type: "button" ,
action: action.action,
style: index === 0 ? "primary" : "secondary" ,
margin: index > 0 ? "sm" : undefined,
}) as FlexButton,
),
paddingAll: "md" ,
},
};
if (options?.imageUrl) {
bubble.hero = {
type: "image" ,
url: options.imageUrl,
size: "full" ,
aspectRatio: options.aspectRatio ?? "20:13" ,
aspectMode: "cover" ,
} as FlexImage;
}
return bubble;
}
/**
* Create a carousel container from multiple bubbles
* LINE allows max 12 bubbles in a carousel
*/
export function createCarousel(bubbles: FlexBubble[]): FlexCarousel {
return {
type: "carousel" ,
contents: bubbles.slice(0 , 12 ),
};
}
/**
* Create a notification bubble (for alerts, status updates)
*
* Editorial design: Bold status indicator with accent color,
* clear typography, optional icon for context.
*/
export function createNotificationBubble(
text: string,
options?: {
icon?: string;
type?: "info" | "success" | "warning" | "error" ;
title?: string;
},
): FlexBubble {
// Color based on notification type
const colors = {
info: { accent: "#3B82F6" , bg: "#EFF6FF" },
success: { accent: "#06C755" , bg: "#F0FDF4" },
warning: { accent: "#F59E0B" , bg: "#FFFBEB" },
error: { accent: "#EF4444" , bg: "#FEF2F2" },
};
const typeColors = colors[options?.type ?? "info" ];
const contents: FlexComponent[] = [];
// Accent bar
contents.push({
type: "box" ,
layout: "vertical" ,
contents: [],
width: "4px" ,
backgroundColor: typeColors.accent,
cornerRadius: "2px" ,
} as FlexBox);
// Content section
const textContents: FlexComponent[] = [];
if (options?.title) {
textContents.push({
type: "text" ,
text: options.title,
size: "md" ,
weight: "bold" ,
color: "#111111" ,
wrap: true ,
} as FlexText);
}
textContents.push({
type: "text" ,
text,
size: options?.title ? "sm" : "md" ,
color: options?.title ? "#666666" : "#333333" ,
wrap: true ,
margin: options?.title ? "sm" : undefined,
} as FlexText);
contents.push({
type: "box" ,
layout: "vertical" ,
contents: textContents,
flex: 1 ,
paddingStart: "lg" ,
} as FlexBox);
return {
type: "bubble" ,
body: {
type: "box" ,
layout: "horizontal" ,
contents,
paddingAll: "xl" ,
backgroundColor: typeColors.bg,
},
};
}
Messung V0.5 in Prozent C=96 H=93 G=94
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland