diff --git a/storybook/src/mocks/auto-loader.ts b/storybook/src/mocks/auto-loader.ts new file mode 100644 index 000000000..15a60ea2b --- /dev/null +++ b/storybook/src/mocks/auto-loader.ts @@ -0,0 +1,234 @@ +/** + * Package Auto-Loader + * + * Loads component trees and metadata directly from the actual packages. + * No manual mocking needed - reads from packages/{id}/seed/ + */ + +import type { LuaUIComponent, LuaPackageMetadata, LuaRenderContext } from '../types/lua-types' +import type { MockLuaResult, MockPackageDefinition } from './lua-engine' +import { registerMockPackage } from './lua-engine' +import type { TemplateVariables } from './schema' +import { processTemplates } from './schema' + +/** + * Component definition from components.json + */ +interface PackageComponent { + id: string + type: string + props?: Record + children?: PackageComponent[] +} + +/** + * Convert a PackageComponent to LuaUIComponent + */ +function packageComponentToLua(component: PackageComponent): LuaUIComponent { + return { + type: component.type, + props: component.props, + children: component.children?.map(packageComponentToLua), + } +} + +/** + * Load package metadata from the actual package + */ +export async function loadPackageMetadata(packageId: string): Promise { + try { + const response = await fetch(`/packages/${packageId}/seed/metadata.json`) + if (!response.ok) return null + return await response.json() + } catch { + return null + } +} + +/** + * Load components.json from a package + */ +export async function loadPackageComponents(packageId: string): Promise { + try { + const response = await fetch(`/packages/${packageId}/seed/components.json`) + if (!response.ok) return [] + const components = await response.json() + return Array.isArray(components) ? components : [] + } catch { + return [] + } +} + +/** + * Load script manifest from a package + */ +export async function loadPackageScripts(packageId: string): Promise> { + try { + const response = await fetch(`/packages/${packageId}/seed/scripts/manifest.json`) + if (!response.ok) return [] + const manifest = await response.json() + return manifest.scripts || [] + } catch { + return [] + } +} + +/** + * Create template variables from context + */ +function createTemplateVars(ctx: LuaRenderContext): TemplateVariables { + return { + 'user.username': ctx.user?.username ?? 'Guest', + 'user.email': ctx.user?.email ?? 'guest@example.com', + 'user.level': ctx.user?.level ?? 1, + 'user.id': ctx.user?.id ?? 'guest', + 'tenant.name': ctx.tenant?.name ?? 'Default', + 'tenant.id': ctx.tenant?.id ?? 'default', + 'nerdMode': ctx.nerdMode ?? false, + 'theme': ctx.theme ?? 'light', + } +} + +/** + * Auto-generate renders from components.json + * Each component becomes a render with its id as the key + */ +function componentsToRenders( + components: PackageComponent[] +): Record LuaUIComponent> { + const renders: Record LuaUIComponent> = {} + + for (const component of components) { + const luaComponent = packageComponentToLua(component) + + // Register by component id (e.g., "stat_card", "dashboard_grid") + renders[component.id] = (ctx: LuaRenderContext) => { + const vars = createTemplateVars(ctx) + return processTemplates(luaComponent, vars) + } + + // Also register with .json suffix for discovery + renders[`${component.id}.json`] = renders[component.id] + } + + // Add a special "all_components" render that shows all components + if (components.length > 0) { + renders['all_components'] = (ctx: LuaRenderContext) => { + const vars = createTemplateVars(ctx) + return processTemplates({ + type: 'Stack', + props: { spacing: 4 }, + children: components.map(c => ({ + type: 'Box', + children: [ + { + type: 'Typography', + props: { variant: 'overline', text: c.id }, + }, + packageComponentToLua(c), + ], + })), + }, vars) + } + } + + return renders +} + +/** + * Auto-load a package from the workspace + * Reads metadata.json and components.json directly + */ +export async function autoLoadPackage(packageId: string): Promise { + const [metadata, components] = await Promise.all([ + loadPackageMetadata(packageId), + loadPackageComponents(packageId), + ]) + + if (!metadata) { + console.warn(`[AutoLoader] No metadata found for ${packageId}`) + return null + } + + // Generate renders from components + const renders = componentsToRenders(components) + + // Add an info render for packages with no components + if (Object.keys(renders).length === 0) { + renders['info'] = () => ({ + type: 'Alert', + props: { severity: 'info' }, + children: [ + { + type: 'Typography', + props: { + variant: 'body2', + text: `Package "${metadata.name}" has no components.json definitions. Scripts: ${metadata.exports?.scripts?.join(', ') || 'none'}`, + }, + }, + ], + }) + } + + return { + metadata, + renders, + } +} + +/** + * Auto-load and register a package + */ +export async function autoRegisterPackage(packageId: string): Promise { + const pkg = await autoLoadPackage(packageId) + if (pkg) { + registerMockPackage(pkg) + return true + } + return false +} + +/** + * Auto-load multiple packages + */ +export async function autoRegisterPackages(packageIds: string[]): Promise { + const registered: string[] = [] + + await Promise.all( + packageIds.map(async (id) => { + const success = await autoRegisterPackage(id) + if (success) registered.push(id) + }) + ) + + return registered +} + +/** + * Execute a render from an auto-loaded package + */ +export function executeAutoRender( + packageId: string, + componentId: string, + context: LuaRenderContext, + components: PackageComponent[] +): MockLuaResult { + const component = components.find(c => c.id === componentId) + + if (!component) { + return { + success: false, + error: `Component "${componentId}" not found in ${packageId}`, + logs: [`Available: ${components.map(c => c.id).join(', ')}`], + } + } + + const luaComponent = packageComponentToLua(component) + const vars = createTemplateVars(context) + + return { + success: true, + result: processTemplates(luaComponent, vars), + logs: [], + } +} diff --git a/storybook/src/mocks/packages/index.ts b/storybook/src/mocks/packages/index.ts index 4896b77ac..1e809c175 100644 --- a/storybook/src/mocks/packages/index.ts +++ b/storybook/src/mocks/packages/index.ts @@ -1,20 +1,44 @@ /** * Package mocks index * - * JSON-driven mocks are now preferred! - * Add new mocks as JSON files in ../data/*.json - * - * Legacy TypeScript mocks are still imported for backwards compatibility. + * Three sources of mock data (in priority order): + * 1. Auto-loaded from packages/{id}/seed/components.json (real data!) + * 2. JSON files in ../data/*.json (manual overrides) + * 3. Legacy TypeScript mocks (deprecated) */ -// Register JSON-based mocks (preferred) +// Register JSON-based mocks first (these can override auto-loaded) import { registerJsonMocks } from '../json-loader' -registerJsonMocks() -// Legacy TypeScript mocks (kept for reference, but JSON takes precedence) -// import './dashboard' -// import './ui-level4' -// import './user-manager' +// Auto-load packages that have components.json +import { autoRegisterPackages } from '../auto-loader' + +// Packages known to have good components.json files +const AUTO_LOAD_PACKAGES = [ + 'dashboard', + 'ui_header', + 'ui_footer', + 'nav_menu', + 'data_table', + 'stats_grid', + 'form_builder', + 'notification_center', +] + +// Initialize on module load +let initialized = false +export async function initializeMocks(): Promise { + if (initialized) return + initialized = true + + // Auto-load from real packages first + const autoLoaded = await autoRegisterPackages(AUTO_LOAD_PACKAGES) + console.log(`[Mocks] Auto-loaded ${autoLoaded.length} packages from workspace:`, autoLoaded) + + // Then register JSON overrides (these take precedence) + registerJsonMocks() + console.log('[Mocks] Registered JSON mock overrides') +} // Re-export utilities export { @@ -26,3 +50,6 @@ export { // Export JSON utilities export { getRenderDescriptions, executeJsonMock } from '../json-loader' + +// Export auto-loader utilities +export { autoLoadPackage, loadPackageComponents, loadPackageMetadata } from '../auto-loader' diff --git a/storybook/src/types/lua-types.ts b/storybook/src/types/lua-types.ts index ffa435b1d..898a940d3 100644 --- a/storybook/src/types/lua-types.ts +++ b/storybook/src/types/lua-types.ts @@ -47,6 +47,10 @@ export interface LuaRenderContext { level?: number email?: string } + tenant?: { + id?: string + name?: string + } nerdMode?: boolean theme?: 'light' | 'dark' [key: string]: unknown