Files
openclaw/src/line/flex-templates/basic-cards.ts
2026-02-17 13:36:48 +09:00

396 lines
8.6 KiB
TypeScript

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,
},
};
}