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 5 complete - registry, versioning, bundling, dev tools

+1591 -4
+221
app/components/README.md
··· 1892 1892 6. **Cleanup resources** - Call `destroy()` on extension contexts when unloading to prevent memory leaks. 1893 1893 1894 1894 7. **Keyboard navigation** - All interactive components support keyboard navigation out of the box. 1895 + 1896 + --- 1897 + 1898 + ## Component Registry 1899 + 1900 + Dynamic component registration, lazy loading, and dependency management. 1901 + 1902 + ```javascript 1903 + import { 1904 + registry, defineComponent, loadComponent, 1905 + createElement, whenDefined 1906 + } from 'peek://app/components/registry.js'; 1907 + 1908 + // Check registry stats 1909 + console.log(registry.stats()); 1910 + // { version: '1.0.0', total: 22, pending: 22, loading: 0, loaded: 0, error: 0 } 1911 + 1912 + // Load a component on demand 1913 + await loadComponent('peek-dialog'); 1914 + 1915 + // Load multiple components 1916 + await loadComponents(['peek-button', 'peek-card', 'peek-list']); 1917 + 1918 + // Preload components without waiting 1919 + preloadComponents(['peek-drawer', 'peek-tabs']); 1920 + 1921 + // Wait for component to be defined 1922 + const PeekButton = await whenDefined('peek-button'); 1923 + 1924 + // Create element with initial props 1925 + const button = await createElement('peek-button', { 1926 + variant: 'primary', 1927 + loading: true 1928 + }); 1929 + document.body.appendChild(button); 1930 + 1931 + // Register custom component 1932 + defineComponent('my-widget', { 1933 + version: '1.0.0', 1934 + module: () => import('./my-widget.js'), 1935 + dependencies: ['peek-card', 'peek-button'] 1936 + }); 1937 + ``` 1938 + 1939 + ### Registry API 1940 + 1941 + | Function | Description | 1942 + |----------|-------------| 1943 + | `defineComponent(name, def)` | Register a component | 1944 + | `undefineComponent(name)` | Unregister a component | 1945 + | `hasComponent(name)` | Check if registered | 1946 + | `getComponent(name)` | Get component definition | 1947 + | `loadComponent(name)` | Load component and dependencies | 1948 + | `loadComponents(names)` | Load multiple components | 1949 + | `preloadComponents(names)` | Preload without waiting | 1950 + | `whenDefined(name, timeout?)` | Wait for component definition | 1951 + | `createElement(name, props)` | Create and configure element | 1952 + 1953 + --- 1954 + 1955 + ## Version Management 1956 + 1957 + Semantic versioning, compatibility checking, and migrations. 1958 + 1959 + ```javascript 1960 + import { 1961 + version, LIBRARY_VERSION, 1962 + checkCompatibility, satisfies, 1963 + getChangelog, getBreakingChanges, 1964 + registerMigration, migrate 1965 + } from 'peek://app/components/version.js'; 1966 + 1967 + // Current version 1968 + console.log(version.current); // '1.0.0' 1969 + 1970 + // Check compatibility 1971 + if (checkCompatibility('>=1.0.0')) { 1972 + // Safe to use current features 1973 + } 1974 + 1975 + // Version constraint checking 1976 + satisfies('1.2.3', '>=1.0.0'); // true 1977 + satisfies('1.2.3', '^1.0.0'); // true (same major) 1978 + satisfies('1.2.3', '~1.2.0'); // true (same major.minor) 1979 + 1980 + // Get changelog 1981 + const changes = getChangelog('0.9.0', '1.0.0'); 1982 + const breaking = getBreakingChanges('0.9.0'); 1983 + 1984 + // Register migration 1985 + registerMigration('0.9.0', '1.0.0', async (data) => { 1986 + // Transform data for new version 1987 + return { ...data, newField: 'default' }; 1988 + }); 1989 + 1990 + // Run migration 1991 + const migratedData = await migrate(oldData, '0.9.0', '1.0.0'); 1992 + ``` 1993 + 1994 + --- 1995 + 1996 + ## Bundle Configuration 1997 + 1998 + Configuration for building distribution bundles with any ESM bundler. 1999 + 2000 + ```javascript 2001 + import { 2002 + bundleConfig, generateImportMap, 2003 + getEntryPoints, PRESETS 2004 + } from 'peek://app/components/bundle.js'; 2005 + 2006 + // Available presets 2007 + console.log(Object.keys(PRESETS)); 2008 + // ['full', 'core', 'minimal', 'basic', 'forms', 'layout', 'interactive'] 2009 + 2010 + // Get entry points for a preset 2011 + const entries = getEntryPoints('forms'); 2012 + // { base: './base.js', 'peek-button': './peek-button.js', ... } 2013 + 2014 + // Generate import map for native ESM 2015 + const importMap = generateImportMap('/components/', 'full'); 2016 + // { imports: { 'peek://app/components/': '/components/', ... } } 2017 + 2018 + // Get bundler-specific config 2019 + const config = bundleConfig({ 2020 + preset: 'full', 2021 + format: 'esm', 2022 + minify: true, 2023 + outdir: 'dist' 2024 + }); 2025 + 2026 + // Use with esbuild 2027 + // esbuild.build(config.esbuild); 2028 + 2029 + // Use with rollup 2030 + // rollup(config.rollup); 2031 + 2032 + // Use with vite 2033 + // vite.build(config.vite); 2034 + ``` 2035 + 2036 + ### Bundle Presets 2037 + 2038 + | Preset | Description | Size | 2039 + |--------|-------------|------| 2040 + | `full` | Complete library | ~50KB | 2041 + | `core` | Utilities only, no components | ~15KB | 2042 + | `minimal` | Base, signals, events | ~8KB | 2043 + | `basic` | Button, card, list | ~12KB | 2044 + | `forms` | Form components | ~18KB | 2045 + | `layout` | Layout components | ~20KB | 2046 + | `interactive` | Menus and popovers | ~16KB | 2047 + 2048 + --- 2049 + 2050 + ## Development Tools 2051 + 2052 + Debugging and hot-reload utilities for development. 2053 + 2054 + ```javascript 2055 + // Only import in development 2056 + import { devTools, enableDevMode, enableHotReload } from 'peek://app/components/dev.js'; 2057 + 2058 + // Enable dev mode (adds window.__PEEK_DEV__) 2059 + enableDevMode(); 2060 + 2061 + // Connect to hot-reload server 2062 + enableHotReload({ 2063 + url: 'ws://localhost:35729/livereload', 2064 + reconnectDelay: 1000 2065 + }); 2066 + 2067 + // Inspect a component 2068 + devTools.inspect('peek-button'); 2069 + // Logs: definition, load state, properties, attributes, shadow root 2070 + 2071 + // List all components 2072 + devTools.list(); 2073 + // Table: name, state, defined, instances 2074 + 2075 + // Get stats 2076 + devTools.stats(); 2077 + // { version, components, theme, extensions, devMode, hotReload } 2078 + 2079 + // Create scoped logger 2080 + const log = devTools.logger('my-component'); 2081 + log.debug('Only in dev mode'); 2082 + log.time('render'); 2083 + // ... render ... 2084 + log.timeEnd('render'); 2085 + ``` 2086 + 2087 + ### Performance Profiling 2088 + 2089 + ```javascript 2090 + import { ProfilerMixin, startTiming, endTiming } from 'peek://app/components/dev.js'; 2091 + 2092 + // Manual timing 2093 + startTiming('my-operation'); 2094 + // ... operation ... 2095 + const duration = endTiming('my-operation'); 2096 + 2097 + // Mixin for automatic render timing 2098 + class MyComponent extends ProfilerMixin(PeekElement) { 2099 + // Renders are automatically timed 2100 + // Warns if render takes > 16ms (one frame) 2101 + } 2102 + ``` 2103 + 2104 + ### Console Access 2105 + 2106 + When dev mode is enabled, access tools via browser console: 2107 + 2108 + ```javascript 2109 + // In browser console 2110 + __PEEK_DEV__.inspect('peek-card'); 2111 + __PEEK_DEV__.list(); 2112 + __PEEK_DEV__.stats(); 2113 + __PEEK_DEV__.theme.current(); 2114 + __PEEK_DEV__.registry.names(); 2115 + ```
+390
app/components/bundle.js
··· 1 + /** 2 + * Peek Component Bundle Configuration 3 + * 4 + * Defines the component library structure for bundling and distribution. 5 + * Can be used with any ESM-compatible bundler (esbuild, rollup, vite, etc.) 6 + * 7 + * Usage: 8 + * import { bundleConfig, generateImportMap, getEntryPoints } from 'peek://app/components/bundle.js'; 9 + */ 10 + 11 + import { LIBRARY_VERSION } from './version.js'; 12 + 13 + /** 14 + * Component module definitions 15 + */ 16 + export const MODULES = { 17 + // Core utilities 18 + base: { 19 + path: './base.js', 20 + exports: ['PeekElement', 'sharedStyles'], 21 + dependencies: ['lit'] 22 + }, 23 + signals: { 24 + path: './signals.js', 25 + exports: ['signal', 'computed', 'effect', 'batch', 'watch', 'fromExternal'], 26 + dependencies: [] 27 + }, 28 + schema: { 29 + path: './schema.js', 30 + exports: ['validate', 'createValidator', 'assertValid', 'isValid', 'Schema'], 31 + dependencies: [] 32 + }, 33 + 'data-binding': { 34 + path: './data-binding.js', 35 + exports: ['DataBoundElement', 'DataBindingMixin', 'createDataComponent'], 36 + dependencies: ['./base.js', './signals.js', './schema.js'] 37 + }, 38 + events: { 39 + path: './events.js', 40 + exports: ['bus', 'on', 'once', 'emit', 'channel', 'typedEvent', 'waitFor', 'EventBusMixin'], 41 + dependencies: [] 42 + }, 43 + theme: { 44 + path: './theme.js', 45 + exports: ['registerTheme', 'setTheme', 'getTheme', 'getThemeTokens', 'ThemeMixin'], 46 + dependencies: [] 47 + }, 48 + extension: { 49 + path: './extension.js', 50 + exports: ['registerExtension', 'ExtensionContext', 'initContentScript', 'initPopup'], 51 + dependencies: ['./theme.js'] 52 + }, 53 + registry: { 54 + path: './registry.js', 55 + exports: ['registry', 'defineComponent', 'loadComponent', 'createElement'], 56 + dependencies: [] 57 + }, 58 + version: { 59 + path: './version.js', 60 + exports: ['version', 'LIBRARY_VERSION', 'checkCompatibility'], 61 + dependencies: [] 62 + }, 63 + dev: { 64 + path: './dev.js', 65 + exports: ['devTools', 'enableDevMode', 'enableHotReload'], 66 + dependencies: ['./registry.js', './theme.js', './extension.js', './version.js'], 67 + devOnly: true 68 + }, 69 + 70 + // Basic components 71 + 'peek-button': { 72 + path: './peek-button.js', 73 + exports: ['PeekButton'], 74 + dependencies: ['./base.js'], 75 + component: true 76 + }, 77 + 'peek-card': { 78 + path: './peek-card.js', 79 + exports: ['PeekCard'], 80 + dependencies: ['./base.js'], 81 + component: true 82 + }, 83 + 'peek-list': { 84 + path: './peek-list.js', 85 + exports: ['PeekList', 'PeekListItem'], 86 + dependencies: ['./base.js'], 87 + component: true 88 + }, 89 + 90 + // Complex components 91 + 'peek-carousel': { 92 + path: './peek-carousel.js', 93 + exports: ['PeekCarousel'], 94 + dependencies: ['./base.js'], 95 + component: true 96 + }, 97 + 'peek-input': { 98 + path: './peek-input.js', 99 + exports: ['PeekInput'], 100 + dependencies: ['./base.js'], 101 + component: true 102 + }, 103 + 'peek-grid': { 104 + path: './peek-grid.js', 105 + exports: ['PeekGrid', 'PeekGridItem'], 106 + dependencies: ['./base.js'], 107 + component: true 108 + }, 109 + 'peek-dialog': { 110 + path: './peek-dialog.js', 111 + exports: ['PeekDialog'], 112 + dependencies: ['./base.js'], 113 + component: true 114 + }, 115 + 116 + // Native/Open UI components 117 + 'peek-popover': { 118 + path: './peek-popover.js', 119 + exports: ['PeekPopover'], 120 + dependencies: ['./base.js'], 121 + component: true 122 + }, 123 + 'peek-tabs': { 124 + path: './peek-tabs.js', 125 + exports: ['PeekTabs', 'PeekTab', 'PeekTabPanel'], 126 + dependencies: ['./base.js'], 127 + component: true 128 + }, 129 + 'peek-details': { 130 + path: './peek-details.js', 131 + exports: ['PeekDetails'], 132 + dependencies: ['./base.js'], 133 + component: true 134 + }, 135 + 136 + // Phase 4 components 137 + 'peek-select': { 138 + path: './peek-select.js', 139 + exports: ['PeekSelect'], 140 + dependencies: ['./base.js'], 141 + component: true 142 + }, 143 + 'peek-dropdown': { 144 + path: './peek-dropdown.js', 145 + exports: ['PeekDropdown', 'PeekDropdownItem', 'PeekDropdownDivider'], 146 + dependencies: ['./base.js'], 147 + component: true 148 + }, 149 + 'peek-switch': { 150 + path: './peek-switch.js', 151 + exports: ['PeekSwitch'], 152 + dependencies: ['./base.js'], 153 + component: true 154 + }, 155 + 'peek-drawer': { 156 + path: './peek-drawer.js', 157 + exports: ['PeekDrawer'], 158 + dependencies: ['./base.js'], 159 + component: true 160 + }, 161 + 'peek-tooltip': { 162 + path: './peek-tooltip.js', 163 + exports: ['PeekTooltip'], 164 + dependencies: ['./base.js'], 165 + component: true 166 + }, 167 + 'peek-button-group': { 168 + path: './peek-button-group.js', 169 + exports: ['PeekButtonGroup', 'PeekButtonGroupItem'], 170 + dependencies: ['./base.js'], 171 + component: true 172 + } 173 + }; 174 + 175 + /** 176 + * Bundle presets 177 + */ 178 + export const PRESETS = { 179 + // Full bundle with everything 180 + full: { 181 + include: Object.keys(MODULES).filter(m => !MODULES[m].devOnly), 182 + description: 'Complete component library' 183 + }, 184 + 185 + // Core only - utilities without components 186 + core: { 187 + include: ['base', 'signals', 'schema', 'data-binding', 'events', 'theme', 'extension', 'registry', 'version'], 188 + description: 'Core utilities only, no components' 189 + }, 190 + 191 + // Minimal - just base and essential utilities 192 + minimal: { 193 + include: ['base', 'signals', 'events'], 194 + description: 'Minimal utilities for custom components' 195 + }, 196 + 197 + // Basic components 198 + basic: { 199 + include: ['base', 'peek-button', 'peek-card', 'peek-list'], 200 + description: 'Basic components only' 201 + }, 202 + 203 + // Form components 204 + forms: { 205 + include: ['base', 'peek-button', 'peek-input', 'peek-select', 'peek-switch', 'peek-button-group'], 206 + description: 'Form-related components' 207 + }, 208 + 209 + // Layout components 210 + layout: { 211 + include: ['base', 'peek-card', 'peek-grid', 'peek-tabs', 'peek-dialog', 'peek-drawer', 'peek-details'], 212 + description: 'Layout and container components' 213 + }, 214 + 215 + // Interactive components 216 + interactive: { 217 + include: ['base', 'peek-button', 'peek-list', 'peek-dropdown', 'peek-popover', 'peek-tooltip'], 218 + description: 'Interactive and menu components' 219 + } 220 + }; 221 + 222 + /** 223 + * Get entry points for bundler 224 + * @param {string} preset - Preset name or 'full' 225 + * @returns {Object} Entry point configuration 226 + */ 227 + export function getEntryPoints(preset = 'full') { 228 + const modules = PRESETS[preset]?.include || Object.keys(MODULES); 229 + 230 + const entries = {}; 231 + modules.forEach(name => { 232 + const mod = MODULES[name]; 233 + if (mod) { 234 + entries[name] = mod.path; 235 + } 236 + }); 237 + 238 + return entries; 239 + } 240 + 241 + /** 242 + * Get dependency graph 243 + * @param {string[]} modules - Module names 244 + * @returns {Map<string, string[]>} 245 + */ 246 + export function getDependencyGraph(modules = null) { 247 + const names = modules || Object.keys(MODULES); 248 + const graph = new Map(); 249 + 250 + names.forEach(name => { 251 + const mod = MODULES[name]; 252 + if (mod) { 253 + graph.set(name, mod.dependencies.map(d => d.replace('./', '').replace('.js', ''))); 254 + } 255 + }); 256 + 257 + return graph; 258 + } 259 + 260 + /** 261 + * Generate import map for native ES modules 262 + * @param {string} baseUrl - Base URL for components 263 + * @param {string} preset - Preset name 264 + * @returns {Object} Import map 265 + */ 266 + export function generateImportMap(baseUrl = '/components/', preset = 'full') { 267 + const modules = PRESETS[preset]?.include || Object.keys(MODULES); 268 + 269 + const imports = { 270 + 'peek://app/components/': baseUrl 271 + }; 272 + 273 + modules.forEach(name => { 274 + const mod = MODULES[name]; 275 + if (mod) { 276 + imports[`peek://app/components/${name}.js`] = `${baseUrl}${mod.path}`; 277 + } 278 + }); 279 + 280 + // Add index 281 + imports['peek://app/components/index.js'] = `${baseUrl}index.js`; 282 + 283 + return { imports }; 284 + } 285 + 286 + /** 287 + * Generate bundle configuration 288 + * @param {Object} options - Configuration options 289 + * @returns {Object} Bundle config 290 + */ 291 + export function bundleConfig(options = {}) { 292 + const { 293 + preset = 'full', 294 + format = 'esm', 295 + minify = true, 296 + sourcemap = true, 297 + outdir = 'dist', 298 + external = ['lit'] 299 + } = options; 300 + 301 + const entryPoints = getEntryPoints(preset); 302 + 303 + return { 304 + // Common bundler options 305 + entryPoints: Object.values(entryPoints), 306 + format, 307 + minify, 308 + sourcemap, 309 + outdir, 310 + external, 311 + 312 + // Metadata 313 + version: LIBRARY_VERSION, 314 + preset, 315 + modules: Object.keys(entryPoints), 316 + 317 + // ESBuild specific 318 + esbuild: { 319 + entryPoints, 320 + bundle: true, 321 + format, 322 + minify, 323 + sourcemap, 324 + outdir, 325 + external, 326 + target: ['es2022'], 327 + platform: 'browser' 328 + }, 329 + 330 + // Rollup specific 331 + rollup: { 332 + input: entryPoints, 333 + output: { 334 + dir: outdir, 335 + format, 336 + sourcemap, 337 + preserveModules: true 338 + }, 339 + external 340 + }, 341 + 342 + // Vite specific 343 + vite: { 344 + build: { 345 + lib: { 346 + entry: entryPoints, 347 + formats: [format === 'esm' ? 'es' : format] 348 + }, 349 + outDir: outdir, 350 + sourcemap, 351 + minify: minify ? 'esbuild' : false, 352 + rollupOptions: { 353 + external 354 + } 355 + } 356 + } 357 + }; 358 + } 359 + 360 + /** 361 + * Get all component tag names 362 + * @returns {string[]} 363 + */ 364 + export function getComponentNames() { 365 + return Object.entries(MODULES) 366 + .filter(([_, mod]) => mod.component) 367 + .map(([name]) => name); 368 + } 369 + 370 + /** 371 + * Get all utility module names 372 + * @returns {string[]} 373 + */ 374 + export function getUtilityNames() { 375 + return Object.entries(MODULES) 376 + .filter(([_, mod]) => !mod.component) 377 + .map(([name]) => name); 378 + } 379 + 380 + export default { 381 + MODULES, 382 + PRESETS, 383 + getEntryPoints, 384 + getDependencyGraph, 385 + generateImportMap, 386 + bundleConfig, 387 + getComponentNames, 388 + getUtilityNames, 389 + version: LIBRARY_VERSION 390 + };
+371
app/components/dev.js
··· 1 + /** 2 + * Peek Development Utilities 3 + * 4 + * Hot-reload, debugging, and development helpers. 5 + * Only import in development - tree-shake in production. 6 + * 7 + * Usage: 8 + * import { enableHotReload, devTools } from 'peek://app/components/dev.js'; 9 + * 10 + * enableHotReload(); // Connect to dev server 11 + * devTools.inspect('peek-button'); // Inspect component 12 + */ 13 + 14 + import { registry, getComponent, loadState } from './registry.js'; 15 + import { getTheme, getThemeTokens, getThemeNames } from './theme.js'; 16 + import { getExtensionIds, getExtension } from './extension.js'; 17 + import { version } from './version.js'; 18 + 19 + // Dev mode state 20 + let isDevMode = false; 21 + let hotReloadSocket = null; 22 + const componentInstances = new WeakMap(); 23 + 24 + /** 25 + * Enable development mode 26 + */ 27 + export function enableDevMode() { 28 + isDevMode = true; 29 + 30 + // Add global debug object 31 + if (typeof window !== 'undefined') { 32 + window.__PEEK_DEV__ = { 33 + registry, 34 + theme: { 35 + current: getTheme, 36 + tokens: getThemeTokens, 37 + names: getThemeNames 38 + }, 39 + extensions: { 40 + ids: getExtensionIds, 41 + get: getExtension 42 + }, 43 + version, 44 + inspect: inspectComponent, 45 + listComponents: listComponents, 46 + stats: getStats 47 + }; 48 + 49 + console.log( 50 + '%c🔍 Peek Dev Mode Enabled', 51 + 'background: #007aff; color: white; padding: 4px 8px; border-radius: 4px;' 52 + ); 53 + console.log('Access dev tools via window.__PEEK_DEV__'); 54 + } 55 + } 56 + 57 + /** 58 + * Check if dev mode is enabled 59 + */ 60 + export function isDevModeEnabled() { 61 + return isDevMode; 62 + } 63 + 64 + /** 65 + * Enable hot-reload connection 66 + * @param {Object} options - Hot reload options 67 + * @param {string} options.url - WebSocket server URL 68 + * @param {number} options.reconnectDelay - Reconnection delay in ms 69 + */ 70 + export function enableHotReload(options = {}) { 71 + const { 72 + url = 'ws://localhost:35729/livereload', 73 + reconnectDelay = 1000 74 + } = options; 75 + 76 + if (typeof WebSocket === 'undefined') { 77 + console.warn('Hot-reload requires WebSocket support'); 78 + return; 79 + } 80 + 81 + function connect() { 82 + try { 83 + hotReloadSocket = new WebSocket(url); 84 + 85 + hotReloadSocket.onopen = () => { 86 + console.log('%c🔥 Hot-reload connected', 'color: #28a745;'); 87 + }; 88 + 89 + hotReloadSocket.onmessage = (event) => { 90 + try { 91 + const message = JSON.parse(event.data); 92 + handleHotReloadMessage(message); 93 + } catch (e) { 94 + // Ignore non-JSON messages 95 + } 96 + }; 97 + 98 + hotReloadSocket.onclose = () => { 99 + console.log('%c🔥 Hot-reload disconnected, reconnecting...', 'color: #ffc107;'); 100 + setTimeout(connect, reconnectDelay); 101 + }; 102 + 103 + hotReloadSocket.onerror = () => { 104 + hotReloadSocket.close(); 105 + }; 106 + } catch (e) { 107 + console.warn('Failed to connect to hot-reload server:', e); 108 + setTimeout(connect, reconnectDelay); 109 + } 110 + } 111 + 112 + connect(); 113 + enableDevMode(); 114 + } 115 + 116 + /** 117 + * Handle hot-reload messages 118 + */ 119 + function handleHotReloadMessage(message) { 120 + switch (message.command) { 121 + case 'reload': 122 + if (message.path?.endsWith('.css')) { 123 + reloadStyles(message.path); 124 + } else if (message.path?.endsWith('.js')) { 125 + reloadComponent(message.path); 126 + } else { 127 + // Full page reload 128 + location.reload(); 129 + } 130 + break; 131 + 132 + case 'refresh-css': 133 + reloadAllStyles(); 134 + break; 135 + 136 + case 'refresh-component': 137 + if (message.component) { 138 + reloadComponent(message.component); 139 + } 140 + break; 141 + } 142 + } 143 + 144 + /** 145 + * Reload CSS styles 146 + */ 147 + function reloadStyles(path) { 148 + const links = document.querySelectorAll('link[rel="stylesheet"]'); 149 + links.forEach(link => { 150 + if (link.href.includes(path)) { 151 + const url = new URL(link.href); 152 + url.searchParams.set('_reload', Date.now()); 153 + link.href = url.toString(); 154 + } 155 + }); 156 + 157 + console.log(`%c🔄 Reloaded styles: ${path}`, 'color: #17a2b8;'); 158 + } 159 + 160 + /** 161 + * Reload all styles 162 + */ 163 + function reloadAllStyles() { 164 + const links = document.querySelectorAll('link[rel="stylesheet"]'); 165 + links.forEach(link => { 166 + const url = new URL(link.href); 167 + url.searchParams.set('_reload', Date.now()); 168 + link.href = url.toString(); 169 + }); 170 + 171 + // Also reload peek theme styles 172 + const peekStyles = document.querySelectorAll('style[data-peek-theme]'); 173 + peekStyles.forEach(style => { 174 + // Re-inject theme 175 + const theme = style.dataset.peekTheme; 176 + if (theme) { 177 + import('./theme.js').then(({ injectThemeCSS }) => { 178 + style.remove(); 179 + injectThemeCSS(theme, document); 180 + }); 181 + } 182 + }); 183 + 184 + console.log('%c🔄 Reloaded all styles', 'color: #17a2b8;'); 185 + } 186 + 187 + /** 188 + * Reload a component module 189 + * Note: Full module hot-reload requires native import.meta.hot support 190 + */ 191 + async function reloadComponent(pathOrName) { 192 + // Extract component name from path 193 + const name = pathOrName.includes('/') 194 + ? pathOrName.split('/').pop().replace('.js', '') 195 + : pathOrName; 196 + 197 + console.log(`%c🔄 Component changed: ${name}`, 'color: #17a2b8;'); 198 + 199 + // For now, suggest full reload - true HMR requires bundler support 200 + console.log('Full hot-reload for components requires page refresh or bundler HMR support'); 201 + } 202 + 203 + /** 204 + * Inspect a component 205 + * @param {string|HTMLElement} target - Component name or instance 206 + */ 207 + export function inspectComponent(target) { 208 + let element; 209 + let name; 210 + 211 + if (typeof target === 'string') { 212 + name = target; 213 + element = document.querySelector(target); 214 + } else { 215 + element = target; 216 + name = element?.tagName?.toLowerCase(); 217 + } 218 + 219 + const def = getComponent(name); 220 + const state = loadState.get(name); 221 + 222 + const info = { 223 + name, 224 + definition: def, 225 + loadState: state, 226 + element: element || null, 227 + properties: {}, 228 + attributes: {}, 229 + shadowRoot: null 230 + }; 231 + 232 + if (element) { 233 + // Get properties 234 + const proto = Object.getPrototypeOf(element); 235 + const propNames = Object.keys(element.constructor.properties || {}); 236 + propNames.forEach(prop => { 237 + info.properties[prop] = element[prop]; 238 + }); 239 + 240 + // Get attributes 241 + Array.from(element.attributes).forEach(attr => { 242 + info.attributes[attr.name] = attr.value; 243 + }); 244 + 245 + // Shadow root info 246 + if (element.shadowRoot) { 247 + info.shadowRoot = { 248 + mode: element.shadowRoot.mode, 249 + childCount: element.shadowRoot.childElementCount, 250 + styles: element.shadowRoot.querySelectorAll('style').length 251 + }; 252 + } 253 + } 254 + 255 + console.group(`🔍 Component: ${name}`); 256 + console.log('Definition:', info.definition); 257 + console.log('Load state:', info.loadState); 258 + console.log('Properties:', info.properties); 259 + console.log('Attributes:', info.attributes); 260 + console.log('Shadow root:', info.shadowRoot); 261 + if (element) console.log('Element:', element); 262 + console.groupEnd(); 263 + 264 + return info; 265 + } 266 + 267 + /** 268 + * List all components and their states 269 + */ 270 + export function listComponents() { 271 + const names = registry.names(); 272 + const components = names.map(name => ({ 273 + name, 274 + state: loadState.get(name), 275 + defined: !!customElements.get(name), 276 + instances: document.querySelectorAll(name).length 277 + })); 278 + 279 + console.table(components); 280 + return components; 281 + } 282 + 283 + /** 284 + * Get development statistics 285 + */ 286 + export function getStats() { 287 + return { 288 + version: version.current, 289 + components: registry.stats(), 290 + theme: getTheme(), 291 + extensions: getExtensionIds().length, 292 + devMode: isDevMode, 293 + hotReload: !!hotReloadSocket 294 + }; 295 + } 296 + 297 + /** 298 + * Performance timing for component rendering 299 + */ 300 + const renderTimings = new Map(); 301 + 302 + /** 303 + * Start timing a component render 304 + */ 305 + export function startTiming(componentName) { 306 + renderTimings.set(componentName, performance.now()); 307 + } 308 + 309 + /** 310 + * End timing and log result 311 + */ 312 + export function endTiming(componentName) { 313 + const start = renderTimings.get(componentName); 314 + if (start) { 315 + const duration = performance.now() - start; 316 + renderTimings.delete(componentName); 317 + 318 + if (duration > 16) { // Longer than one frame 319 + console.warn(`⚠️ Slow render: ${componentName} took ${duration.toFixed(2)}ms`); 320 + } 321 + 322 + return duration; 323 + } 324 + return 0; 325 + } 326 + 327 + /** 328 + * Component render profiler mixin 329 + */ 330 + export function ProfilerMixin(Base) { 331 + return class extends Base { 332 + performUpdate() { 333 + startTiming(this.tagName.toLowerCase()); 334 + super.performUpdate(); 335 + endTiming(this.tagName.toLowerCase()); 336 + } 337 + }; 338 + } 339 + 340 + /** 341 + * Debug logger with component context 342 + */ 343 + export function createLogger(componentName) { 344 + const prefix = `[${componentName}]`; 345 + return { 346 + log: (...args) => console.log(prefix, ...args), 347 + warn: (...args) => console.warn(prefix, ...args), 348 + error: (...args) => console.error(prefix, ...args), 349 + debug: (...args) => isDevMode && console.debug(prefix, ...args), 350 + group: (label) => console.group(`${prefix} ${label}`), 351 + groupEnd: () => console.groupEnd(), 352 + time: (label) => console.time(`${prefix} ${label}`), 353 + timeEnd: (label) => console.timeEnd(`${prefix} ${label}`) 354 + }; 355 + } 356 + 357 + /** 358 + * Dev tools object for console access 359 + */ 360 + export const devTools = { 361 + enable: enableDevMode, 362 + hotReload: enableHotReload, 363 + inspect: inspectComponent, 364 + list: listComponents, 365 + stats: getStats, 366 + timing: { start: startTiming, end: endTiming }, 367 + logger: createLogger, 368 + ProfilerMixin 369 + }; 370 + 371 + export default devTools;
+24
app/components/index.js
··· 55 55 initContentScript, initPopup 56 56 } from './extension.js'; 57 57 58 + // Component registry 59 + export { 60 + registry, defineComponent, undefineComponent, 61 + hasComponent, getComponent, getComponentNames, 62 + loadComponent, loadComponents, preloadComponents, 63 + whenDefined, createElement 64 + } from './registry.js'; 65 + 66 + // Version management 67 + export { 68 + version, LIBRARY_VERSION, 69 + parseVersion, compareVersions, satisfies, 70 + checkCompatibility, isDeprecated, 71 + getChangelog, getBreakingChanges, 72 + registerMigration, migrate, requireVersion 73 + } from './version.js'; 74 + 75 + // Bundle configuration (for build tools) 76 + export { 77 + MODULES, PRESETS, 78 + getEntryPoints, getDependencyGraph, 79 + generateImportMap, bundleConfig 80 + } from './bundle.js'; 81 + 58 82 // Components - Basic 59 83 export { PeekButton } from './peek-button.js'; 60 84 export { PeekCard } from './peek-card.js';
+305
app/components/registry.js
··· 1 + /** 2 + * Peek Component Registry 3 + * 4 + * Dynamic component registration, discovery, and lazy loading. 5 + * 6 + * Usage: 7 + * import { registry, defineComponent, loadComponent } from 'peek://app/components/registry.js'; 8 + * 9 + * // Register a component 10 + * defineComponent('peek-custom', { 11 + * version: '1.0.0', 12 + * module: () => import('./peek-custom.js'), 13 + * dependencies: ['peek-button'] 14 + * }); 15 + * 16 + * // Load component on demand 17 + * await loadComponent('peek-custom'); 18 + * 19 + * // Check if registered 20 + * registry.has('peek-custom'); 21 + */ 22 + 23 + // Component metadata registry 24 + const componentRegistry = new Map(); 25 + 26 + // Load state tracking 27 + const loadState = new Map(); // 'pending' | 'loading' | 'loaded' | 'error' 28 + const loadPromises = new Map(); 29 + 30 + // Version info 31 + const REGISTRY_VERSION = '1.0.0'; 32 + 33 + /** 34 + * Component definition interface 35 + * @typedef {Object} ComponentDefinition 36 + * @property {string} version - Semantic version 37 + * @property {Function} module - Dynamic import function 38 + * @property {string[]} [dependencies] - Required component names 39 + * @property {string} [description] - Component description 40 + * @property {Object} [metadata] - Additional metadata 41 + */ 42 + 43 + /** 44 + * Define/register a component 45 + * @param {string} name - Component tag name (e.g., 'peek-button') 46 + * @param {ComponentDefinition} definition - Component definition 47 + */ 48 + export function defineComponent(name, definition) { 49 + if (!name || typeof name !== 'string') { 50 + throw new Error('Component name must be a non-empty string'); 51 + } 52 + 53 + if (!definition.module || typeof definition.module !== 'function') { 54 + throw new Error('Component definition must include a module loader function'); 55 + } 56 + 57 + const def = { 58 + name, 59 + version: definition.version || '0.0.0', 60 + module: definition.module, 61 + dependencies: definition.dependencies || [], 62 + description: definition.description || '', 63 + metadata: definition.metadata || {}, 64 + registeredAt: Date.now() 65 + }; 66 + 67 + componentRegistry.set(name, def); 68 + loadState.set(name, 'pending'); 69 + 70 + return def; 71 + } 72 + 73 + /** 74 + * Unregister a component 75 + * @param {string} name - Component name 76 + */ 77 + export function undefineComponent(name) { 78 + componentRegistry.delete(name); 79 + loadState.delete(name); 80 + loadPromises.delete(name); 81 + } 82 + 83 + /** 84 + * Check if a component is registered 85 + * @param {string} name - Component name 86 + * @returns {boolean} 87 + */ 88 + export function hasComponent(name) { 89 + return componentRegistry.has(name); 90 + } 91 + 92 + /** 93 + * Get component definition 94 + * @param {string} name - Component name 95 + * @returns {ComponentDefinition|undefined} 96 + */ 97 + export function getComponent(name) { 98 + return componentRegistry.get(name); 99 + } 100 + 101 + /** 102 + * Get all registered component names 103 + * @returns {string[]} 104 + */ 105 + export function getComponentNames() { 106 + return Array.from(componentRegistry.keys()); 107 + } 108 + 109 + /** 110 + * Get component load state 111 + * @param {string} name - Component name 112 + * @returns {'pending'|'loading'|'loaded'|'error'|undefined} 113 + */ 114 + export function getLoadState(name) { 115 + return loadState.get(name); 116 + } 117 + 118 + /** 119 + * Load a component and its dependencies 120 + * @param {string} name - Component name 121 + * @returns {Promise<void>} 122 + */ 123 + export async function loadComponent(name) { 124 + const state = loadState.get(name); 125 + 126 + // Already loaded 127 + if (state === 'loaded') { 128 + return; 129 + } 130 + 131 + // Currently loading - return existing promise 132 + if (state === 'loading') { 133 + return loadPromises.get(name); 134 + } 135 + 136 + // Check if registered 137 + const def = componentRegistry.get(name); 138 + if (!def) { 139 + // Try to load from custom elements registry 140 + if (customElements.get(name)) { 141 + loadState.set(name, 'loaded'); 142 + return; 143 + } 144 + throw new Error(`Component '${name}' is not registered`); 145 + } 146 + 147 + // Start loading 148 + loadState.set(name, 'loading'); 149 + 150 + const loadPromise = (async () => { 151 + try { 152 + // Load dependencies first 153 + if (def.dependencies.length > 0) { 154 + await Promise.all(def.dependencies.map(dep => loadComponent(dep))); 155 + } 156 + 157 + // Load the component module 158 + await def.module(); 159 + 160 + // Verify it's registered with custom elements 161 + await customElements.whenDefined(name); 162 + 163 + loadState.set(name, 'loaded'); 164 + } catch (error) { 165 + loadState.set(name, 'error'); 166 + throw error; 167 + } 168 + })(); 169 + 170 + loadPromises.set(name, loadPromise); 171 + return loadPromise; 172 + } 173 + 174 + /** 175 + * Load multiple components 176 + * @param {string[]} names - Component names 177 + * @returns {Promise<void>} 178 + */ 179 + export async function loadComponents(names) { 180 + await Promise.all(names.map(name => loadComponent(name))); 181 + } 182 + 183 + /** 184 + * Preload components without waiting 185 + * @param {string[]} names - Component names 186 + */ 187 + export function preloadComponents(names) { 188 + names.forEach(name => { 189 + loadComponent(name).catch(() => { 190 + // Silently fail preloads 191 + }); 192 + }); 193 + } 194 + 195 + /** 196 + * Wait for a component to be defined 197 + * @param {string} name - Component name 198 + * @param {number} timeout - Timeout in ms (default: 10000) 199 + * @returns {Promise<CustomElementConstructor>} 200 + */ 201 + export async function whenDefined(name, timeout = 10000) { 202 + const timeoutPromise = new Promise((_, reject) => { 203 + setTimeout(() => reject(new Error(`Timeout waiting for '${name}'`)), timeout); 204 + }); 205 + 206 + return Promise.race([ 207 + customElements.whenDefined(name), 208 + timeoutPromise 209 + ]); 210 + } 211 + 212 + /** 213 + * Create a component instance 214 + * @param {string} name - Component name 215 + * @param {Object} props - Initial properties 216 + * @returns {Promise<HTMLElement>} 217 + */ 218 + export async function createElement(name, props = {}) { 219 + await loadComponent(name); 220 + 221 + const element = document.createElement(name); 222 + 223 + Object.entries(props).forEach(([key, value]) => { 224 + if (key in element) { 225 + element[key] = value; 226 + } else { 227 + element.setAttribute(key, value); 228 + } 229 + }); 230 + 231 + return element; 232 + } 233 + 234 + /** 235 + * Registry statistics 236 + * @returns {Object} 237 + */ 238 + export function getStats() { 239 + const stats = { 240 + version: REGISTRY_VERSION, 241 + total: componentRegistry.size, 242 + pending: 0, 243 + loading: 0, 244 + loaded: 0, 245 + error: 0 246 + }; 247 + 248 + loadState.forEach(state => { 249 + stats[state]++; 250 + }); 251 + 252 + return stats; 253 + } 254 + 255 + /** 256 + * Export registry for inspection 257 + */ 258 + export const registry = { 259 + define: defineComponent, 260 + undefine: undefineComponent, 261 + has: hasComponent, 262 + get: getComponent, 263 + names: getComponentNames, 264 + load: loadComponent, 265 + loadAll: loadComponents, 266 + preload: preloadComponents, 267 + whenDefined, 268 + createElement, 269 + getState: getLoadState, 270 + stats: getStats, 271 + version: REGISTRY_VERSION 272 + }; 273 + 274 + // Register built-in components 275 + const BUILTIN_COMPONENTS = [ 276 + { name: 'peek-button', module: () => import('./peek-button.js') }, 277 + { name: 'peek-card', module: () => import('./peek-card.js') }, 278 + { name: 'peek-list', module: () => import('./peek-list.js'), exports: ['PeekList', 'PeekListItem'] }, 279 + { name: 'peek-list-item', module: () => import('./peek-list.js') }, 280 + { name: 'peek-carousel', module: () => import('./peek-carousel.js') }, 281 + { name: 'peek-input', module: () => import('./peek-input.js') }, 282 + { name: 'peek-grid', module: () => import('./peek-grid.js'), exports: ['PeekGrid', 'PeekGridItem'] }, 283 + { name: 'peek-grid-item', module: () => import('./peek-grid.js') }, 284 + { name: 'peek-dialog', module: () => import('./peek-dialog.js') }, 285 + { name: 'peek-popover', module: () => import('./peek-popover.js') }, 286 + { name: 'peek-tabs', module: () => import('./peek-tabs.js'), exports: ['PeekTabs', 'PeekTab', 'PeekTabPanel'] }, 287 + { name: 'peek-tab', module: () => import('./peek-tabs.js') }, 288 + { name: 'peek-tab-panel', module: () => import('./peek-tabs.js') }, 289 + { name: 'peek-details', module: () => import('./peek-details.js') }, 290 + { name: 'peek-select', module: () => import('./peek-select.js') }, 291 + { name: 'peek-dropdown', module: () => import('./peek-dropdown.js'), exports: ['PeekDropdown', 'PeekDropdownItem', 'PeekDropdownDivider'] }, 292 + { name: 'peek-dropdown-item', module: () => import('./peek-dropdown.js') }, 293 + { name: 'peek-dropdown-divider', module: () => import('./peek-dropdown.js') }, 294 + { name: 'peek-switch', module: () => import('./peek-switch.js') }, 295 + { name: 'peek-drawer', module: () => import('./peek-drawer.js') }, 296 + { name: 'peek-tooltip', module: () => import('./peek-tooltip.js') }, 297 + { name: 'peek-button-group', module: () => import('./peek-button-group.js'), exports: ['PeekButtonGroup', 'PeekButtonGroupItem'] }, 298 + { name: 'peek-button-group-item', module: () => import('./peek-button-group.js') } 299 + ]; 300 + 301 + BUILTIN_COMPONENTS.forEach(({ name, module }) => { 302 + defineComponent(name, { module, version: '1.0.0' }); 303 + }); 304 + 305 + export default registry;
+276
app/components/version.js
··· 1 + /** 2 + * Peek Component Version Management 3 + * 4 + * Semantic versioning, compatibility checking, and migration support. 5 + * 6 + * Usage: 7 + * import { version, checkCompatibility, migrate } from 'peek://app/components/version.js'; 8 + * 9 + * console.log(version.current); // '1.0.0' 10 + * checkCompatibility('>=0.9.0'); // true 11 + */ 12 + 13 + // Current library version 14 + export const LIBRARY_VERSION = '1.0.0'; 15 + 16 + // Minimum supported version for migrations 17 + export const MIN_SUPPORTED_VERSION = '0.1.0'; 18 + 19 + // Version history for changelog/migrations 20 + const VERSION_HISTORY = [ 21 + { 22 + version: '1.0.0', 23 + date: '2026-01-29', 24 + changes: [ 25 + 'Initial stable release', 26 + 'Phase 1-4 components complete', 27 + 'Theme system with light/dark built-in', 28 + 'Extension system for content scripts', 29 + 'Component registry with lazy loading' 30 + ], 31 + breaking: [] 32 + } 33 + ]; 34 + 35 + /** 36 + * Parse semantic version string 37 + * @param {string} version - Version string (e.g., '1.2.3') 38 + * @returns {{ major: number, minor: number, patch: number, prerelease?: string }} 39 + */ 40 + export function parseVersion(version) { 41 + const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/); 42 + if (!match) { 43 + throw new Error(`Invalid version format: ${version}`); 44 + } 45 + 46 + return { 47 + major: parseInt(match[1], 10), 48 + minor: parseInt(match[2], 10), 49 + patch: parseInt(match[3], 10), 50 + prerelease: match[4] || null 51 + }; 52 + } 53 + 54 + /** 55 + * Compare two versions 56 + * @param {string} a - First version 57 + * @param {string} b - Second version 58 + * @returns {number} -1 if a < b, 0 if a == b, 1 if a > b 59 + */ 60 + export function compareVersions(a, b) { 61 + const va = parseVersion(a); 62 + const vb = parseVersion(b); 63 + 64 + if (va.major !== vb.major) return va.major - vb.major; 65 + if (va.minor !== vb.minor) return va.minor - vb.minor; 66 + if (va.patch !== vb.patch) return va.patch - vb.patch; 67 + 68 + // Prerelease versions are lower than release 69 + if (va.prerelease && !vb.prerelease) return -1; 70 + if (!va.prerelease && vb.prerelease) return 1; 71 + if (va.prerelease && vb.prerelease) { 72 + return va.prerelease.localeCompare(vb.prerelease); 73 + } 74 + 75 + return 0; 76 + } 77 + 78 + /** 79 + * Check if version satisfies a constraint 80 + * @param {string} version - Version to check 81 + * @param {string} constraint - Constraint (e.g., '>=1.0.0', '^1.2.0', '~1.2.3') 82 + * @returns {boolean} 83 + */ 84 + export function satisfies(version, constraint) { 85 + const v = parseVersion(version); 86 + 87 + // Exact match 88 + if (!constraint.match(/^[<>=^~]/)) { 89 + return compareVersions(version, constraint) === 0; 90 + } 91 + 92 + // Range operators 93 + if (constraint.startsWith('>=')) { 94 + return compareVersions(version, constraint.slice(2)) >= 0; 95 + } 96 + if (constraint.startsWith('<=')) { 97 + return compareVersions(version, constraint.slice(2)) <= 0; 98 + } 99 + if (constraint.startsWith('>')) { 100 + return compareVersions(version, constraint.slice(1)) > 0; 101 + } 102 + if (constraint.startsWith('<')) { 103 + return compareVersions(version, constraint.slice(1)) < 0; 104 + } 105 + 106 + // Caret (^) - compatible with version (same major) 107 + if (constraint.startsWith('^')) { 108 + const c = parseVersion(constraint.slice(1)); 109 + return v.major === c.major && compareVersions(version, constraint.slice(1)) >= 0; 110 + } 111 + 112 + // Tilde (~) - approximately equivalent (same major.minor) 113 + if (constraint.startsWith('~')) { 114 + const c = parseVersion(constraint.slice(1)); 115 + return v.major === c.major && v.minor === c.minor && v.patch >= c.patch; 116 + } 117 + 118 + return false; 119 + } 120 + 121 + /** 122 + * Check compatibility with current library version 123 + * @param {string} constraint - Version constraint 124 + * @returns {boolean} 125 + */ 126 + export function checkCompatibility(constraint) { 127 + return satisfies(LIBRARY_VERSION, constraint); 128 + } 129 + 130 + /** 131 + * Check if a version is deprecated 132 + * @param {string} version - Version to check 133 + * @returns {boolean} 134 + */ 135 + export function isDeprecated(version) { 136 + return compareVersions(version, MIN_SUPPORTED_VERSION) < 0; 137 + } 138 + 139 + /** 140 + * Get changelog for a version range 141 + * @param {string} fromVersion - Starting version 142 + * @param {string} toVersion - Ending version (default: current) 143 + * @returns {Array} 144 + */ 145 + export function getChangelog(fromVersion, toVersion = LIBRARY_VERSION) { 146 + return VERSION_HISTORY.filter(entry => { 147 + const cmp = compareVersions(entry.version, fromVersion); 148 + const cmpTo = compareVersions(entry.version, toVersion); 149 + return cmp > 0 && cmpTo <= 0; 150 + }); 151 + } 152 + 153 + /** 154 + * Get breaking changes for a version range 155 + * @param {string} fromVersion - Starting version 156 + * @param {string} toVersion - Ending version (default: current) 157 + * @returns {Array} 158 + */ 159 + export function getBreakingChanges(fromVersion, toVersion = LIBRARY_VERSION) { 160 + const changelog = getChangelog(fromVersion, toVersion); 161 + return changelog.flatMap(entry => 162 + entry.breaking.map(change => ({ 163 + version: entry.version, 164 + change 165 + })) 166 + ); 167 + } 168 + 169 + /** 170 + * Migration registry 171 + */ 172 + const migrations = new Map(); 173 + 174 + /** 175 + * Register a migration 176 + * @param {string} fromVersion - Source version 177 + * @param {string} toVersion - Target version 178 + * @param {Function} migrateFn - Migration function 179 + */ 180 + export function registerMigration(fromVersion, toVersion, migrateFn) { 181 + const key = `${fromVersion}->${toVersion}`; 182 + migrations.set(key, { 183 + from: fromVersion, 184 + to: toVersion, 185 + migrate: migrateFn 186 + }); 187 + } 188 + 189 + /** 190 + * Find migration path between versions 191 + * @param {string} fromVersion - Source version 192 + * @param {string} toVersion - Target version 193 + * @returns {Array} Array of migration steps 194 + */ 195 + export function findMigrationPath(fromVersion, toVersion) { 196 + // Simple direct path lookup for now 197 + // Could be extended to find multi-step paths 198 + const key = `${fromVersion}->${toVersion}`; 199 + const migration = migrations.get(key); 200 + return migration ? [migration] : []; 201 + } 202 + 203 + /** 204 + * Run migrations 205 + * @param {Object} data - Data to migrate 206 + * @param {string} fromVersion - Source version 207 + * @param {string} toVersion - Target version (default: current) 208 + * @returns {Object} Migrated data 209 + */ 210 + export async function migrate(data, fromVersion, toVersion = LIBRARY_VERSION) { 211 + const path = findMigrationPath(fromVersion, toVersion); 212 + 213 + if (path.length === 0 && fromVersion !== toVersion) { 214 + console.warn(`No migration path from ${fromVersion} to ${toVersion}`); 215 + return data; 216 + } 217 + 218 + let result = data; 219 + for (const step of path) { 220 + result = await step.migrate(result); 221 + } 222 + 223 + return result; 224 + } 225 + 226 + /** 227 + * Version object for easy access 228 + */ 229 + export const version = { 230 + current: LIBRARY_VERSION, 231 + min: MIN_SUPPORTED_VERSION, 232 + parse: parseVersion, 233 + compare: compareVersions, 234 + satisfies, 235 + checkCompatibility, 236 + isDeprecated, 237 + getChangelog, 238 + getBreakingChanges, 239 + migrate, 240 + registerMigration, 241 + history: VERSION_HISTORY 242 + }; 243 + 244 + /** 245 + * Runtime version check - warns if incompatible 246 + * @param {string} requiredVersion - Required version constraint 247 + * @param {string} context - Context for warning message 248 + */ 249 + export function requireVersion(requiredVersion, context = '') { 250 + if (!checkCompatibility(requiredVersion)) { 251 + const msg = `Version mismatch${context ? ` in ${context}` : ''}: requires ${requiredVersion}, current is ${LIBRARY_VERSION}`; 252 + console.warn(msg); 253 + return false; 254 + } 255 + return true; 256 + } 257 + 258 + /** 259 + * Decorator for version-gated features 260 + * @param {string} minVersion - Minimum version required 261 + * @returns {Function} Decorator 262 + */ 263 + export function sinceVersion(minVersion) { 264 + return function(target, propertyKey, descriptor) { 265 + const original = descriptor.value; 266 + descriptor.value = function(...args) { 267 + if (!satisfies(LIBRARY_VERSION, `>=${minVersion}`)) { 268 + throw new Error(`${propertyKey} requires version >=${minVersion}, current is ${LIBRARY_VERSION}`); 269 + } 270 + return original.apply(this, args); 271 + }; 272 + return descriptor; 273 + }; 274 + } 275 + 276 + export default version;
+4 -4
notes/research-ui-componentry.md
··· 627 627 - Usage examples 628 628 629 629 **Phase 5: Extension Distribution** 630 - 1. Module Federation or shared `.js` bundle 631 - 2. Component registry API 632 - 3. Hot-reload for development 633 - 4. Version management for stability 630 + 1. Bundle configuration for ESM distribution (esbuild, rollup, vite compatible) 631 + 2. Component registry API with lazy loading and dependency tracking 632 + 3. Development utilities with hot-reload support 633 + 4. Version management with semantic versioning and migration support 634 634 635 635 --- 636 636