new structure
This commit is contained in:
760
packages/docs/api/components.md
Normal file
760
packages/docs/api/components.md
Normal file
@@ -0,0 +1,760 @@
|
||||
# Components API 🧩
|
||||
|
||||
Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup.
|
||||
|
||||
## `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)`
|
||||
|
||||
Creates a custom element with reactive properties and automatic dependency tracking.
|
||||
|
||||
```javascript
|
||||
import { $, html } from 'sigpro';
|
||||
|
||||
$.component('my-button', (props, { slot, emit }) => {
|
||||
return html`
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => emit('click')}
|
||||
>
|
||||
${slot()}
|
||||
</button>
|
||||
`;
|
||||
}, ['variant']); // Observe the 'variant' attribute
|
||||
```
|
||||
|
||||
## 📋 API Reference
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `tagName` | `string` | required | Custom element tag name (must include a hyphen, e.g., `my-button`) |
|
||||
| `setupFunction` | `Function` | required | Function that returns the component's template |
|
||||
| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes (become reactive props) |
|
||||
| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) |
|
||||
|
||||
### Setup Function Parameters
|
||||
|
||||
The setup function receives two arguments:
|
||||
|
||||
1. **`props`** - Object containing reactive signals for each observed attribute
|
||||
2. **`context`** - Object with helper methods and properties
|
||||
|
||||
#### Context Object Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `slot(name)` | `Function` | Returns array of child nodes for the specified slot |
|
||||
| `emit(name, detail)` | `Function` | Dispatches a custom event |
|
||||
| `select(selector)` | `Function` | Query selector within component's root |
|
||||
| `selectAll(selector)` | `Function` | Query selector all within component's root |
|
||||
| `host` | `HTMLElement` | Reference to the custom element instance |
|
||||
| `root` | `Node` | Component's root (shadow root or element itself) |
|
||||
| `onUnmount(callback)` | `Function` | Register cleanup function |
|
||||
|
||||
## 🏠 Light DOM vs Shadow DOM
|
||||
|
||||
### Light DOM (`useShadowDOM = false`) - Default
|
||||
|
||||
The component **inherits global styles** from the application. Perfect for components that should integrate with your site's design system.
|
||||
|
||||
```javascript
|
||||
// Button that uses global Tailwind CSS
|
||||
$.component('tw-button', (props, { slot, emit }) => {
|
||||
const variant = props.variant() || 'primary';
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
|
||||
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
|
||||
outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50'
|
||||
};
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="px-4 py-2 rounded font-semibold transition-colors ${variants[variant]}"
|
||||
@click=${() => emit('click')}
|
||||
>
|
||||
${slot()}
|
||||
</button>
|
||||
`;
|
||||
}, ['variant']);
|
||||
```
|
||||
|
||||
### Shadow DOM (`useShadowDOM = true`) - Encapsulated
|
||||
|
||||
The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out.
|
||||
|
||||
```javascript
|
||||
// Calendar with encapsulated styles
|
||||
$.component('ui-calendar', (props) => {
|
||||
return html`
|
||||
<style>
|
||||
/* These styles won't affect the rest of the page */
|
||||
.calendar {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.day.selected {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="calendar">
|
||||
${renderCalendar(props.date())}
|
||||
</div>
|
||||
`;
|
||||
}, ['date'], true); // true = use Shadow DOM
|
||||
```
|
||||
|
||||
## 🎯 Basic Examples
|
||||
|
||||
### Simple Counter Component
|
||||
|
||||
```javascript
|
||||
// counter.js
|
||||
$.component('my-counter', (props) => {
|
||||
const count = $(0);
|
||||
|
||||
return html`
|
||||
<div class="counter">
|
||||
<p>Count: ${count}</p>
|
||||
<button @click=${() => count(c => c + 1)}>+</button>
|
||||
<button @click=${() => count(c => c - 1)}>-</button>
|
||||
<button @click=${() => count(0)}>Reset</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<my-counter></my-counter>
|
||||
```
|
||||
|
||||
### Component with Props
|
||||
|
||||
```javascript
|
||||
// greeting.js
|
||||
$.component('my-greeting', (props) => {
|
||||
const name = props.name() || 'World';
|
||||
const greeting = $(() => `Hello, ${name}!`);
|
||||
|
||||
return html`
|
||||
<div class="greeting">
|
||||
<h1>${greeting}</h1>
|
||||
<p>This is a greeting component.</p>
|
||||
</div>
|
||||
`;
|
||||
}, ['name']); // Observe the 'name' attribute
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<my-greeting name="John"></my-greeting>
|
||||
<my-greeting name="Jane"></my-greeting>
|
||||
```
|
||||
|
||||
### Component with Events
|
||||
|
||||
```javascript
|
||||
// toggle.js
|
||||
$.component('my-toggle', (props, { emit }) => {
|
||||
const isOn = $(props.initial() === 'on');
|
||||
|
||||
const toggle = () => {
|
||||
isOn(!isOn());
|
||||
emit('toggle', { isOn: isOn() });
|
||||
emit(isOn() ? 'on' : 'off');
|
||||
};
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="toggle ${() => isOn() ? 'active' : ''}"
|
||||
@click=${toggle}
|
||||
>
|
||||
${() => isOn() ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
`;
|
||||
}, ['initial']);
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<my-toggle
|
||||
initial="off"
|
||||
@toggle=${(e) => console.log('Toggled:', e.detail)}
|
||||
@on=${() => console.log('Turned on')}
|
||||
@off=${() => console.log('Turned off')}
|
||||
></my-toggle>
|
||||
```
|
||||
|
||||
## 🎨 Advanced Examples
|
||||
|
||||
### Form Input Component
|
||||
|
||||
```javascript
|
||||
// form-input.js
|
||||
$.component('form-input', (props, { emit }) => {
|
||||
const value = $(props.value() || '');
|
||||
const error = $(null);
|
||||
const touched = $(false);
|
||||
|
||||
// Validation effect
|
||||
$.effect(() => {
|
||||
if (props.pattern() && touched()) {
|
||||
const regex = new RegExp(props.pattern());
|
||||
const isValid = regex.test(value());
|
||||
error(isValid ? null : props.errorMessage() || 'Invalid input');
|
||||
emit('validate', { isValid, value: value() });
|
||||
}
|
||||
});
|
||||
|
||||
const handleInput = (e) => {
|
||||
value(e.target.value);
|
||||
emit('update', e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
touched(true);
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="form-group">
|
||||
${props.label() ? html`
|
||||
<label class="form-label">
|
||||
${props.label()}
|
||||
${props.required() ? html`<span class="required">*</span>` : ''}
|
||||
</label>
|
||||
` : ''}
|
||||
|
||||
<input
|
||||
type="${props.type() || 'text'}"
|
||||
class="form-control ${() => error() ? 'is-invalid' : ''}"
|
||||
:value=${value}
|
||||
@input=${handleInput}
|
||||
@blur=${handleBlur}
|
||||
placeholder="${props.placeholder() || ''}"
|
||||
?disabled=${props.disabled}
|
||||
?required=${props.required}
|
||||
/>
|
||||
|
||||
${() => error() ? html`
|
||||
<div class="error-message">${error()}</div>
|
||||
` : ''}
|
||||
|
||||
${props.helpText() ? html`
|
||||
<small class="help-text">${props.helpText()}</small>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']);
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<form-input
|
||||
label="Email"
|
||||
type="email"
|
||||
required
|
||||
pattern="^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
||||
errorMessage="Please enter a valid email"
|
||||
@update=${(e) => formData.email = e.detail}
|
||||
@validate=${(e) => setEmailValid(e.detail.isValid)}
|
||||
>
|
||||
</form-input>
|
||||
```
|
||||
|
||||
### Modal/Dialog Component
|
||||
|
||||
```javascript
|
||||
// modal.js
|
||||
$.component('my-modal', (props, { slot, emit, onUnmount }) => {
|
||||
const isOpen = $(false);
|
||||
|
||||
// Handle escape key
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape' && isOpen()) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
$.effect(() => {
|
||||
if (isOpen()) {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
const open = () => {
|
||||
isOpen(true);
|
||||
emit('open');
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
isOpen(false);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// Expose methods to parent
|
||||
props.open = open;
|
||||
props.close = close;
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<!-- Trigger button -->
|
||||
<button
|
||||
class="modal-trigger"
|
||||
@click=${open}
|
||||
>
|
||||
${slot('trigger') || 'Open Modal'}
|
||||
</button>
|
||||
|
||||
<!-- Modal overlay -->
|
||||
${() => isOpen() ? html`
|
||||
<div class="modal-overlay" @click=${close}>
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>${props.title() || 'Modal'}</h3>
|
||||
<button class="close-btn" @click=${close}>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${slot('body')}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${slot('footer') || html`
|
||||
<button @click=${close}>Close</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}, ['title'], false);
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<my-modal title="Confirm Delete">
|
||||
<button slot="trigger">Delete Item</button>
|
||||
|
||||
<div slot="body">
|
||||
<p>Are you sure you want to delete this item?</p>
|
||||
<p class="warning">This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<button class="cancel" @click=${close}>Cancel</button>
|
||||
<button class="delete" @click=${handleDelete}>Delete</button>
|
||||
</div>
|
||||
</my-modal>
|
||||
```
|
||||
|
||||
### Data Table Component
|
||||
|
||||
```javascript
|
||||
// data-table.js
|
||||
$.component('data-table', (props, { emit }) => {
|
||||
const data = $(props.data() || []);
|
||||
const columns = $(props.columns() || []);
|
||||
const sortColumn = $(null);
|
||||
const sortDirection = $('asc');
|
||||
const filterText = $('');
|
||||
|
||||
// Computed: filtered and sorted data
|
||||
const processedData = $(() => {
|
||||
let result = [...data()];
|
||||
|
||||
// Filter
|
||||
if (filterText()) {
|
||||
const search = filterText().toLowerCase();
|
||||
result = result.filter(row =>
|
||||
Object.values(row).some(val =>
|
||||
String(val).toLowerCase().includes(search)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (sortColumn()) {
|
||||
const col = sortColumn();
|
||||
const direction = sortDirection() === 'asc' ? 1 : -1;
|
||||
|
||||
result.sort((a, b) => {
|
||||
if (a[col] < b[col]) return -direction;
|
||||
if (a[col] > b[col]) return direction;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const handleSort = (col) => {
|
||||
if (sortColumn() === col) {
|
||||
sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
sortColumn(col);
|
||||
sortDirection('asc');
|
||||
}
|
||||
emit('sort', { column: col, direction: sortDirection() });
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="data-table">
|
||||
<!-- Search input -->
|
||||
<div class="table-toolbar">
|
||||
<input
|
||||
type="search"
|
||||
:value=${filterText}
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
<span class="record-count">
|
||||
${() => `${processedData().length} of ${data().length} records`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${columns().map(col => html`
|
||||
<th
|
||||
@click=${() => handleSort(col.field)}
|
||||
class:sortable=${true}
|
||||
class:sorted=${() => sortColumn() === col.field}
|
||||
>
|
||||
${col.label}
|
||||
${() => sortColumn() === col.field ? html`
|
||||
<span class="sort-icon">
|
||||
${sortDirection() === 'asc' ? '↑' : '↓'}
|
||||
</span>
|
||||
` : ''}
|
||||
</th>
|
||||
`)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${() => processedData().map(row => html`
|
||||
<tr @click=${() => emit('row-click', row)}>
|
||||
${columns().map(col => html`
|
||||
<td>${row[col.field]}</td>
|
||||
`)}
|
||||
</tr>
|
||||
`)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Empty state -->
|
||||
${() => processedData().length === 0 ? html`
|
||||
<div class="empty-state">
|
||||
No data found
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}, ['data', 'columns']);
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
const userColumns = [
|
||||
{ field: 'id', label: 'ID' },
|
||||
{ field: 'name', label: 'Name' },
|
||||
{ field: 'email', label: 'Email' },
|
||||
{ field: 'role', label: 'Role' }
|
||||
];
|
||||
|
||||
const userData = [
|
||||
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
||||
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
|
||||
];
|
||||
```
|
||||
|
||||
```html
|
||||
<data-table
|
||||
.data=${userData}
|
||||
.columns=${userColumns}
|
||||
@row-click=${(e) => console.log('Row clicked:', e.detail)}
|
||||
>
|
||||
</data-table>
|
||||
```
|
||||
|
||||
### Tabs Component
|
||||
|
||||
```javascript
|
||||
// tabs.js
|
||||
$.component('my-tabs', (props, { slot, emit }) => {
|
||||
const activeTab = $(props.active() || 0);
|
||||
|
||||
// Get all tab headers from slots
|
||||
const tabs = $(() => {
|
||||
const headers = slot('tab');
|
||||
return headers.map((node, index) => ({
|
||||
index,
|
||||
title: node.textContent,
|
||||
content: slot(`panel-${index}`)[0]
|
||||
}));
|
||||
});
|
||||
|
||||
$.effect(() => {
|
||||
emit('change', { index: activeTab(), tab: tabs()[activeTab()] });
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="tabs">
|
||||
<div class="tab-headers">
|
||||
${tabs().map(tab => html`
|
||||
<button
|
||||
class="tab-header ${() => activeTab() === tab.index ? 'active' : ''}"
|
||||
@click=${() => activeTab(tab.index)}
|
||||
>
|
||||
${tab.title}
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="tab-panels">
|
||||
${tabs().map(tab => html`
|
||||
<div
|
||||
class="tab-panel"
|
||||
style="display: ${() => activeTab() === tab.index ? 'block' : 'none'}"
|
||||
>
|
||||
${tab.content}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}, ['active']);
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```html
|
||||
<my-tabs @change=${(e) => console.log('Tab changed:', e.detail)}>
|
||||
<div slot="tab">Profile</div>
|
||||
<div slot="panel-0">
|
||||
<h3>Profile Settings</h3>
|
||||
<form>...</form>
|
||||
</div>
|
||||
|
||||
<div slot="tab">Security</div>
|
||||
<div slot="panel-1">
|
||||
<h3>Security Settings</h3>
|
||||
<form>...</form>
|
||||
</div>
|
||||
|
||||
<div slot="tab">Notifications</div>
|
||||
<div slot="panel-2">
|
||||
<h3>Notification Preferences</h3>
|
||||
<form>...</form>
|
||||
</div>
|
||||
</my-tabs>
|
||||
```
|
||||
|
||||
### Component with External Data
|
||||
|
||||
```javascript
|
||||
// user-profile.js
|
||||
$.component('user-profile', (props, { emit, onUnmount }) => {
|
||||
const user = $(null);
|
||||
const loading = $(false);
|
||||
const error = $(null);
|
||||
|
||||
// Fetch user data when userId changes
|
||||
$.effect(() => {
|
||||
const userId = props.userId();
|
||||
if (!userId) return;
|
||||
|
||||
loading(true);
|
||||
error(null);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/users/${userId}`, { signal: controller.signal })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
user(data);
|
||||
emit('loaded', data);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name !== 'AbortError') {
|
||||
error(err.message);
|
||||
emit('error', err);
|
||||
}
|
||||
})
|
||||
.finally(() => loading(false));
|
||||
|
||||
// Cleanup: abort fetch if component unmounts or userId changes
|
||||
onUnmount(() => controller.abort());
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="user-profile">
|
||||
${() => loading() ? html`
|
||||
<div class="spinner">Loading...</div>
|
||||
` : error() ? html`
|
||||
<div class="error">Error: ${error()}</div>
|
||||
` : user() ? html`
|
||||
<div class="user-info">
|
||||
<img src="${user().avatar}" class="avatar" />
|
||||
<h2>${user().name}</h2>
|
||||
<p>${user().email}</p>
|
||||
<p>Member since: ${new Date(user().joined).toLocaleDateString()}</p>
|
||||
</div>
|
||||
` : html`
|
||||
<div class="no-user">No user selected</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}, ['user-id']);
|
||||
```
|
||||
|
||||
## 📦 Component Libraries
|
||||
|
||||
### Building a Reusable Component Library
|
||||
|
||||
```javascript
|
||||
// components/index.js
|
||||
import { $, html } from 'sigpro';
|
||||
|
||||
// Button component
|
||||
export const Button = $.component('ui-button', (props, { slot, emit }) => {
|
||||
const variant = props.variant() || 'primary';
|
||||
const size = props.size() || 'md';
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-1 text-sm',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-6 py-3 text-lg'
|
||||
};
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
|
||||
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
|
||||
danger: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
};
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="rounded font-semibold transition-colors ${sizes[size]} ${variants[variant]}"
|
||||
?disabled=${props.disabled}
|
||||
@click=${() => emit('click')}
|
||||
>
|
||||
${slot()}
|
||||
</button>
|
||||
`;
|
||||
}, ['variant', 'size', 'disabled']);
|
||||
|
||||
// Card component
|
||||
export const Card = $.component('ui-card', (props, { slot }) => {
|
||||
return html`
|
||||
<div class="card border rounded-lg shadow-sm overflow-hidden">
|
||||
${props.title() ? html`
|
||||
<div class="card-header bg-gray-50 px-4 py-3 border-b">
|
||||
<h3 class="font-semibold">${props.title()}</h3>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="card-body p-4">
|
||||
${slot()}
|
||||
</div>
|
||||
|
||||
${props.footer() ? html`
|
||||
<div class="card-footer bg-gray-50 px-4 py-3 border-t">
|
||||
${slot('footer')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}, ['title']);
|
||||
|
||||
// Badge component
|
||||
export const Badge = $.component('ui-badge', (props, { slot }) => {
|
||||
const type = props.type() || 'default';
|
||||
|
||||
const types = {
|
||||
default: 'bg-gray-100 text-gray-800',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
warning: 'bg-yellow-100 text-yellow-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
info: 'bg-blue-100 text-blue-800'
|
||||
};
|
||||
|
||||
return html`
|
||||
<span class="inline-block px-2 py-1 text-xs font-semibold rounded ${types[type]}">
|
||||
${slot()}
|
||||
</span>
|
||||
`;
|
||||
}, ['type']);
|
||||
|
||||
export { $, html };
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
import { Button, Card, Badge } from './components/index.js';
|
||||
|
||||
// Use components anywhere
|
||||
const app = html`
|
||||
<div>
|
||||
<Card title="Welcome">
|
||||
<p>This is a card component</p>
|
||||
<div slot="footer">
|
||||
<Button variant="primary" @click=${handleClick}>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Badge type="success">New</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
## 🎯 Decision Guide: Light DOM vs Shadow DOM
|
||||
|
||||
| Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... |
|
||||
|--------------------------------|-------------------------------|
|
||||
| Component is part of your main app | Building a UI library for others |
|
||||
| Using global CSS (Tailwind, Bootstrap) | Creating embeddable widgets |
|
||||
| Need to inherit theme variables | Styles must be pixel-perfect everywhere |
|
||||
| Working with existing design system | Component has complex, specific styles |
|
||||
| Quick prototyping | Distributing to different projects |
|
||||
| Form elements that should match site | Need style isolation/encapsulation |
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Native Web Components** | Built on Custom Elements standard |
|
||||
| **Reactive Props** | Observed attributes become signals |
|
||||
| **Two Rendering Modes** | Light DOM (default) or Shadow DOM |
|
||||
| **Automatic Cleanup** | Effects and listeners cleaned up on disconnect |
|
||||
| **Event System** | Custom events with `emit()` |
|
||||
| **Slot Support** | Full slot API for content projection |
|
||||
| **Zero Dependencies** | Pure vanilla JavaScript |
|
||||
|
||||
---
|
||||
|
||||
> **Pro Tip:** Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.
|
||||
Reference in New Issue
Block a user