experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(components): Phase 4 complete - components, theming, extension system, developer guide

+336
+336
app/components/README.md
··· 1556 1556 - `name` attribute for exclusive accordions - Chrome 120+, Safari 17.2+ 1557 1557 1558 1558 Supported in all modern browsers (Chrome 120+, Firefox 125+, Safari 17.2+, Edge 120+). 1559 + 1560 + --- 1561 + 1562 + ## Extension Developer Guide 1563 + 1564 + Complete guide for building Peek extensions with the component library. 1565 + 1566 + ### Getting Started 1567 + 1568 + ```javascript 1569 + // Import components you need 1570 + import { 1571 + PeekButton, PeekCard, PeekList, 1572 + registerExtension, setTheme 1573 + } from 'peek://app/components/index.js'; 1574 + 1575 + // Or import everything 1576 + import 'peek://app/components/index.js'; 1577 + ``` 1578 + 1579 + ### Project Structure 1580 + 1581 + ``` 1582 + my-extension/ 1583 + ├── manifest.json 1584 + ├── background.js 1585 + ├── content.js # Content script 1586 + ├── popup/ 1587 + │ ├── popup.html 1588 + │ └── popup.js 1589 + └── styles/ 1590 + └── theme.js # Custom theme tokens 1591 + ``` 1592 + 1593 + ### Content Script Pattern 1594 + 1595 + Content scripts need style isolation to avoid conflicts with host pages: 1596 + 1597 + ```javascript 1598 + // content.js 1599 + import { initContentScript } from 'peek://app/components/extension.js'; 1600 + 1601 + const { container, destroy } = initContentScript({ 1602 + id: 'my-extension', 1603 + theme: { 1604 + 'theme-accent': '#9b59b6' 1605 + }, 1606 + render: (shadow) => { 1607 + // Shadow DOM container - styles are isolated 1608 + shadow.innerHTML = ` 1609 + <div class="my-extension-ui"> 1610 + <peek-card> 1611 + <span slot="header">My Extension</span> 1612 + <peek-list selection="single"> 1613 + <peek-list-item value="1">Item 1</peek-list-item> 1614 + <peek-list-item value="2">Item 2</peek-list-item> 1615 + </peek-list> 1616 + <div slot="footer"> 1617 + <peek-button variant="primary">Action</peek-button> 1618 + </div> 1619 + </peek-card> 1620 + </div> 1621 + `; 1622 + 1623 + // Add event listeners 1624 + shadow.querySelector('peek-list').addEventListener('selection-change', (e) => { 1625 + console.log('Selected:', e.detail); 1626 + }); 1627 + } 1628 + }); 1629 + 1630 + // Cleanup when extension unloads 1631 + window.addEventListener('unload', destroy); 1632 + ``` 1633 + 1634 + ### Popup Pattern 1635 + 1636 + ```html 1637 + <!-- popup.html --> 1638 + <!DOCTYPE html> 1639 + <html> 1640 + <head> 1641 + <style> 1642 + body { width: 320px; padding: 16px; margin: 0; } 1643 + </style> 1644 + </head> 1645 + <body> 1646 + <peek-card> 1647 + <span slot="header">Settings</span> 1648 + <peek-switch id="darkMode">Dark Mode</peek-switch> 1649 + <peek-select id="theme" placeholder="Choose theme"></peek-select> 1650 + </peek-card> 1651 + <script type="module" src="popup.js"></script> 1652 + </body> 1653 + </html> 1654 + ``` 1655 + 1656 + ```javascript 1657 + // popup.js 1658 + import { initPopup, setTheme, getThemeNames } from 'peek://app/components/index.js'; 1659 + 1660 + initPopup({ id: 'my-popup' }); 1661 + 1662 + // Setup theme selector 1663 + const themeSelect = document.getElementById('theme'); 1664 + themeSelect.options = getThemeNames(); 1665 + themeSelect.addEventListener('change', (e) => setTheme(e.detail.value)); 1666 + 1667 + // Dark mode toggle 1668 + document.getElementById('darkMode').addEventListener('change', (e) => { 1669 + setTheme(e.detail.checked ? 'dark' : 'light'); 1670 + }); 1671 + ``` 1672 + 1673 + ### Custom Theming 1674 + 1675 + ```javascript 1676 + // theme.js - Define your extension's theme 1677 + import { registerTheme } from 'peek://app/components/theme.js'; 1678 + 1679 + // Extend the light theme 1680 + registerTheme('my-brand', { 1681 + 'theme-accent': '#e74c3c', 1682 + 'theme-accent-hover': '#c0392b', 1683 + 'peek-radius-md': '8px', 1684 + 'peek-btn-height-md': '40px' 1685 + }); 1686 + 1687 + // Dark variant 1688 + registerTheme('my-brand-dark', { 1689 + 'theme-accent': '#e74c3c', 1690 + 'theme-bg': '#1e1e1e', 1691 + 'theme-text': '#f0f0f0' 1692 + }, { extends: 'dark' }); 1693 + 1694 + export { 'my-brand', 'my-brand-dark' }; 1695 + ``` 1696 + 1697 + ### Data-Driven Components 1698 + 1699 + ```javascript 1700 + import { signal, effect } from 'peek://app/components/signals.js'; 1701 + import { DataBoundElement } from 'peek://app/components/data-binding.js'; 1702 + 1703 + // Create reactive data 1704 + const items = signal([ 1705 + { id: 1, name: 'Task 1', done: false }, 1706 + { id: 2, name: 'Task 2', done: true } 1707 + ]); 1708 + 1709 + // Update UI automatically when data changes 1710 + effect(() => { 1711 + const list = document.querySelector('peek-list'); 1712 + list.innerHTML = items.value.map(item => ` 1713 + <peek-list-item value="${item.id}" ${item.done ? 'selected' : ''}> 1714 + ${item.name} 1715 + </peek-list-item> 1716 + `).join(''); 1717 + }); 1718 + 1719 + // Add item 1720 + function addItem(name) { 1721 + items.value = [...items.value, { id: Date.now(), name, done: false }]; 1722 + } 1723 + ``` 1724 + 1725 + ### Cross-Component Communication 1726 + 1727 + ```javascript 1728 + import { on, emit, channel } from 'peek://app/components/events.js'; 1729 + 1730 + // Create namespaced channel 1731 + const taskChannel = channel('tasks'); 1732 + 1733 + // Subscribe to events 1734 + taskChannel.on('add', (task) => { 1735 + console.log('Task added:', task); 1736 + }); 1737 + 1738 + taskChannel.on('complete', (taskId) => { 1739 + console.log('Task completed:', taskId); 1740 + }); 1741 + 1742 + // Emit events 1743 + taskChannel.emit('add', { id: 1, name: 'New task' }); 1744 + 1745 + // Wait for event (async) 1746 + const task = await taskChannel.waitFor('add', { timeout: 5000 }); 1747 + ``` 1748 + 1749 + ### Form Validation 1750 + 1751 + ```javascript 1752 + import { validate, Schema } from 'peek://app/components/schema.js'; 1753 + 1754 + const formSchema = Schema.object({ 1755 + email: Schema.string({ format: 'email' }), 1756 + password: Schema.string({ minLength: 8 }), 1757 + age: Schema.integer({ minimum: 18 }) 1758 + }, { required: ['email', 'password'] }); 1759 + 1760 + function handleSubmit(formData) { 1761 + const result = validate(formData, formSchema); 1762 + 1763 + if (!result.valid) { 1764 + result.errors.forEach(err => { 1765 + console.error(`${err.path}: ${err.message}`); 1766 + }); 1767 + return; 1768 + } 1769 + 1770 + // Process valid data 1771 + submitForm(result.data); 1772 + } 1773 + ``` 1774 + 1775 + ### Responsive Layouts 1776 + 1777 + ```html 1778 + <!-- Auto-fit grid --> 1779 + <peek-grid min-item-width="200" gap="16"> 1780 + <peek-card>Card 1</peek-card> 1781 + <peek-card>Card 2</peek-card> 1782 + <peek-card>Card 3</peek-card> 1783 + </peek-grid> 1784 + 1785 + <!-- Fixed columns --> 1786 + <peek-grid columns="2"> 1787 + <peek-grid-item col-span="2">Wide item</peek-grid-item> 1788 + <peek-grid-item>Normal</peek-grid-item> 1789 + <peek-grid-item>Normal</peek-grid-item> 1790 + </peek-grid> 1791 + ``` 1792 + 1793 + ### Accessibility Patterns 1794 + 1795 + ```html 1796 + <!-- Keyboard navigable list with selection --> 1797 + <peek-list selection="single" wrap> 1798 + <peek-list-item value="opt1">Option 1</peek-list-item> 1799 + <peek-list-item value="opt2">Option 2</peek-list-item> 1800 + <peek-list-item value="opt3" disabled>Disabled</peek-list-item> 1801 + </peek-list> 1802 + 1803 + <!-- Accessible tabs --> 1804 + <peek-tabs> 1805 + <peek-tab>General</peek-tab> 1806 + <peek-tab>Advanced</peek-tab> 1807 + <peek-tab-panel>General content</peek-tab-panel> 1808 + <peek-tab-panel>Advanced content</peek-tab-panel> 1809 + </peek-tabs> 1810 + 1811 + <!-- Accessible dialog --> 1812 + <peek-dialog id="confirmDialog" size="sm" close-on-escape> 1813 + <span slot="header">Confirm Action</span> 1814 + <p>Are you sure?</p> 1815 + <div slot="footer"> 1816 + <peek-button variant="ghost" onclick="confirmDialog.close()">Cancel</peek-button> 1817 + <peek-button variant="danger" onclick="doAction()">Confirm</peek-button> 1818 + </div> 1819 + </peek-dialog> 1820 + ``` 1821 + 1822 + ### Component Customization 1823 + 1824 + #### Via CSS Custom Properties 1825 + 1826 + ```css 1827 + /* Global customization */ 1828 + :root { 1829 + --peek-radius-md: 12px; 1830 + --peek-btn-height-md: 42px; 1831 + } 1832 + 1833 + /* Component-specific */ 1834 + peek-button { 1835 + --peek-btn-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 1836 + } 1837 + 1838 + peek-card { 1839 + --peek-card-bg: #f8f9fa; 1840 + --peek-card-border: transparent; 1841 + --peek-card-radius: 16px; 1842 + } 1843 + ``` 1844 + 1845 + #### Via CSS Parts 1846 + 1847 + ```css 1848 + /* Style shadow DOM parts */ 1849 + peek-button::part(button) { 1850 + text-transform: uppercase; 1851 + letter-spacing: 0.5px; 1852 + } 1853 + 1854 + peek-card::part(header) { 1855 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 1856 + color: white; 1857 + } 1858 + 1859 + peek-dialog::part(backdrop) { 1860 + backdrop-filter: blur(4px); 1861 + } 1862 + ``` 1863 + 1864 + #### Via Slots 1865 + 1866 + ```html 1867 + <!-- Replace default content with custom markup --> 1868 + <peek-card> 1869 + <div slot="header"> 1870 + <img src="icon.png" alt=""> 1871 + <h3>Custom Header</h3> 1872 + </div> 1873 + <div slot="media"> 1874 + <video src="preview.mp4" autoplay muted loop></video> 1875 + </div> 1876 + <p>Card body with custom header and video media.</p> 1877 + </peek-card> 1878 + ``` 1879 + 1880 + ### Best Practices 1881 + 1882 + 1. **Use Shadow DOM for content scripts** - Always use `createContainer()` or `initContentScript()` to isolate styles from host pages. 1883 + 1884 + 2. **Prefer native elements** - Components wrap native elements (`<dialog>`, `<details>`, `<select>`) for maximum accessibility. 1885 + 1886 + 3. **Use signals for shared state** - Signals provide efficient reactive updates without framework overhead. 1887 + 1888 + 4. **Theme inheritance** - Extend existing themes rather than defining all tokens from scratch. 1889 + 1890 + 5. **Event composition** - Use `composed: true` for custom events that need to cross shadow boundaries. 1891 + 1892 + 6. **Cleanup resources** - Call `destroy()` on extension contexts when unloading to prevent memory leaks. 1893 + 1894 + 7. **Keyboard navigation** - All interactive components support keyboard navigation out of the box.