b0nes
Version:
Zero-dependency component library and SSR/SSG framework
179 lines (166 loc) • 6.48 kB
JavaScript
import { box } from '../../atoms/index.js';
import { processSlotTrusted } from '../../utils/processSlot.js';
import { normalizeClasses } from '../../utils/normalizeClasses.js';
import { validatePropTypes } from '../../utils/componentError.js';
import { escapeAttr } from '../../utils/escapeAttr.js';
/**
* Card component - A flexible content container molecule
*
* Renders a card component using the box atom with structured content areas.
* Supports two modes:
* 1. Custom mode: Use the slot prop for full control over card content
* 2. Structured mode: Use headerSlot, mediaSlot, linkSlot, contentSlot for organized layout
*
* Cards are commonly used for:
* - Product displays
* - Blog post previews
* - User profiles
* - Feature highlights
* - Gallery items
*
* @param {Object} props - Component properties
* @param {string | Array<string>} [props.slot] - Custom card content (overrides structured slots)
* @param {string | Array<string>} [props.headerSlot] - Card header content (title, badges, etc.)
* @param {string | Array<string>} [props.mediaSlot] - Card media content (images, videos)
* @param {string | Array<string>} [props.linkSlot] - Card action links or buttons
* @param {string | Array<string>} [props.contentSlot] - Main card content (description, details)
* @param {string} [props.attrs=''] - Additional HTML attributes (e.g., 'data-product-id="123"')
* @param {string} [props.className=''] - Additional CSS classes to apply
* @param {string} [props.id] - Card ID attribute
* @param {string} [props.role] - ARIA role attribute (default: none, consider 'article' for semantic cards)
*
* @returns {string} Rendered HTML card element (div with card class)
*
* @example
* // Custom card with full control
* card({
* className: 'profile-card',
* slot: [
* '<img src="/avatar.jpg" alt="User"/>',
* '<h3>John Doe</h3>',
* '<p>Software Developer</p>'
* ]
* })
* // Returns: '<div class="box card profile-card"><img src="/avatar.jpg" alt="User"/><h3>John Doe</h3><p>Software Developer</p></div>'
*
* @example
* // Structured card with all sections
* card({
* headerSlot: '<h2>Product Name</h2>',
* mediaSlot: '<img src="/product.jpg" alt="Product"/>',
* contentSlot: '<p>Product description goes here...</p>',
* linkSlot: '<a href="/products/123">View Details</a>'
* })
* // Returns: '<div class="box card"><h2>Product Name</h2><img src="/product.jpg" alt="Product"/><a href="/products/123">View Details</a><p>Product description goes here...</p></div>'
*
* @example
* // Blog post card
* card({
* className: 'blog-card',
* headerSlot: [
* '<span class="date">Jan 15, 2025</span>',
* '<h3>Blog Post Title</h3>'
* ],
* mediaSlot: '<img src="/blog-cover.jpg" alt="Blog cover"/>',
* contentSlot: '<p>Brief excerpt from the blog post...</p>',
* linkSlot: '<a href="/blog/post-slug">Read More →</a>'
* })
*
* @example
* // Minimal card with just header and content
* card({
* headerSlot: '<h4>Card Title</h4>',
* contentSlot: 'Simple card content without media or links.'
* })
* // Returns: '<div class="box card"><h4>Card Title</h4>Simple card content without media or links.</div>'
*
* @example
* // Card with ID and semantic role
* card({
* id: 'featured-product',
* role: 'article',
* className: 'featured',
* headerSlot: '<h2>Featured Item</h2>',
* contentSlot: '<p>Special featured content</p>',
* attrs: 'data-featured="true"'
* })
* // Returns: '<div class="box card featured" id="featured-product" role="article" data-featured="true">...</div>'
*
* @example
* // Interactive card with button
* card({
* className: 'action-card',
* headerSlot: '<h3>Get Started</h3>',
* contentSlot: '<p>Sign up today and get 30 days free!</p>',
* linkSlot: '<button class="btn primary">Sign Up Now</button>'
* })
*/
export const card = ({
slot,
headerSlot,
mediaSlot,
linkSlot,
contentSlot,
attrs = '',
className = '',
id,
role
}) => {
// Validate prop types
validatePropTypes(
{ attrs, className },
{
attrs: 'string',
className: 'string'
},
{ componentName: 'card', componentType: 'molecule' }
);
// Determine if using custom mode or structured mode
const hasCustomSlot = slot !== undefined && slot !== null && slot !== '';
const hasStructuredSlots = headerSlot || mediaSlot || linkSlot || contentSlot;
// Warn if both modes are used (custom slot takes precedence)
if (hasCustomSlot && hasStructuredSlots) {
console.warn(
`[b0nes Warning] Card has both 'slot' and structured slots (headerSlot, mediaSlot, etc.). ` +
`The 'slot' prop takes precedence and structured slots will be ignored.`
);
}
// Warn if card is completely empty
if (!hasCustomSlot && !hasStructuredSlots) {
console.warn(
`[b0nes Warning] Card has no content. ` +
`Provide either a 'slot' prop or structured slots (headerSlot, contentSlot, etc.).`
);
}
// Process attributes
const escapedAttrs = attrs ? ` ${attrs}` : '';
// Add id attribute if provided
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
// Add role attribute if provided
const roleAttr = role ? ` role="${escapeAttr(role)}"` : '';
// Build the final content
let finalContent;
if (hasCustomSlot) {
// Custom mode: use slot directly
finalContent = processSlotTrusted(slot);
} else {
// Structured mode: combine all structured slots in order
// Order: header → media → link → content
const header = processSlotTrusted(headerSlot) || '';
const media = processSlotTrusted(mediaSlot) || '';
const link = processSlotTrusted(linkSlot) || '';
const content = processSlotTrusted(contentSlot) || '';
finalContent = `${header}${media}${link}${content}`;
}
// Normalize classes - card class plus any custom classes
const classes = normalizeClasses(['card', className]);
// Use box component as the base
// Construct attrs string for box with id, role, and other attrs
const boxAttrs = `${idAttr}${roleAttr}${escapedAttrs}`.trim();
return box({
is: 'div',
className: classes,
attrs: boxAttrs,
slot: finalContent
});
};