Overview
What this template helps teams build
Overview
The KPI Card Set template is a reusable custom code control concept for Yeeflow builders. It helps teams add a focused interface pattern for KPI monitoring and dashboard summaries while keeping the asset in review before public launch.
Key capabilities
- Supports dashboard & analytics scenarios
- Designed for use in Dashboard, Application page
- Includes TSX source, user guidance, and example configuration
- Prepared as a preview asset for technical review and future public documentation
Recommended use cases
- KPI monitoring and dashboard summaries
- Dashboard & Analytics template library planning
- Internal builder education and implementation review
Governance notes
This record is created in review status for editorial and product validation. It should not be positioned as launchable until implementation, security, runtime, and documentation review are complete.
Developer reference
Build patterns behind this template
Use this template as a reference while reviewing the Custom Code developer guide. Learn how the required export structure, input parameters, and rendering patterns fit together.
Source preview
Code preview
import React from 'react';
type KpiCard = {
title: string;
value: any;
subtitle: string;
trend: string;
trendDirection: string;
tone: string;
prefix: string;
suffix: string;
format: string;
decimals?: number;
icon: string;
targetLabel: string;
targetValue: string;
};
export class CodeInApplication implements CodeInComp {
description() {
return 'KPI Card Set - reusable Yeeflow dashboard custom code template for displaying configurable KPI cards.';
}
requiredFields(params?: CodeInParams) {
return [];
}
inputParameters(): InputParameter[] {
return [
{
id: 'cardsConfig',
name: 'Cards Config',
type: 'variable',
desc: 'Expression-editor value containing KPI card configuration. Supports JSON array, object with cards/items/data, variable, temp variable, or expression result.',
},
{
id: 'titleText',
name: 'Title Text',
type: 'string',
desc: 'Optional title shown above the KPI card set.',
},
{
id: 'subtitleText',
name: 'Subtitle Text',
type: 'string',
desc: 'Optional supporting text shown below the title.',
},
{
id: 'layoutMode',
name: 'Layout Mode',
type: 'string',
desc: 'Card layout: auto, one, two, three, or four. Default is auto.',
},
{
id: 'cardTone',
name: 'Default Card Tone',
type: 'string',
desc: 'Default tone for cards without their own tone: blue, green, amber, red, violet, or slate.',
},
{
id: 'titleField',
name: 'Title Field',
type: 'variable',
desc: 'Optional source data field used as each KPI card title. Example: MetricName.',
},
{
id: 'valueField',
name: 'Value Field',
type: 'variable',
desc: 'Optional source data field used as each KPI card value. Example: MetricValue.',
},
{
id: 'subtitleField',
name: 'Subtitle Field',
type: 'variable',
desc: 'Optional source data field used as each KPI card subtitle/helper text.',
},
{
id: 'trendField',
name: 'Trend Field',
type: 'variable',
desc: 'Optional source data field used as each KPI card trend/change text.',
},
{
id: 'trendDirectionField',
name: 'Trend Direction Field',
type: 'variable',
desc: 'Optional source data field used as trend direction: up, down, or neutral.',
},
{
id: 'toneField',
name: 'Tone Field',
type: 'variable',
desc: 'Optional source data field used as card tone: blue, green, amber, red, violet, or slate.',
},
{
id: 'formatField',
name: 'Format Field',
type: 'variable',
desc: 'Optional source data field used as value format: auto, number, percent, or text.',
},
{
id: 'prefixField',
name: 'Prefix Field',
type: 'variable',
desc: 'Optional source data field used as text before the KPI value.',
},
{
id: 'suffixField',
name: 'Suffix Field',
type: 'variable',
desc: 'Optional source data field used as text after the KPI value.',
},
{
id: 'iconField',
name: 'Icon Field',
type: 'variable',
desc: 'Optional source data field used as the small card marker/icon text.',
},
{
id: 'targetLabelField',
name: 'Target Label Field',
type: 'variable',
desc: 'Optional source data field used as the target label.',
},
{
id: 'targetValueField',
name: 'Target Value Field',
type: 'variable',
desc: 'Optional source data field used as the target value.',
},
{
id: 'decimalsField',
name: 'Decimals Field',
type: 'variable',
desc: 'Optional source data field used as card-specific decimal places.',
},
{
id: 'showTrend',
name: 'Show Trend',
type: 'variable',
desc: 'Whether trend/change text should be shown. Supports true/false or dynamic expression.',
},
{
id: 'showIcon',
name: 'Show Icon',
type: 'variable',
desc: 'Whether card icons/initial markers should be shown. Supports true/false or dynamic expression.',
},
{
id: 'compactMode',
name: 'Compact Mode',
type: 'variable',
desc: 'Whether compact spacing is enabled. Supports true/false or dynamic expression.',
},
{
id: 'emptyStateText',
name: 'Empty State Text',
type: 'string',
desc: 'Text shown when no KPI card configuration is available.',
},
{
id: 'numberLocale',
name: 'Number Locale',
type: 'string',
desc: 'Locale used for number formatting, such as en-US. Leave blank for browser default.',
},
{
id: 'decimalPlaces',
name: 'Decimal Places',
type: 'string',
desc: 'Default decimal places for numeric KPI values. Default is 0.',
},
{
id: 'minCardWidth',
name: 'Minimum Card Width',
type: 'string',
desc: 'Minimum card width in pixels when layoutMode is auto. Default is 220.',
},
{
id: 'valueSize',
name: 'Value Size',
type: 'string',
desc: 'Value text size: small, medium, or large. Default is medium.',
},
];
}
render(context: CodeInContext, fieldsValues: any, readonly: boolean) {
return (
<KpiCardSet
context={context}
fieldsValues={fieldsValues}
readonly={readonly}
params={(context && context.params) || {}}
/>
);
}
}
class KpiCardSet extends React.Component<any, any> {
getConfig() {
const params = this.props.params || {};
const defaultTone = this.normalizeTone(this.normalizeToString(params.cardTone) || 'blue');
const fieldMap = {
title: this.normalizeToString(params.titleField),
value: this.normalizeToString(params.valueField),
subtitle: this.normalizeToString(params.subtitleField),
trend: this.normalizeToString(params.trendField),
trendDirection: this.normalizeToString(params.trendDirectionField),
tone: this.normalizeToString(params.toneField),
format: this.normalizeToString(params.formatField),
prefix: this.normalizeToString(params.prefixField),
suffix: this.normalizeToString(params.suffixField),
icon: this.normalizeToString(params.iconField),
targetLabel: this.normalizeToString(params.targetLabelField),
targetValue: this.normalizeToString(params.targetValueField),
decimals: this.normalizeToString(params.decimalsField),
};
return {
cards: this.parseCardsConfig(params.cardsConfig, defaultTone, fieldMap),
titleText: this.normalizeToString(params.titleText),
subtitleText: this.normalizeToString(params.subtitleText),
layoutMode: this.normalizeLayoutMode(this.normalizeToString(params.layoutMode)),
cardTone: defaultTone,
fieldMap: fieldMap,
showTrend: this.normalizeToBoolean(params.showTrend, true),
showIcon: this.normalizeToBoolean(params.showIcon, true),
compactMode: this.normalizeToBoolean(params.compactMode, false),
emptyStateText: this.normalizeToString(params.emptyStateText) || 'No KPI cards configured',
numberLocale: this.normalizeToString(params.numberLocale),
decimalPlaces: this.normalizeToNumber(params.decimalPlaces, 0),
minCardWidth: Math.max(160, this.normalizeToNumber(params.minCardWidth, 220)),
valueSize: this.normalizeValueSize(this.normalizeToString(params.valueSize)),
};
}
parseCardsConfig(rawValue: any, defaultTone: string, fieldMap: any): KpiCard[] {
const resolved = this.resolveParameterValue(rawValue);
const parsed = this.safeParseJson(resolved);
const source = parsed !== null && parsed !== undefined ? parsed : resolved;
const cardSource = this.extractCardArray(source);
return cardSource
.map((item: any, index: number) => this.normalizeCard(item, index, defaultTone, fieldMap))
.filter((card: KpiCard | null) => Boolean(card)) as KpiCard[];
}
extractCardArray(value: any): any[] {
if (Array.isArray(value)) {
return value;
}
if (value && typeof value === 'object') {
const candidates = [
value.cards,
value.Cards,
value.items,
value.Items,
value.data,
value.Data,
value.list,
value.List,
value.values,
value.Values,
];
for (let index = 0; index < candidates.length; index += 1) {
if (Array.isArray(candidates[index])) {
return candidates[index];
}
}
if (value.title !== undefined || value.label !== undefined || value.value !== undefined) {
return [value];
}
}
const text = this.normalizeToString(value);
if (!text) {
return [];
}
/*
Fallback for simple manual configuration:
Each line can be "Title | Value | Subtitle | Trend | Tone".
JSON is recommended for production use because it is clearer and safer.
*/
return text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.split('|').map((part) => part.trim()));
}
normalizeCard(item: any, index: number, defaultTone: string, fieldMap: any): KpiCard | null {
if (Array.isArray(item)) {
const title = this.normalizeToString(item[0]);
const value = item.length > 1 ? item[1] : '';
if (!title && this.normalizeToString(value) === '') {
return null;
}
return {
title: title || 'KPI ' + (index + 1),
value: value,
subtitle: this.normalizeToString(item[2]),
trend: this.normalizeToString(item[3]),
trendDirection: this.inferTrendDirection(item[3]),
tone: this.normalizeTone(this.normalizeToString(item[4]) || defaultTone),
prefix: '',
suffix: '',
format: 'auto',
icon: '',
targetLabel: '',
targetValue: '',
};
}
if (item && typeof item === 'object') {
const title = this.normalizeToString(
this.firstDefined([
this.readMappedField(item, fieldMap.title),
item.title,
item.label,
item.name,
item.heading,
])
);
const value = this.firstDefined([
this.readMappedField(item, fieldMap.value),
item.value,
item.count,
item.number,
item.total,
item.amount,
]);
if (!title && this.normalizeToString(value) === '') {
return null;
}
const trend = this.normalizeToString(
this.firstDefined([
this.readMappedField(item, fieldMap.trend),
item.trend,
item.trendValue,
item.change,
item.delta,
])
);
const trendDirection = this.normalizeToString(
this.firstDefined([
this.readMappedField(item, fieldMap.trendDirection),
item.trendDirection,
item.direction,
item.status,
])
) || this.inferTrendDirection(trend);
const decimalsValue = this.firstDefined([
this.readMappedField(item, fieldMap.decimals),
item.decimals,
]);
return {
title: title || 'KPI ' + (index + 1),
value: value,
subtitle: this.normalizeToString(
this.firstDefined([
this.readMappedField(item, fieldMap.subtitle),
item.subtitle,
item.helper,
item.description,
item.caption,
])
),
trend: trend,
trendDirection: this.normalizeTrendDirection(trendDirection),
tone: this.normalizeTone(this.normalizeToString(this.firstDefined([
this.readMappedField(item, fieldMap.tone),
item.tone,
item.color,
item.accent,
])) || defaultTone),
prefix: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.prefix), item.prefix])),
suffix: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.suffix), item.suffix])),
format: this.normalizeFormat(this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.format), item.format]))),
decimals: decimalsValue !== undefined ? this.normalizeToNumber(decimalsValue, 0) : undefined,
icon: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.icon), item.icon, item.initials])),
targetLabel: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.targetLabel), item.targetLabel, item.targetName])),
targetValue: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.targetValue), item.targetValue, item.target])),
};
}
const primitiveValue = this.normalizeToString(item);
if (!primitiveValue) {
return null;
}
return {
title: 'KPI ' + (index + 1),
value: primitiveValue,
subtitle: '',
trend: '',
trendDirection: 'neutral',
tone: defaultTone,
prefix: '',
suffix: '',
format: 'auto',
icon: '',
targetLabel: '',
targetValue: '',
};
}
firstDefined(values: any[]) {
for (let index = 0; index < values.length; index += 1) {
if (values[index] !== undefined && values[index] !== null) {
return values[index];
}
}
return undefined;
}
readMappedField(item: any, fieldName: string): any {
const fieldPath = this.normalizeToString(fieldName);
if (!item || !fieldPath) {
return undefined;
}
const directValue = this.readObjectValueByKey(item, fieldPath);
if (directValue !== undefined) {
return directValue;
}
if (fieldPath.indexOf('.') !== -1) {
const parts = fieldPath.split('.');
let current = item;
for (let index = 0; index < parts.length; index += 1) {
current = this.readObjectValueByKey(current, parts[index]);
if (current === undefined || current === null) {
return undefined;
}
}
return current;
}
return undefined;
}
readObjectValueByKey(source: any, wantedKey: string): any {
if (!source || typeof source !== 'object' || Array.isArray(source) || !wantedKey) {
return undefined;
}
if (source[wantedKey] !== undefined) {
return source[wantedKey];
}
const normalizedWanted = wantedKey.toLowerCase();
const keys = Object.keys(source);
for (let index = 0; index < keys.length; index += 1) {
if (keys[index].toLowerCase() === normalizedWanted) {
return source[keys[index]];
}
}
return undefined;
}
resolveParameterValue(value: any): any {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) {
return value;
}
if (typeof value === 'object') {
const preferred =
value.value !== undefined ? value.value :
value.Value !== undefined ? value.Value :
value.data !== undefined ? value.data :
value.Data !== undefined ? value.Data :
value.items !== undefined ? value.items :
value.Items !== undefined ? value.Items :
value.cards !== undefined ? value.cards :
value.Cards !== undefined ? value.Cards :
value.result !== undefined ? value.result :
value.Result !== undefined ? value.Result :
value.key !== undefined ? value.key :
value.Key !== undefined ? value.Key :
value.label !== undefined ? value.label :
value.Label !== undefined ? value.Label : undefined;
if (preferred !== undefined) {
return this.resolveParameterValue(preferred);
}
}
return value;
}
safeParseJson(value: any): any {
if (Array.isArray(value) || (value && typeof value === 'object')) {
return value;
}
const text = this.normalizeToString(value);
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
return null;
}
}
normalizeToString(value: any): string {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return String(value).trim();
}
if (Array.isArray(value)) {
return value.map((item) => this.normalizeToString(item)).filter(Boolean).join(', ');
}
if (typeof value === 'object') {
const resolved =
value.value !== undefined ? value.value :
value.Value !== undefined ? value.Value :
value.text !== undefined ? value.text :
value.Text !== undefined ? value.Text :
value.display !== undefined ? value.display :
value.Display !== undefined ? value.Display :
value.label !== undefined ? value.label :
value.Label !== undefined ? value.Label :
value.name !== undefined ? value.name :
value.Name !== undefined ? value.Name :
value.title !== undefined ? value.title :
value.Title !== undefined ? value.Title :
value.key !== undefined ? value.key :
value.Key !== undefined ? value.Key : undefined;
if (resolved !== undefined) {
return this.normalizeToString(resolved);
}
try {
return JSON.stringify(value);
} catch (error) {
return '';
}
}
return String(value).trim();
}
normalizeToBoolean(value: any, fallback: boolean): boolean {
if (value === true || value === false) {
return value;
}
if (Array.isArray(value) && value.length > 0) {
return this.normalizeToBoolean(value[0], fallback);
}
if (value && typeof value === 'object') {
const objectValue =
value.value !== undefined ? value.value :
value.Value !== undefined ? value.Value :
value.checked !== undefined ? value.checked :
value.Checked !== undefined ? value.Checked :
value.selected !== undefined ? value.selected :
value.Selected !== undefined ? value.Selected :
value.key !== undefined ? value.key :
value.Key !== undefined ? value.Key :
value.label !== undefined ? value.label :
value.Label !== undefined ? value.Label : undefined;
if (objectValue !== undefined) {
return this.normalizeToBoolean(objectValue, fallback);
}
}
const text = this.normalizeToString(value).toLowerCase();
if (['true', '1', 'yes', 'y', 'on', 'show', 'enabled'].indexOf(text) !== -1) {
return true;
}
if (['false', '0', 'no', 'n', 'off', 'hide', 'disabled'].indexOf(text) !== -1) {
return false;
}
return fallback;
}
normalizeToNumber(value: any, fallback: number): number {
if (typeof value === 'number' && isFinite(value)) {
return value;
}
const text = this.normalizeToString(value).replace(/,/g, '').replace(/%/g, '');
if (!text) {
return fallback;
}
const parsed = parseFloat(text);
return isFinite(parsed) ? parsed : fallback;
}
formatValue(card: KpiCard, config: any): string {
const rawText = this.normalizeToString(card.value);
const format = this.normalizeFormat(card.format);
if (format === 'text') {
return card.prefix + rawText + card.suffix;
}
const numericValue = this.toNumberOrNull(card.value);
if (numericValue === null || format === 'auto' && !this.looksNumeric(rawText)) {
return card.prefix + rawText + card.suffix;
}
const decimals = card.decimals !== undefined ? card.decimals : config.decimalPlaces;
const normalizedDecimals = Math.max(0, Math.min(6, decimals));
let formatted = '';
try {
formatted = numericValue.toLocaleString(config.numberLocale || undefined, {
minimumFractionDigits: normalizedDecimals,
maximumFractionDigits: normalizedDecimals,
});
} catch (error) {
formatted = String(numericValue);
}
if (format === 'percent') {
return card.prefix + formatted + '%' + card.suffix;
}
return card.prefix + formatted + card.suffix;
}
toNumberOrNull(value: any): number | null {
if (typeof value === 'number' && isFinite(value)) {
return value;
}
const text = this.normalizeToString(value).replace(/,/g, '').replace(/%/g, '');
if (!text) {
return null;
}
const parsed = parseFloat(text);
return isFinite(parsed) ? parsed : null;
}
looksNumeric(value: string) {
return /^-?\d+(\.\d+)?%?$/.test((value || '').replace(/,/g, '').trim());
}
normalizeLayoutMode(value: string) {
const normalized = (value || '').toLowerCase();
if (['one', 'two', 'three', 'four', 'auto'].indexOf(normalized) !== -1) {
return normalized;
}
if (['1', '2', '3', '4'].indexOf(normalized) !== -1) {
return ['one', 'two', 'three', 'four'][parseInt(normalized, 10) - 1];
}
return 'auto';
}
normalizeTone(value: string) {
const normalized = (value || '').toLowerCase();
if (['blue', 'green', 'amber', 'red', 'violet', 'slate'].indexOf(normalized) !== -1) {
return normalized;
}
return 'blue';
}
normalizeFormat(value: string) {
const normalized = (value || '').toLowerCase();
if (['auto', 'number', 'text', 'percent'].indexOf(normalized) !== -1) {
return normalized;
}
return 'auto';
}
normalizeValueSize(value: string) {
const normalized = (value || '').toLowerCase();
if (['small', 'medium', 'large'].indexOf(normalized) !== -1) {
return normalized;
}
return 'medium';
}
normalizeTrendDirection(value: any) {
const normalized = this.normalizeToString(value).toLowerCase();
if (['up', 'positive', 'good', 'increase', 'success'].indexOf(normalized) !== -1) {
return 'up';
}
if (['down', 'negative', 'bad', 'decrease', 'danger'].indexOf(normalized) !== -1) {
return 'down';
}
return 'neutral';
}
inferTrendDirection(value: any) {
const text = this.normalizeToString(value);
if (text.indexOf('-') === 0) {
return 'down';
}
if (text.indexOf('+') === 0) {
return 'up';
}
return 'neutral';
}
getGridStyle(config: any) {
const minWidth = config.minCardWidth + 'px';
const map: any = {
one: 'repeat(1, minmax(0, 1fr))',
two: 'repeat(2, minmax(0, 1fr))',
three: 'repeat(3, minmax(0, 1fr))',
four: 'repeat(4, minmax(0, 1fr))',
auto: 'repeat(auto-fit, minmax(' + minWidth + ', 1fr))',
};
return {
gridTemplateColumns: map[config.layoutMode] || map.auto,
};
}
getCardIcon(card: KpiCard) {
const configured = this.normalizeToString(card.icon);
if (configured) {
return configured.substring(0, 3).toUpperCase();
}
const words = this.normalizeToString(card.title).split(/\s+/).filter(Boolean);
if (words.length === 0) {
return 'KPI';
}
if (words.length === 1) {
return words[0].substring(0, 2).toUpperCase();
}
return (words[0].charAt(0) + words[1].charAt(0)).toUpperCase();
}
renderTrend(card: KpiCard, config: any) {
if (!config.showTrend || !card.trend) {
return null;
}
return (
<span className={'kcs-trend kcs-trend-' + this.normalizeTrendDirection(card.trendDirection)}>
{card.trend}
</span>
);
}
renderTarget(card: KpiCard) {
if (!card.targetLabel && !card.targetValue) {
return null;
}
return (
<div className="kcs-target">
{card.targetLabel ? <span>{card.targetLabel}</span> : null}
{card.targetValue ? <strong>{card.targetValue}</strong> : null}
</div>
);
}
renderCard(card: KpiCard, index: number, config: any) {
return (
<div className={'kcs-card kcs-tone-' + card.tone} key={index}>
<div className="kcs-card-top">
<div>
<div className="kcs-title">{card.title}</div>
{card.subtitle ? <div className="kcs-subtitle">{card.subtitle}</div> : null}
</div>
{config.showIcon ? <div className="kcs-icon">{this.getCardIcon(card)}</div> : null}
</div>
<div className={'kcs-value kcs-value-' + config.valueSize}>{this.formatValue(card, config)}</div>
<div className="kcs-card-bottom">
{this.renderTrend(card, config)}
{this.renderTarget(card)}
</div>
</div>
);
}
renderEmptyState(config: any) {
return (
<div className="kcs-empty">
<div className="kcs-empty-title">{config.emptyStateText}</div>
<div className="kcs-empty-help">Configure cardsConfig with KPI card data to show dashboard metrics.</div>
</div>
);
}
render() {
const config = this.getConfig();
const rootClass = 'kpi-card-set' + (config.compactMode ? ' kcs-compact' : '');
const styles = [
'.kpi-card-set{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;color:#172033;width:100%;box-sizing:border-box;}',
'.kcs-header{margin:0 0 14px 0;}',
'.kcs-heading{font-size:18px;line-height:1.3;font-weight:700;color:#111827;margin:0;}',
'.kcs-support{font-size:13px;line-height:1.45;color:#64748b;margin:4px 0 0 0;}',
'.kcs-grid{display:grid;gap:14px;width:100%;}',
'.kcs-card{position:relative;overflow:hidden;min-height:138px;border:1px solid #d9e2ef;border-radius:8px;background:#fff;padding:16px;box-sizing:border-box;box-shadow:0 1px 2px rgba(15,23,42,0.04);}',
'.kcs-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:#2f73ff;}',
'.kcs-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;}',
'.kcs-title{font-size:13px;line-height:1.35;font-weight:650;color:#344054;}',
'.kcs-subtitle{font-size:12px;line-height:1.4;color:#7a8699;margin-top:3px;}',
'.kcs-icon{flex:0 0 auto;min-width:34px;height:28px;border-radius:7px;background:#eef4ff;border:1px solid #cfe0ff;color:#185abc;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:750;letter-spacing:0;}',
'.kcs-value{font-weight:750;color:#0f172a;margin-top:18px;line-height:1.05;letter-spacing:0;}',
'.kcs-value-small{font-size:24px;}',
'.kcs-value-medium{font-size:30px;}',
'.kcs-value-large{font-size:38px;}',
'.kcs-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:24px;margin-top:12px;}',
'.kcs-trend{display:inline-flex;align-items:center;min-height:24px;border-radius:999px;padding:0 9px;font-size:12px;font-weight:700;border:1px solid transparent;}',
'.kcs-trend-up{color:#067647;background:#ecfdf3;border-color:#abefc6;}',
'.kcs-trend-down{color:#b42318;background:#fef3f2;border-color:#fecdca;}',
'.kcs-trend-neutral{color:#475467;background:#f8fafc;border-color:#e4e7ec;}',
'.kcs-target{display:flex;align-items:baseline;gap:5px;font-size:12px;color:#667085;min-width:0;}',
'.kcs-target strong{font-size:12px;color:#344054;font-weight:700;white-space:nowrap;}',
'.kcs-tone-blue:before{background:#2f73ff;}.kcs-tone-blue .kcs-icon{background:#eef4ff;border-color:#cfe0ff;color:#185abc;}',
'.kcs-tone-green:before{background:#12b76a;}.kcs-tone-green .kcs-icon{background:#ecfdf3;border-color:#abefc6;color:#067647;}',
'.kcs-tone-amber:before{background:#f79009;}.kcs-tone-amber .kcs-icon{background:#fffaeb;border-color:#fedf89;color:#b54708;}',
'.kcs-tone-red:before{background:#f04438;}.kcs-tone-red .kcs-icon{background:#fef3f2;border-color:#fecdca;color:#b42318;}',
'.kcs-tone-violet:before{background:#7a5af8;}.kcs-tone-violet .kcs-icon{background:#f4f3ff;border-color:#d9d6fe;color:#5925dc;}',
'.kcs-tone-slate:before{background:#667085;}.kcs-tone-slate .kcs-icon{background:#f8fafc;border-color:#e4e7ec;color:#475467;}',
'.kcs-empty{border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc;padding:18px;color:#64748b;}',
'.kcs-empty-title{font-size:14px;font-weight:700;color:#344054;}',
'.kcs-empty-help{font-size:12px;line-height:1.5;margin-top:4px;}',
'.kcs-compact .kcs-grid{gap:10px;}',
'.kcs-compact .kcs-card{min-height:112px;padding:12px;}',
'.kcs-compact .kcs-value{margin-top:12px;}',
'@media(max-width:640px){.kcs-grid{grid-template-columns:1fr!important;}.kcs-value-large{font-size:32px;}.kcs-card{min-height:auto;}.kcs-card-bottom{align-items:flex-start;flex-direction:column;}}',
].join('');
return (
<div className={rootClass}>
<style>{styles}</style>
{config.titleText || config.subtitleText ? (
<div className="kcs-header">
{config.titleText ? <div className="kcs-heading">{config.titleText}</div> : null}
{config.subtitleText ? <div className="kcs-support">{config.subtitleText}</div> : null}
</div>
) : null}
{config.cards.length > 0 ? (
<div className="kcs-grid" style={this.getGridStyle(config)}>
{config.cards.map((card: KpiCard, index: number) => this.renderCard(card, index, config))}
</div>
) : this.renderEmptyState(config)}
</div>
);
}
}Implementation notes
User guide
# KPI Card Set
## A. Template name
KPI Card Set
## B. Short description
KPI Card Set is a reusable Yeeflow dashboard custom code template that displays a clean set of configurable KPI cards from a JSON array, variable, temp variable, or expression result.
## C. Purpose / what this is for
This template helps teams present important business metrics on Yeeflow dashboards in a consistent, readable format. It is useful when a dashboard needs to show a quick operational summary, such as totals, counts, performance values, service levels, or current status indicators.
Customers use this control when they want a polished KPI section without building custom UI from scratch each time. It keeps the display logic reusable while allowing each app or dashboard to provide different metric data.
## D. Supported placement
This template is optimized for:
- Dashboard page
It can display values from fixed JSON, dashboard variables, temp variables, or expressions. It does not write values back to form fields or temp variables.
## E. When to use this control
Use this control when:
- A dashboard needs 2-8 summary metrics.
- Metrics are already available as variables, temp variables, or expression results.
- The same KPI card layout should be reused across multiple dashboards.
- Delivery teams need a clean, enterprise-ready dashboard component.
- You want consistent visual treatment for totals, trends, targets, and status indicators.
Do not use this control when the dashboard needs a full chart, pivot table, drill-down table, or complex data exploration experience.
## F. Input parameters overview table
| Parameter name | Type | Required | Purpose | Example value |
| --- | --- | --- | --- | --- |
| `cardsConfig` | Expression / variable | Recommended | KPI card data as JSON array, object, variable, temp variable, or expression result. | See example below |
| `titleText` | Plain text | Optional | Title shown above the KPI card set. | `Executive Summary` |
| `subtitleText` | Plain text | Optional | Supporting text below the title. | `Current operational performance` |
| `layoutMode` | Plain text | Optional | Card layout: `auto`, `one`, `two`, `three`, or `four`. | `auto` |
| `cardTone` | Plain text | Optional | Default card color tone. | `blue` |
| `titleField` | Expression / variable | Optional | Source JSON field used as the card title. | `MetricName` |
| `valueField` | Expression / variable | Optional | Source JSON field used as the card value. | `MetricValue` |
| `subtitleField` | Expression / variable | Optional | Source JSON field used as subtitle/helper text. | `Description` |
| `trendField` | Expression / variable | Optional | Source JSON field used as trend/change text. | `ChangeText` |
| `trendDirectionField` | Expression / variable | Optional | Source JSON field used as trend direction. | `ChangeDirection` |
| `toneField` | Expression / variable | Optional | Source JSON field used as card tone. | `StatusColor` |
| `formatField` | Expression / variable | Optional | Source JSON field used as value format. | `ValueFormat` |
| `prefixField` | Expression / variable | Optional | Source JSON field used as value prefix. | `CurrencySymbol` |
| `suffixField` | Expression / variable | Optional | Source JSON field used as value suffix. | `UnitText` |
| `iconField` | Expression / variable | Optional | Source JSON field used as icon/marker text. | `ShortCode` |
| `targetLabelField` | Expression / variable | Optional | Source JSON field used as target label. | `GoalLabel` |
| `targetValueField` | Expression / variable | Optional | Source JSON field used as target value. | `GoalValue` |
| `decimalsField` | Expression / variable | Optional | Source JSON field used as decimal places. | `DecimalPlaces` |
| `showTrend` | Expression / variable | Optional | Shows or hides trend labels. | `true` |
| `showIcon` | Expression / variable | Optional | Shows or hides card icon/initial markers. | `true` |
| `compactMode` | Expression / variable | Optional | Uses tighter spacing for dense dashboards. | `false` |
| `emptyStateText` | Plain text | Optional | Text shown when no card data is configured. | `No KPI data available` |
| `numberLocale` | Plain text | Optional | Locale for number formatting. | `en-US` |
| `decimalPlaces` | Numeric config text | Optional | Default decimal places for numeric values. | `0` |
| `minCardWidth` | Numeric config text | Optional | Minimum card width in auto layout. | `220` |
| `valueSize` | Plain text | Optional | KPI value size: `small`, `medium`, or `large`. | `medium` |
## G. Detailed parameter explanation
### `cardsConfig`
The main KPI data source. This parameter uses the expression editor type so it can support static JSON, dashboard variables, temp variables, complex variables, or dynamic expressions.
Recommended format:
```json
[
{
"title": "Open Requests",
"value": 42,
"subtitle": "Pending review",
"trend": "+12%",
"trendDirection": "up",
"tone": "blue",
"format": "number"
}
]
```
Supported card fields include:
| Field | Purpose | Example |
| --- | --- | --- |
| `title` | Card title. | `Open Requests` |
| `value` | Main KPI value. | `42` |
| `subtitle` | Short supporting text. | `Pending review` |
| `trend` | Change or trend label. | `+12%` |
| `trendDirection` | Trend style: `up`, `down`, or `neutral`. | `up` |
| `tone` | Card tone: `blue`, `green`, `amber`, `red`, `violet`, `slate`. | `green` |
| `format` | Value format: `auto`, `number`, `percent`, or `text`. | `number` |
| `prefix` | Text before the value. | `$` |
| `suffix` | Text after the value. | `hrs` |
| `decimals` | Card-specific decimal places. | `1` |
| `icon` | Small text marker shown in the icon area. | `REQ` |
| `targetLabel` | Optional target label. | `Target` |
| `targetValue` | Optional target value. | `95%` |
If `cardsConfig` is an object with `cards`, `items`, `data`, `list`, or `values`, the template will try to use that array. If it is a JSON string, the template will parse it safely. If it is invalid or blank, the empty state is shown.
You can use `cardsConfig` in two ways:
1. Use the built-in KPI schema shown above, with fields such as `title`, `value`, `subtitle`, and `trend`.
2. Pass any JSON array and use the field-mapping parameters below to tell the template which source fields to read.
Flexible source data example:
```json
[
{
"MetricName": "Open Requests",
"MetricValue": 42,
"Description": "Pending review",
"ChangeText": "+12%",
"ChangeDirection": "up",
"StatusColor": "blue",
"ValueFormat": "number"
}
]
```
For the example above, configure:
| Parameter | Value |
| --- | --- |
| `titleField` | `MetricName` |
| `valueField` | `MetricValue` |
| `subtitleField` | `Description` |
| `trendField` | `ChangeText` |
| `trendDirectionField` | `ChangeDirection` |
| `toneField` | `StatusColor` |
| `formatField` | `ValueFormat` |
Field names are matched case-insensitively. Nested paths are also supported using dot notation, such as `metric.name` or `summary.currentValue`.
### Field mapping parameters
Field mapping parameters are optional expression-editor parameters. Use them when the data source uses customer-specific field names instead of the built-in KPI schema.
If a mapping parameter is blank, the template falls back to the built-in field names. For example, if `titleField` is blank, the template looks for `title`, `label`, `name`, or `heading`.
### `titleField`
The field in each source JSON item used as the KPI title.
Example: `MetricName`
### `valueField`
The field in each source JSON item used as the main KPI value.
Example: `MetricValue`
### `subtitleField`
The field used as the small helper text under the title.
Example: `Description`
### `trendField`
The field used as the trend or change label.
Example: `ChangeText`
### `trendDirectionField`
The field used to style the trend as `up`, `down`, or `neutral`.
Example: `ChangeDirection`
### `toneField`
The field used as each card's color tone.
Example: `StatusColor`
### `formatField`
The field used as each value's display format.
Example: `ValueFormat`
### `prefixField` and `suffixField`
Fields used to place text before or after the KPI value.
Examples: `CurrencySymbol`, `UnitText`
### `iconField`
The field used as the small card marker text.
Example: `ShortCode`
### `targetLabelField` and `targetValueField`
Fields used to show target or goal information at the bottom of the card.
Examples: `GoalLabel`, `GoalValue`
### `decimalsField`
The field used as card-specific decimal places.
Example: `DecimalPlaces`
### `titleText`
Optional title shown above the card grid. Use this when the KPI section needs a dashboard heading.
Example: `Procurement Overview`
### `subtitleText`
Optional helper text under the title. Keep it short and business-friendly.
Example: `Live summary for active supplier requests`
### `layoutMode`
Controls how many columns the card grid tries to use.
Supported values:
- `auto`: responsive layout based on available width
- `one`: one column
- `two`: two columns
- `three`: three columns
- `four`: four columns
Use `auto` for most dashboards.
### `cardTone`
Default visual tone for cards that do not define their own `tone`.
Supported values:
- `blue`
- `green`
- `amber`
- `red`
- `violet`
- `slate`
Use calm tones that match the business meaning. For example, green for healthy values, amber for warning, red for risk, blue for neutral operational metrics.
### `showTrend`
Controls whether trend labels are shown. This is an expression-editor parameter, so it can be a fixed `true`/`false`, dashboard variable, temp variable, or expression.
Accepted values include `true`, `false`, `1`, `0`, `yes`, `no`, `on`, and `off`.
### `showIcon`
Controls whether the small card marker is shown. If a card has an `icon`, that text is shown. Otherwise, the template derives initials from the card title.
This parameter supports expression-editor values.
### `compactMode`
Uses tighter spacing and smaller card height. This is useful for dense dashboards or narrow dashboard sections.
This parameter supports expression-editor values.
### `emptyStateText`
Text shown when no valid cards are available.
Example: `No KPI data available`
### `numberLocale`
Locale used for number formatting. Leave blank to use the browser default.
Examples:
- `en-US`
- `en-GB`
- `de-DE`
### `decimalPlaces`
Default number of decimal places for numeric KPI values. A card can override this with its own `decimals` field.
Example: `1`
### `minCardWidth`
Minimum card width in pixels when `layoutMode` is `auto`.
Recommended values:
- `200` for dense dashboards
- `220` for normal dashboards
- `260` for wider executive dashboards
### `valueSize`
Controls the size of the KPI value.
Supported values:
- `small`
- `medium`
- `large`
Use `medium` for most dashboards.
## H. Step-by-step setup guide
1. Add a Custom Code control to a Yeeflow dashboard page.
2. Open the custom code editor.
3. Paste the `kpi-card-set.tsx` code.
4. Save the code.
5. Open input parameters.
6. Configure `cardsConfig` with a JSON array or select a dashboard variable/temp variable that returns the KPI card array.
7. If the source JSON does not use the built-in KPI field names, configure `titleField`, `valueField`, `subtitleField`, `trendField`, and any other field mappings you need.
8. Optionally configure `titleText` and `subtitleText`.
9. Set `layoutMode` to `auto` unless a fixed column count is needed.
10. Configure `showTrend`, `showIcon`, and `compactMode` as needed.
11. Save the dashboard.
12. Preview and confirm the card values, layout, number formatting, trend styles, and mobile/narrow behavior.
## I. Result / expected output
After setup, users will see a responsive set of KPI cards on the dashboard. Each card can show:
- title
- main value
- subtitle
- trend/change label
- tone/accent color
- optional icon marker
- optional target label/value
The template is display-only. It does not save values back into Yeeflow fields or variables.
## J. Real business examples
### Procurement dashboard
Show open purchase requests, approved requests this month, overdue supplier reviews, and average approval cycle time.
### Customer service dashboard
Show open tickets, SLA compliance, average response time, and escalations.
### HR operations dashboard
Show active onboarding cases, pending approvals, monthly new hires, and training completion rate.
### Asset management dashboard
Show total assets, maintenance due, overdue inspections, and asset utilization.
## K. Notes / assumptions / limitations
- This template is optimized for dashboard display.
- It expects KPI values to be prepared by variables, temp variables, expressions, or upstream dashboard logic.
- It supports custom source JSON field names through field mapping parameters.
- It does not query a Yeeflow data list directly in this version.
- It does not save output values.
- Invalid JSON or empty card data will show the empty state instead of crashing.
- `percent` format appends `%` to the provided numeric value. If you want `85%`, provide `85`, not `0.85`.
- Use short card titles and subtitles for best dashboard readability.
## L. Testing checklist
- Confirm the dashboard renders with a valid `cardsConfig` JSON array.
- Confirm field mappings work with customer-specific JSON field names.
- Confirm blank field mappings fall back to the built-in KPI schema.
- Confirm the empty state appears when `cardsConfig` is blank or invalid.
- Test `layoutMode`: `auto`, `two`, `three`, and `four`.
- Test `showTrend` with `true` and `false`.
- Test `showIcon` with `true` and `false`.
- Test `compactMode` with `true` and `false`.
- Test numeric values with `numberLocale` and `decimalPlaces`.
- Test card-level tones: `blue`, `green`, `amber`, `red`, `violet`, `slate`.
- Test mobile or narrow dashboard width.
- Test expression-editor input using a dashboard temp variable.
## M. Troubleshooting
### No cards appear
Check that `cardsConfig` is not blank and contains a valid JSON array or an expression result with a card array. If the data is inside an object, use a property such as `cards`, `items`, or `data`.
### The value formatting looks wrong
Check each card's `format`, `prefix`, `suffix`, and `decimals`. For percent values, pass the display number directly, such as `92`, and use `"format": "percent"`.
### Trend color is not correct
Set `trendDirection` on the card to `up`, `down`, or `neutral`. If this is omitted, the template infers `up` from values starting with `+` and `down` from values starting with `-`.
### The layout has too many or too few columns
Use `layoutMode` for fixed layouts or `minCardWidth` to tune responsive `auto` layout.
### A variable or temp variable does not render as expected
Check what the variable returns. The template supports arrays, JSON strings, and objects with `cards`, `items`, or `data`, but the final resolved value must contain card data.Configuration
Example configuration
# KPI Card Set Example Configuration
Use either example in the `cardsConfig` parameter for a quick dashboard preview.
## Example A: Built-in KPI schema
This example works without field mapping parameters because it uses the template's built-in field names.
```json
[
{
"title": "Open Requests",
"value": 42,
"subtitle": "Pending review",
"trend": "+12%",
"trendDirection": "up",
"tone": "blue",
"format": "number",
"icon": "REQ",
"targetLabel": "Target",
"targetValue": "< 50"
},
{
"title": "Approval Rate",
"value": 91.4,
"subtitle": "This month",
"trend": "+4.2%",
"trendDirection": "up",
"tone": "green",
"format": "percent",
"decimals": 1,
"icon": "APR",
"targetLabel": "Goal",
"targetValue": "90%"
},
{
"title": "Overdue Items",
"value": 7,
"subtitle": "Need attention",
"trend": "-3",
"trendDirection": "down",
"tone": "amber",
"format": "number",
"icon": "OD"
},
{
"title": "Cycle Time",
"value": 3.8,
"subtitle": "Average business days",
"trend": "Stable",
"trendDirection": "neutral",
"tone": "slate",
"format": "number",
"decimals": 1,
"suffix": " days",
"icon": "CT"
}
]
```
Recommended parameter values:
| Parameter | Value |
| --- | --- |
| `titleText` | `Operational KPI Summary` |
| `subtitleText` | `Live dashboard metrics for the current reporting period` |
| `layoutMode` | `auto` |
| `cardTone` | `blue` |
| `showTrend` | `true` |
| `showIcon` | `true` |
| `compactMode` | `false` |
| `numberLocale` | `en-US` |
| `decimalPlaces` | `0` |
| `minCardWidth` | `220` |
| `valueSize` | `medium` |
## Example B: Custom source JSON with field mappings
This example is useful when the source data comes from an API, dashboard variable, temp variable, AI result, or data transform that uses different field names.
`cardsConfig`:
```json
[
{
"MetricName": "Open Requests",
"MetricValue": 42,
"MetricDescription": "Pending review",
"ChangeText": "+12%",
"ChangeDirection": "up",
"StatusColor": "blue",
"ValueFormat": "number",
"Marker": "REQ",
"GoalLabel": "Target",
"GoalValue": "< 50"
},
{
"MetricName": "Approval Rate",
"MetricValue": 91.4,
"MetricDescription": "This month",
"ChangeText": "+4.2%",
"ChangeDirection": "up",
"StatusColor": "green",
"ValueFormat": "percent",
"DecimalPlaces": 1,
"Marker": "APR",
"GoalLabel": "Goal",
"GoalValue": "90%"
},
{
"MetricName": "Overdue Items",
"MetricValue": 7,
"MetricDescription": "Need attention",
"ChangeText": "-3",
"ChangeDirection": "down",
"StatusColor": "amber",
"ValueFormat": "number",
"Marker": "OD"
},
{
"MetricName": "Cycle Time",
"MetricValue": 3.8,
"MetricDescription": "Average business days",
"ChangeText": "Stable",
"ChangeDirection": "neutral",
"StatusColor": "slate",
"ValueFormat": "number",
"DecimalPlaces": 1,
"UnitText": " days",
"Marker": "CT"
}
]
```
Field mapping parameter values:
| Parameter | Value |
| --- | --- |
| `titleField` | `MetricName` |
| `valueField` | `MetricValue` |
| `subtitleField` | `MetricDescription` |
| `trendField` | `ChangeText` |
| `trendDirectionField` | `ChangeDirection` |
| `toneField` | `StatusColor` |
| `formatField` | `ValueFormat` |
| `suffixField` | `UnitText` |
| `iconField` | `Marker` |
| `targetLabelField` | `GoalLabel` |
| `targetValueField` | `GoalValue` |
| `decimalsField` | `DecimalPlaces` |





