// Gentelella v4 — markup helpers.
//
// Pure functions that return HTML strings. Use them when building content
// dynamically (rows from a fetched list, cards from an array, etc.) so you
// stop hand-writing 20–30 line scaffolds in every page script. Static pages
// keep their hand-written HTML — these aren't a templating engine, they're
// a duplication killer for JS-driven content.
//
// All user-supplied strings are auto-escaped via `escapeHtml`. Pass raw SVG
// or trusted HTML through fields documented as "html-trusted".
const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
/**
* Escape a string for safe insertion into HTML text or attribute values.
* @param {unknown} value
* @returns {string}
*/
export function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value).replace(/[&<>"']/g, (c) => ESC[c]);
}
const e = escapeHtml;
// ────────────────────────────────────────────────────────────────────────
// Page header — used on 46 pages
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {object} PageHeaderOptions
* @property {string} title Main page title.
* @property {string} [pretitle] Section label above the title.
* @property {string} [actionsHtml] html-trusted: raw markup for action buttons.
*/
/**
* Render a `.page-header` block.
* @param {PageHeaderOptions} opts
* @returns {string}
*/
export function pageHeader({ title, pretitle, actionsHtml }) {
const pre = pretitle ? `
${e(pretitle)}
` : '';
const actions = actionsHtml ? `${actionsHtml}
` : '';
return ``;
}
// ────────────────────────────────────────────────────────────────────────
// Stat tile — used on 8 dashboard pages
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {'teal'|'green'|'blue'|'yellow'|'red'|'purple'|'pink'|'azure'} StatColor
*
* @typedef {object} StatTileOptions
* @property {string} label
* @property {string} value
* @property {StatColor} [color] Defaults to 'teal'.
* @property {string} [iconHtml] html-trusted: raw SVG markup for the icon.
* @property {string} [subtext]
* @property {{ pct: string, direction: 'up'|'down' }} [change]
*/
/**
* Render a `.card .stat` tile (icon + label + value + optional change/subtext).
* @param {StatTileOptions} opts
* @returns {string}
*/
export function statTile({ label, value, color = 'teal', iconHtml = '', subtext, change }) {
const icon = iconHtml ? `${iconHtml}
` : '';
const chg = change
? `${e(change.pct)}`
: '';
const sub = subtext ? `${e(subtext)}
` : '';
return `${icon}
${e(label)}
${e(value)}${chg}
${sub}
`;
}
// ────────────────────────────────────────────────────────────────────────
// Status badge — used on 16+ pages
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {'green'|'blue'|'yellow'|'red'|'gray'} StatusColor
*/
/**
* Render a `.status` pill. Pass the visible label and the status color.
* @param {string} label
* @param {StatusColor} color
* @returns {string}
*/
export function statusBadge(label, color) {
return `${e(label)}`;
}
// ────────────────────────────────────────────────────────────────────────
// Customer cell — table cell with avatar + name (10 pages)
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {object} CustomerCellOptions
* @property {string} name
* @property {string} [initials] Two letters; defaults to first letters of name.
* @property {string} [avatarColor] CSS color value for the avatar background.
*/
/**
* Render a `.cell-customer` block (avatar circle + name). Use as the inner
* HTML of a ``.
* @param {CustomerCellOptions} opts
* @returns {string}
*/
export function customerCell({ name, initials, avatarColor = 'var(--primary)' }) {
const init = initials ?? name.split(/\s+/).slice(0, 2).map((w) => w[0] ?? '').join('').toUpperCase();
return ``;
}
// ────────────────────────────────────────────────────────────────────────
// Activity item — used in feeds, audit logs (5 pages)
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {object} ActivityItemOptions
* @property {string} bodyHtml html-trusted: rendered message (often contains ).
* @property {string} time Relative timestamp like "2 min ago".
* @property {string} [initials]
* @property {string} [avatarBg] CSS background for the avatar (any gradient or color).
*/
/**
* Render an `` row. Wrap multiple in
* ``.
* @param {ActivityItemOptions} opts
* @returns {string}
*/
export function activityItem({ bodyHtml, time, initials = '', avatarBg = 'var(--primary)' }) {
return ` ${e(initials)} `;
}
// ────────────────────────────────────────────────────────────────────────
// Visitor / distribution row (5 pages)
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {object} VisitorRowOptions
* @property {string} name
* @property {number} pct 0–100; rendered as "NN%" and as the bar fill width.
* @property {string} [flag] Emoji or short prefix (rendered before the name).
*/
/**
* Render a `.visitor-row` (label + percentage + horizontal bar).
* @param {VisitorRowOptions} opts
* @returns {string}
*/
export function visitorRow({ name, pct, flag = '' }) {
const f = flag ? `${e(flag)}` : '';
const w = Math.max(0, Math.min(100, pct));
return ``;
}
// ────────────────────────────────────────────────────────────────────────
// Empty state — used as a fallback when a list/table has 0 results
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {object} EmptyStateOptions
* @property {string} title
* @property {string} [desc] Description text below the title.
* @property {string} [iconHtml] html-trusted: raw SVG.
* @property {string} [actionHtml] html-trusted: a button or link.
*/
/**
* Render an empty-state block (icon + title + description + optional CTA).
* @param {EmptyStateOptions} opts
* @returns {string}
*/
export function emptyState({ title, desc, iconHtml, actionHtml }) {
const icon = iconHtml ? `${iconHtml} ` : '';
const d = desc ? `${e(desc)} ` : '';
const a = actionHtml ? `${actionHtml} ` : '';
return `${icon} ${e(title)} ${d}${a} `;
}
// ────────────────────────────────────────────────────────────────────────
// Banner — error/info/warn/success messages (12+ occurrences)
// ────────────────────────────────────────────────────────────────────────
/**
* @typedef {'danger'|'warning'|'info'|'success'} BannerVariant
*
* @typedef {object} BannerOptions
* @property {string} body
* @property {BannerVariant} [variant] Defaults to 'info'.
* @property {string} [title]
* @property {string} [iconHtml] html-trusted: raw SVG.
* @property {string} [actionsHtml] html-trusted: action buttons.
*/
/**
* Render a `.banner` alert (icon + title + body + actions).
* @param {BannerOptions} opts
* @returns {string}
*/
export function banner({ body, variant = 'info', title, iconHtml, actionsHtml }) {
const icon = iconHtml ? `${iconHtml} ` : '';
const t = title ? `${e(title)} ` : '';
const a = actionsHtml ? `${actionsHtml} ` : '';
return `${icon} ${t}${e(body)} ${a} `;
}
// ────────────────────────────────────────────────────────────────────────
// Skeleton row — table loading state
// ────────────────────────────────────────────────────────────────────────
/**
* Render N skeleton rows (each with `cols` cells of pulsing placeholder).
* Returns a string of `… ` ready to drop into a ``.
* @param {number} cols Number of columns per row.
* @param {number} [rows] Number of rows. Defaults to 5.
* @returns {string}
*/
export function skeletonRows(cols, rows = 5) {
const cell = ' | ';
const row = `${cell.repeat(cols)} `;
return row.repeat(rows);
}
|