--- description: --- --- --- title: How to Create a LiquidRender Component purpose: Creating or modifying LiquidRender components answers: - How should I structure a component file? - How do I use design tokens? - What's the difference between dynamic and static variants? - How do I handle empty states? - How do I format values for display? read_when: Creating or modifying a component skip_when: Just using existing components depends_on: files: - packages/liquid-render/src/renderer/components/utils.ts - packages/liquid-render/docs/COMPONENT-GUIDE.md --- # How to Create a LiquidRender Component > **Read when:** Creating or modifying a component ## Sections | Section | Summary | |---------|---------| | [File Structure](#file-structure) | Types, Styles, Helpers, Sub-components, Main, Static | | [Design Tokens](#design-tokens) | Import from utils.ts, never hardcode values | | [Data Attribute](#data-attribute) | Required `data-liquid-type` on root element | | [Empty States](#empty-states) | Always handle null/empty data gracefully | | [Value Formatting](#value-formatting) | Use formatDisplayValue() and fieldToLabel() | | [Static Variants](#static-variants) | Provide both dynamic and static exports | | [SSR Handling](#ssr-handling) | Browser detection for chart components | | [Checklist](#checklist) | Quick validation before submitting | --- ## File Structure > **TL;DR:** Follow strict section order: Types, Styles, Helpers, Sub-components, Main, Static. Every component file uses section headers for organization: ```tsx // [ComponentName] Component - Brief description import React from 'react'; import type { LiquidComponentProps } from './utils'; import { tokens, cardStyles, mergeStyles } from './utils'; import { resolveBinding } from '../data-context'; // ============================================================================ // Types // ============================================================================ interface ComponentSpecificType { ... } // ============================================================================ // Styles // ============================================================================ const styles = { wrapper: { ... }, }; // ============================================================================ // Helpers // ============================================================================ function helperFunction() { ... } // ============================================================================ // Sub-components (if needed) // ============================================================================ function SubComponent() { ... } // ============================================================================ // Main Component // ============================================================================ export function ComponentName({ block, data }: LiquidComponentProps): React.ReactElement { // 1. Resolve bindings const value = resolveBinding(block.binding, data); // 2. Extract block properties const label = block.label; const color = getBlockColor(block); // 3. Render return (
{/* content */}
); } // ============================================================================ // Static Component (standalone usage) // ============================================================================ interface StaticComponentProps { ... } export function StaticComponent(props: StaticComponentProps): React.ReactElement { // For use outside LiquidUI context } export default ComponentName; ``` --- ## Design Tokens > **TL;DR:** Import `tokens` from utils.ts. Never hardcode colors, spacing, or sizes. ```tsx import { tokens } from './utils'; // CORRECT padding: tokens.spacing.md, fontSize: tokens.fontSize.sm, color: tokens.colors.foreground, borderRadius: tokens.radius.lg, // INCORRECT - never hardcode padding: '16px', fontSize: '14px', color: '#0a0a0a', ``` ### Available Token Categories | Category | Examples | Values | |----------|----------|--------| | `tokens.colors.*` | `foreground`, `border`, `success` | CSS variables with fallbacks | | `tokens.spacing.*` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl` | 4px to 48px | | `tokens.radius.*` | `sm`, `md`, `lg`, `xl`, `full` | 4px to 9999px | | `tokens.fontSize.*` | `xs`, `sm`, `base`, `lg`, `xl` | 12px to 36px | | `tokens.fontWeight.*` | `normal`, `medium`, `semibold`, `bold` | 400 to 700 | | `tokens.shadow.*` | `none`, `sm`, `md`, `lg` | Box shadows | | `tokens.transition.*` | `fast`, `normal`, `slow` | 150ms to 300ms | ### Style Helpers ```tsx import { cardStyles, buttonStyles, inputStyles, mergeStyles } from './utils'; // Card-like containers const styles = { wrapper: mergeStyles(cardStyles(), { padding: tokens.spacing.md, }), }; // Buttons with variants const btnStyle = buttonStyles('default', 'md'); // variant, size // Input fields const inputStyle = inputStyles({ /* overrides */ }); ``` ### Chart Colors ```tsx import { chartColors } from './utils'; // Consistent palette for data visualization stroke={chartColors[i % chartColors.length]} ``` --- ## Data Attribute > **TL;DR:** Every component MUST have `data-liquid-type` on its root element. ```tsx
``` This enables: - CSS targeting for styling overrides - Testing queries for component selection - Debug inspection in DevTools --- ## Empty States > **TL;DR:** Always check for null/empty data and render a graceful fallback. ```tsx if (!data || data.length === 0) { return (
{label &&
{label}
}
No data available
); } ``` Handle all edge cases: - `null` or `undefined` data - Empty arrays - Missing required fields --- ## Value Formatting > **TL;DR:** Use `formatDisplayValue()` for values and `fieldToLabel()` for auto-labels. ### formatDisplayValue() ```tsx import { formatDisplayValue } from './utils'; {formatDisplayValue(value)} ``` Handles: - Large numbers: `1234567` becomes `1.2M` - Thousands: `12345` becomes `12.3K` - Null/undefined: displays `--` - Booleans: `Yes`/`No` - Dates: Localized format ### fieldToLabel() ```tsx import { fieldToLabel } from './utils'; // Auto-generate labels from field names fieldToLabel('totalRevenue') // "Total Revenue" fieldToLabel('order_count') // "Order Count" fieldToLabel('avgValue') // "Avg Value" ``` ### Common Pattern ```tsx const label = block.label || fieldToLabel(block.binding?.field || ''); ``` --- ## Static Variants > **TL;DR:** Export both `ComponentName` (dynamic) and `StaticComponent` (standalone). ### Dynamic Component (DSL-driven) Used by the LiquidUI renderer. Receives `LiquidComponentProps`: ```tsx export function DataTable({ block, data }: LiquidComponentProps) { const resolvedData = resolveBinding(block.binding, data); // ... } ``` ### Static Component (standalone) Used directly in React apps with explicit props: ```tsx interface StaticTableProps { data: Record[]; columns?: string[]; title?: string; sortable?: boolean; } export function StaticTable({ data, columns, title, sortable }: StaticTableProps) { // No binding resolution needed } ``` ### Export Pattern ```tsx // In component file export { ComponentName, StaticComponent }; export default ComponentName; // In index.ts - register for DSL usage export const liquidComponents = { typename: ComponentName, }; ``` --- ## SSR Handling > **TL;DR:** Use `isBrowser` check for components requiring browser APIs. ```tsx import { isBrowser } from './utils'; export function ChartComponent({ block, data }: LiquidComponentProps) { if (!isBrowser) { return (
[Chart placeholder - {data.length} points]
); } // Browser-only rendering (Recharts, etc.) return ( ... ); } ``` --- ## Checklist Before submitting a new component, verify: - [ ] File follows standard structure with section headers - [ ] Uses `tokens` for all style values (no hardcoded colors/spacing) - [ ] Has `data-liquid-type` attribute on root element - [ ] Handles empty/null data states - [ ] Uses `formatDisplayValue()` for value display - [ ] Uses `fieldToLabel()` for auto-labels - [ ] Has SSR placeholder if browser-dependent - [ ] Includes both dynamic and static variants - [ ] Registered in `liquidComponents` map - [ ] Exported from `index.ts` --- ## Quick Reference ### Imports ```tsx import type { LiquidComponentProps } from './utils'; import { tokens, chartColors, cardStyles, buttonStyles, inputStyles, mergeStyles, getLayoutStyles, getBlockColor, formatDisplayValue, fieldToLabel, generateId, isBrowser, } from './utils'; import { resolveBinding } from '../data-context'; ``` ### Props Interface ```tsx interface LiquidComponentProps { block: Block; // Parsed block from DSL data: DataContext; // Data for binding resolution children?: ReactNode; className?: string; } ``` ### Block Properties ```tsx const value = resolveBinding(block.binding, data); const label = block.label || fieldToLabel(block.binding?.field || ''); const color = getBlockColor(block); const layoutStyles = getLayoutStyles(block); const columns = block.columns; ```