From 8fc4991ae91f31a1085efee7a8fb537f652ecca1 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Tue, 30 Dec 2025 13:00:42 +0000 Subject: [PATCH] docs: nextjs,frontends,ui (11 files) --- fakemui/fakemui/atoms/Heading.tsx | 30 ++++ fakemui/fakemui/inputs/Button.tsx | 127 +++++++++++++++-- .../nextjs/src/components/atoms/README.md | 2 +- .../components/atoms/display/Icon.test.tsx | 6 +- .../src/components/atoms/display/Icon.tsx | 44 +++--- frontends/nextjs/src/components/ui/README.md | 6 +- frontends/nextjs/src/lib/rendering/index.ts | 10 ++ packages/ui_level5/seed/metadata.json | 6 +- .../seed/scripts/tests/transfer.cases.json | 43 ++++++ .../seed/scripts/tests/transfer.test.lua | 133 ++++++++++++++++++ packages/ui_level6/seed/metadata.json | 10 ++ 11 files changed, 379 insertions(+), 38 deletions(-) create mode 100644 fakemui/fakemui/atoms/Heading.tsx create mode 100644 packages/ui_level5/seed/scripts/tests/transfer.cases.json create mode 100644 packages/ui_level5/seed/scripts/tests/transfer.test.lua diff --git a/fakemui/fakemui/atoms/Heading.tsx b/fakemui/fakemui/atoms/Heading.tsx new file mode 100644 index 000000000..699ae1397 --- /dev/null +++ b/fakemui/fakemui/atoms/Heading.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +export interface HeadingProps extends React.HTMLAttributes { + children?: React.ReactNode + text?: string + level?: 1 | 2 | 3 | 4 | 5 | 6 + variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +} + +/** + * Heading component for section titles + * Maps to Typography variant="h1-h6" or can be used standalone + */ +export const Heading: React.FC = ({ + children, + text, + level = 2, + variant, + className = '', + ...props +}) => { + const headingLevel = variant ? parseInt(variant.slice(1)) : level + const Tag = `h${headingLevel}` as keyof JSX.IntrinsicElements + + return ( + + {text || children} + + ) +} diff --git a/fakemui/fakemui/inputs/Button.tsx b/fakemui/fakemui/inputs/Button.tsx index 1284411aa..5e20b4e64 100644 --- a/fakemui/fakemui/inputs/Button.tsx +++ b/fakemui/fakemui/inputs/Button.tsx @@ -1,28 +1,135 @@ import React, { forwardRef } from 'react' +/** + * Valid button variants for styling + */ +export type ButtonVariant = 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'text' + +/** + * Valid button sizes + */ +export type ButtonSize = 'sm' | 'md' | 'lg' + export interface ButtonProps extends React.ButtonHTMLAttributes { children?: React.ReactNode + /** Button style variant */ + variant?: ButtonVariant + /** Button size */ + size?: ButtonSize + /** @deprecated Use variant="primary" instead */ primary?: boolean + /** @deprecated Use variant="secondary" instead */ secondary?: boolean + /** @deprecated Use variant="outline" instead */ outline?: boolean + /** @deprecated Use variant="ghost" instead */ ghost?: boolean + /** @deprecated Use size="sm" instead */ sm?: boolean + /** @deprecated Use size="lg" instead */ lg?: boolean + /** Icon-only button styling */ icon?: boolean + /** Show loading spinner and disable */ loading?: boolean + /** Full width button */ + fullWidth?: boolean + /** Start icon element */ + startIcon?: React.ReactNode + /** End icon element */ + endIcon?: React.ReactNode } +/** + * Get variant class from props (supports legacy and new API) + */ +const getVariantClass = (props: ButtonProps): string => { + if (props.variant) return `btn--${props.variant}` + if (props.primary) return 'btn--primary' + if (props.secondary) return 'btn--secondary' + if (props.outline) return 'btn--outline' + if (props.ghost) return 'btn--ghost' + return '' +} + +/** + * Get size class from props (supports legacy and new API) + */ +const getSizeClass = (props: ButtonProps): string => { + if (props.size) return `btn--${props.size}` + if (props.sm) return 'btn--sm' + if (props.lg) return 'btn--lg' + return '' +} + +/** + * Button component with Material-UI inspired styling + * + * @example + * ```tsx + * + * + * + * ``` + */ export const Button = forwardRef( - ({ children, primary, secondary, outline, ghost, sm, lg, icon, loading, disabled, className = '', ...props }, ref) => ( - - ) + (props, ref) => { + const { + children, + variant, + size, + primary, + secondary, + outline, + ghost, + sm, + lg, + icon, + loading, + fullWidth, + startIcon, + endIcon, + disabled, + className = '', + type = 'button', + 'aria-busy': ariaBusy, + ...restProps + } = props + + const classes = [ + 'btn', + getVariantClass(props), + getSizeClass(props), + icon ? 'btn--icon' : '', + loading ? 'btn--loading' : '', + fullWidth ? 'btn--full-width' : '', + className, + ].filter(Boolean).join(' ') + + return ( + + ) + } ) Button.displayName = 'Button' diff --git a/frontends/nextjs/src/components/atoms/README.md b/frontends/nextjs/src/components/atoms/README.md index 8053e923d..e327e1764 100644 --- a/frontends/nextjs/src/components/atoms/README.md +++ b/frontends/nextjs/src/components/atoms/README.md @@ -26,7 +26,7 @@ Atoms are the smallest, indivisible UI elements in the MetaBuilder component lib | `Badge` | Status indicator chip | `Chip` | | `Avatar` | User/entity image with fallback | `MuiAvatar` | | `IconButton` | Icon-only button | `MuiIconButton` | -| `Icon` | Icon wrapper for MUI icons | `@mui/icons-material` | +| `Icon` | Icon wrapper for fakemui icons | `@/fakemui/icons` | | `Link` | Navigation link with Next.js integration | `MuiLink` + `NextLink` | | `Text` | Typography with weight/alignment options | `Typography` | diff --git a/frontends/nextjs/src/components/atoms/display/Icon.test.tsx b/frontends/nextjs/src/components/atoms/display/Icon.test.tsx index 757515e4b..a58a1badb 100644 --- a/frontends/nextjs/src/components/atoms/display/Icon.test.tsx +++ b/frontends/nextjs/src/components/atoms/display/Icon.test.tsx @@ -7,15 +7,15 @@ describe('Icon', () => { it.each([ { name: 'Home', size: 'small' }, { name: 'Settings', size: 'medium' }, - { name: 'Delete', size: 'large' }, - { name: 'Add', size: 'inherit' }, + { name: 'Trash', size: 'large' }, + { name: 'Plus', size: 'inherit' }, ] as const)('renders icon $name with size $size', ({ name, size }) => { const { container } = render() expect(container.querySelector('svg')).not.toBeNull() }) it('applies custom sx styles', () => { - const { container } = render() + const { container } = render() expect(container.querySelector('svg')).not.toBeNull() }) diff --git a/frontends/nextjs/src/components/atoms/display/Icon.tsx b/frontends/nextjs/src/components/atoms/display/Icon.tsx index 389d3625b..3e2ad100e 100644 --- a/frontends/nextjs/src/components/atoms/display/Icon.tsx +++ b/frontends/nextjs/src/components/atoms/display/Icon.tsx @@ -1,40 +1,44 @@ 'use client' -import * as MuiIcons from '@mui/icons-material' -import { SvgIconProps } from '@mui/material' -import { forwardRef } from 'react' +import * as FakeMuiIcons from '@/fakemui/icons' +import { CSSProperties, forwardRef } from 'react' -export type IconName = keyof typeof MuiIcons +// Create a type from the available fakemui icons +export type IconName = keyof typeof FakeMuiIcons export type IconSize = 'small' | 'medium' | 'large' | 'inherit' -export interface IconProps extends Omit { +export interface IconProps { name: IconName size?: IconSize + sx?: CSSProperties & Record + className?: string + style?: CSSProperties +} + +const sizeMap = { + small: 20, + medium: 24, + large: 32, + inherit: undefined, } const Icon = forwardRef( - ({ name, size = 'medium', sx, ...props }, ref) => { - const IconComponent = MuiIcons[name] + ({ name, size = 'medium', sx, className, style, ...props }, ref) => { + const IconComponent = FakeMuiIcons[name] - if (!IconComponent) { - console.warn(`Icon "${name}" not found in @mui/icons-material`) + if (!IconComponent || typeof IconComponent !== 'function') { + console.warn(`Icon "${name}" not found in @/fakemui/icons`) return null } - const fontSizeMap = { - small: 20, - medium: 24, - large: 32, - inherit: 'inherit' as const, - } + const sizeValue = sizeMap[size] + const combinedStyle = { ...style, ...sx } return ( ) diff --git a/frontends/nextjs/src/components/ui/README.md b/frontends/nextjs/src/components/ui/README.md index 9d3893140..75972b8b9 100644 --- a/frontends/nextjs/src/components/ui/README.md +++ b/frontends/nextjs/src/components/ui/README.md @@ -37,9 +37,11 @@ import { Button, Input } from '@/components/ui/atoms' import { Card, Select } from '@/components/ui/molecules' import { Table, Form } from '@/components/ui/organisms' -// Direct MUI imports are also fine +// Direct MUI imports are also fine for layout/typography import { Box, Typography, Grid } from '@mui/material' -import { Add as AddIcon } from '@mui/icons-material' + +// Use fakemui for icons +import { Plus, Trash, Settings } from '@/fakemui/icons' ``` ## Atomic Design Pattern diff --git a/frontends/nextjs/src/lib/rendering/index.ts b/frontends/nextjs/src/lib/rendering/index.ts index acd094a5a..4138ba170 100644 --- a/frontends/nextjs/src/lib/rendering/index.ts +++ b/frontends/nextjs/src/lib/rendering/index.ts @@ -7,3 +7,13 @@ export { type PageDefinition, PageRenderer, } from './page-renderer' + +// Component registry for Lua to React mapping +export { + componentRegistry, + getComponentByType, + hasComponent, + getRegisteredComponentTypes, + registerComponent, + type LuaComponentProps, +} from './component-registry' diff --git a/packages/ui_level5/seed/metadata.json b/packages/ui_level5/seed/metadata.json index 591df305e..699aecd1b 100644 --- a/packages/ui_level5/seed/metadata.json +++ b/packages/ui_level5/seed/metadata.json @@ -28,11 +28,13 @@ "tests": { "scripts": [ "tests/metadata.test.lua", - "tests/components.test.lua" + "tests/components.test.lua", + "tests/transfer.test.lua" ], "cases": [ "tests/metadata.cases.json", - "tests/components.cases.json" + "tests/components.cases.json", + "tests/transfer.cases.json" ] }, "minLevel": 5 diff --git a/packages/ui_level5/seed/scripts/tests/transfer.cases.json b/packages/ui_level5/seed/scripts/tests/transfer.cases.json new file mode 100644 index 000000000..c43ffab77 --- /dev/null +++ b/packages/ui_level5/seed/scripts/tests/transfer.cases.json @@ -0,0 +1,43 @@ +{ + "initiateTransfer": { + "authorized": [ + { "userLevel": 6, "tenantId": "tenant_001", "description": "supergod can initiate transfer" } + ], + "unauthorized": [ + { "userLevel": 1, "description": "public cannot initiate" }, + { "userLevel": 2, "description": "user cannot initiate" }, + { "userLevel": 3, "description": "moderator cannot initiate" }, + { "userLevel": 4, "description": "admin cannot initiate" }, + { "userLevel": 5, "description": "god cannot initiate" } + ] + }, + "confirmTransfer": { + "valid": [ + { "tenantId": "tenant_001", "targetUserId": "new_owner_1", "description": "transfer to new owner" }, + { "tenantId": "tenant_002", "targetUserId": "admin_user", "description": "transfer to admin" } + ], + "missingTarget": [ + { "tenantId": "tenant_001", "description": "missing target user" } + ], + "unauthorized": [ + { "userLevel": 1, "description": "public cannot confirm" }, + { "userLevel": 2, "description": "user cannot confirm" }, + { "userLevel": 3, "description": "moderator cannot confirm" }, + { "userLevel": 4, "description": "admin cannot confirm" }, + { "userLevel": 5, "description": "god cannot confirm" } + ] + }, + "assignGod": { + "authorized": [ + { "targetUserId": "user_123", "description": "assign god to user" }, + { "targetUserId": "admin_456", "description": "promote admin to god" } + ], + "unauthorized": [ + { "userLevel": 1, "description": "public cannot assign god" }, + { "userLevel": 2, "description": "user cannot assign god" }, + { "userLevel": 3, "description": "moderator cannot assign god" }, + { "userLevel": 4, "description": "admin cannot assign god" }, + { "userLevel": 5, "description": "god cannot assign god" } + ] + } +} diff --git a/packages/ui_level5/seed/scripts/tests/transfer.test.lua b/packages/ui_level5/seed/scripts/tests/transfer.test.lua new file mode 100644 index 000000000..be49188ee --- /dev/null +++ b/packages/ui_level5/seed/scripts/tests/transfer.test.lua @@ -0,0 +1,133 @@ +-- Transfer Tests for ui_level5 +-- Parameterized tests for god-level transfer functions + +local describe = require("lua_test.describe") +local it = require("lua_test.it") +local it_each = require("lua_test.it_each") +local expect = require("lua_test.expect") +local beforeEach = require("lua_test.beforeEach") +local mock = require("lua_test.mock") + +local cases = require("tests.transfer.cases") +local initiateTransfer = require("transfer.initiate_transfer") +local confirmTransfer = require("transfer.confirm_transfer") +local assignGod = require("transfer.assign_god") + +describe("transfer (level5)", function() + -- Mock permission system and levels + beforeEach(function() + _G.LEVELS = { + PUBLIC = 1, + USER = 2, + MODERATOR = 3, + ADMIN = 4, + GOD = 5, + SUPERGOD = 6 + } + _G.check = { + can_access = mock.fn(function(user, level) + return user.level and user.level >= level + end) + } + end) + + describe("initiateTransfer", function() + describe("with supergod permissions", function() + it_each(cases.initiateTransfer.authorized, "$description", function(case) + local user = { id = "god_1", level = case.userLevel } + local ctx = { user = user, tenantId = case.tenantId } + local result = initiateTransfer.initiateTransfer(ctx) + expect(result.success).toBe(true) + expect(result.action).toBe("open_transfer_dialog") + expect(result.tenantId).toBe(case.tenantId) + end) + end) + + describe("without supergod permissions", function() + it_each(cases.initiateTransfer.unauthorized, "$description (level $userLevel)", function(case) + local user = { id = "user_1", level = case.userLevel } + local ctx = { user = user, tenantId = "tenant_1" } + + _G.check.can_access = mock.fn(function(u, l) + return u.level >= l + end) + + local result = initiateTransfer.initiateTransfer(ctx) + expect(result.success).toBe(false) + expect(result.error).toBe("Supergod required") + end) + end) + end) + + describe("confirmTransfer", function() + describe("with valid parameters", function() + it_each(cases.confirmTransfer.valid, "$description", function(case) + local user = { id = "supergod_1", level = 6 } + local ctx = { user = user, tenantId = case.tenantId, targetUserId = case.targetUserId } + local result = confirmTransfer.confirmTransfer(ctx) + expect(result.success).toBe(true) + expect(result.action).toBe("transfer_ownership") + expect(result.targetUserId).toBe(case.targetUserId) + end) + end) + + describe("missing target user", function() + it_each(cases.confirmTransfer.missingTarget, "$description", function(case) + local user = { id = "supergod_1", level = 6 } + local ctx = { user = user, tenantId = case.tenantId, targetUserId = nil } + local result = confirmTransfer.confirmTransfer(ctx) + expect(result.success).toBe(false) + expect(result.error).toBe("Target user required") + end) + end) + + describe("unauthorized", function() + it_each(cases.confirmTransfer.unauthorized, "$description", function(case) + local user = { id = "user_1", level = case.userLevel } + local ctx = { user = user, tenantId = "tenant_1", targetUserId = "user_2" } + + _G.check.can_access = mock.fn(function(u, l) + return u.level >= l + end) + + local result = confirmTransfer.confirmTransfer(ctx) + expect(result.success).toBe(false) + expect(result.error).toBe("Supergod required") + end) + end) + end) + + describe("assignGod", function() + describe("with supergod permissions", function() + it_each(cases.assignGod.authorized, "$description", function(case) + local user = { id = "supergod_1", level = 6 } + local ctx = { user = user, userId = case.targetUserId } + local result = assignGod.assignGod(ctx) + expect(result.success).toBe(true) + expect(result.action).toBe("assign_god") + expect(result.userId).toBe(case.targetUserId) + end) + end) + + describe("without supergod permissions", function() + it_each(cases.assignGod.unauthorized, "$description", function(case) + local user = { id = "user_1", level = case.userLevel } + local ctx = { user = user, userId = "target_1" } + + _G.check.can_access = mock.fn(function(u, l) + return u.level >= l + end) + + local result = assignGod.assignGod(ctx) + expect(result.success).toBe(false) + expect(result.error).toBe("Supergod required") + end) + end) + end) + +end) + +return { + name = "transfer.test", + description = "Tests for god-level transfer functions" +} diff --git a/packages/ui_level6/seed/metadata.json b/packages/ui_level6/seed/metadata.json index 8b844df98..39ff45524 100644 --- a/packages/ui_level6/seed/metadata.json +++ b/packages/ui_level6/seed/metadata.json @@ -15,5 +15,15 @@ "pages": ["level6"], "scripts": ["layout", "tenants", "transfer", "system"], "components": [] + }, + "tests": { + "scripts": [ + "tests/metadata.test.lua", + "tests/components.test.lua", + "tests/transfer.test.lua" + ], + "cases": [ + "tests/transfer.cases.json" + ] } }