mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-27 15:24:56 +00:00
docs: nextjs,frontends,ui (11 files)
This commit is contained in:
30
fakemui/fakemui/atoms/Heading.tsx
Normal file
30
fakemui/fakemui/atoms/Heading.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
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<HeadingProps> = ({
|
||||
children,
|
||||
text,
|
||||
level = 2,
|
||||
variant,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const headingLevel = variant ? parseInt(variant.slice(1)) : level
|
||||
const Tag = `h${headingLevel}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className={`heading heading--level-${headingLevel} ${className}`} {...props}>
|
||||
{text || children}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -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<HTMLButtonElement> {
|
||||
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
|
||||
* <Button variant="primary" size="md">Click me</Button>
|
||||
* <Button variant="outline" startIcon={<Plus />}>Add Item</Button>
|
||||
* <Button loading>Saving...</Button>
|
||||
* ```
|
||||
*/
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, primary, secondary, outline, ghost, sm, lg, icon, loading, disabled, className = '', ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`btn ${primary ? 'btn--primary' : ''} ${secondary ? 'btn--secondary' : ''} ${outline ? 'btn--outline' : ''} ${ghost ? 'btn--ghost' : ''} ${sm ? 'btn--sm' : ''} ${lg ? 'btn--lg' : ''} ${icon ? 'btn--icon' : ''} ${loading ? 'btn--loading' : ''} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
(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
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={ariaBusy ?? loading}
|
||||
aria-disabled={disabled || loading}
|
||||
{...restProps}
|
||||
>
|
||||
{loading && (
|
||||
<span className="btn__spinner" aria-hidden="true">
|
||||
<svg className="btn__spinner-icon" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" opacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
{startIcon && <span className="btn__start-icon" aria-hidden="true">{startIcon}</span>}
|
||||
{children && <span className="btn__content">{children}</span>}
|
||||
{endIcon && <span className="btn__end-icon" aria-hidden="true">{endIcon}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Button.displayName = 'Button'
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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(<Icon name={name} size={size} />)
|
||||
expect(container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('applies custom sx styles', () => {
|
||||
const { container } = render(<Icon name="Home" sx={{ color: 'primary.main' }} />)
|
||||
const { container } = render(<Icon name="Home" sx={{ color: 'blue' }} />)
|
||||
expect(container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -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<SvgIconProps, 'fontSize'> {
|
||||
export interface IconProps {
|
||||
name: IconName
|
||||
size?: IconSize
|
||||
sx?: CSSProperties & Record<string, unknown>
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
const sizeMap = {
|
||||
small: 20,
|
||||
medium: 24,
|
||||
large: 32,
|
||||
inherit: undefined,
|
||||
}
|
||||
|
||||
const Icon = forwardRef<SVGSVGElement, IconProps>(
|
||||
({ 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 (
|
||||
<IconComponent
|
||||
ref={ref}
|
||||
sx={{
|
||||
fontSize: fontSizeMap[size],
|
||||
...sx,
|
||||
}}
|
||||
size={sizeValue}
|
||||
style={combinedStyle}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
43
packages/ui_level5/seed/scripts/tests/transfer.cases.json
Normal file
43
packages/ui_level5/seed/scripts/tests/transfer.cases.json
Normal file
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
133
packages/ui_level5/seed/scripts/tests/transfer.test.lua
Normal file
133
packages/ui_level5/seed/scripts/tests/transfer.test.lua
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user