feat: whyrating - initial project from turbostarter boilerplate

This commit is contained in:
Alejandro Gutiérrez
2026-02-04 01:54:52 +01:00
commit 5cdc07cd39
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
---
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 (
<div data-liquid-type="typename" style={styles.wrapper}>
{/* content */}
</div>
);
}
// ============================================================================
// 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
<div data-liquid-type="kpi" style={styles.wrapper}>
<div data-liquid-type="line" style={styles.wrapper}>
<div data-liquid-type="table" style={styles.wrapper}>
```
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 (
<div style={styles.wrapper}>
{label && <div style={styles.header}>{label}</div>}
<div style={styles.empty}>No data available</div>
</div>
);
}
```
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';
<span>{formatDisplayValue(value)}</span>
```
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<string, unknown>[];
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 (
<div style={styles.placeholder}>
[Chart placeholder - {data.length} points]
</div>
);
}
// Browser-only rendering (Recharts, etc.)
return (
<ResponsiveContainer>
...
</ResponsiveContainer>
);
}
```
---
## 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;
```