Refactor page rendering functions for consistency and readability

- Adjusted indentation and formatting in get-pages.ts for consistency.
- Enhanced readability in build-feature-card.ts by formatting parameters and adding commas.
- Updated build-features-component.ts to maintain consistent formatting and added missing commas.
- Improved formatting in build-hero-component.ts for better readability.
- Standardized formatting in build-level1-homepage.ts by adding commas.
- Refactored initialize-default-pages.ts for consistent indentation and added commas.
- Cleaned up PageRendererUtils.ts by ensuring consistent argument handling.
- Streamlined check-permissions.ts for better readability and consistency.
- Refined execute-lua-script.ts for consistent error handling and formatting.
- Enhanced get-page.ts and get-pages-by-level.ts with consistent return formatting.
- Improved load-pages.ts for better readability and consistent formatting.
- Standardized on-page-load.ts and on-page-unload.ts for consistent formatting and readability.
This commit is contained in:
2025-12-29 22:38:34 +00:00
parent d305b25c76
commit 50cd5c40b2
76 changed files with 1459 additions and 369 deletions

View File

@@ -34,7 +34,9 @@
"Bash(bun run format:*)",
"Bash(while read file)",
"Bash(do eslint:*)",
"Bash(done)"
"Bash(done)",
"Bash(eslint:*)",
"Bash(bunx eslint:*)"
]
}
}

View File

@@ -0,0 +1,377 @@
# Lua UI Migration Strategy
## Vision
Transform MetaBuilder into a **lean framework that loads Lua code** by migrating UI boilerplate from React/TypeScript to Lua packages.
## Current State Analysis
### Statistics
- **401 React components** in the codebase
- **0 Lua UI definition files** currently
- **UI already defined in packages** (TypeScript schemas)
- **Lua engine operational** with Fengari runtime
### Existing Architecture
```
packages/
└── core/
└── package-definitions/
├── set-a/ (forum, guestbook, spotify, youtube)
└── set-b/ (ecommerce, irc-webchat, retro-games)
└── irc-webchat/
├── schema/layout.ts (UI config)
├── actions/commands.ts
├── actions/events.ts
└── validation.ts
```
Currently UI is defined in TypeScript but **already structured for package-based loading**.
## Migration Strategy
### Phase 1: Create Lua UI Definition Format (Weeks 1-2)
#### 1.1 Define Lua UI Schema
Create a Lua DSL for defining UI components:
```lua
-- packages/ui/irc-webchat.lua
return {
metadata = {
id = "irc-webchat",
version = "1.0.0",
name = "IRC Webchat",
description = "Real-time chat interface"
},
pages = {
{
id = "page_chat",
path = "/chat",
title = "IRC Webchat",
level = 2,
requiresAuth = true,
requiredRole = "user",
components = {
{
id = "comp_chat_root",
type = "IRCWebchat",
props = {
channelName = "general",
maxMessages = 100,
enableEmoji = true
}
}
}
}
},
components = {
IRCWebchat = {
defaultProps = {
channelName = "general",
theme = "dark"
},
validation = {
channelName = { type = "string", required = true },
maxMessages = { type = "number", min = 1, max = 1000 }
}
}
}
}
```
#### 1.2 Create Lua-to-React Bridge
**New:** `src/lib/lua/ui/lua-ui-loader.ts`
```typescript
import { executeLuaScript } from '@/lib/lua/engine/execute'
import type { PackageContent } from '@/lib/packages/package-types'
export async function loadLuaUIPackage(
luaSource: string
): Promise<Pick<PackageContent, 'pages' | 'componentConfigs'>> {
// Execute Lua and convert to TypeScript types
const result = await executeLuaScript(luaSource, {
sandbox: true,
timeout: 5000
})
return convertLuaToUISchema(result)
}
```
#### 1.3 TypeScript Type Definitions for Lua UI
**New:** `src/lib/lua/ui/types.ts`
```typescript
export interface LuaUIMetadata {
id: string
version: string
name: string
description: string
}
export interface LuaUIPage {
id: string
path: string
title: string
level: number
requiresAuth?: boolean
requiredRole?: string
components: LuaUIComponent[]
}
export interface LuaUIComponent {
id: string
type: string
props: Record<string, unknown>
children?: LuaUIComponent[]
}
```
### Phase 2: Package Storage System (Weeks 2-3)
#### 2.1 Lua Package Store in Database
Add Lua packages to the database schema:
```typescript
// New table in Prisma schema
model LuaPackage {
id String @id @default(uuid())
packageId String @unique
version String
name String
category String // 'ui', 'action', 'validation'
luaSource String @db.Text // Lua code
metadata Json
tenantId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId, category])
}
```
#### 2.2 Package Installation Flow
```typescript
// src/lib/packages/lua/install-lua-package.ts
export async function installLuaPackage(params: {
packageId: string
luaSource: string
category: 'ui' | 'action' | 'validation'
tenantId: string
}) {
// 1. Validate Lua syntax
await validateLuaSyntax(params.luaSource)
// 2. Execute in sandbox to extract metadata
const metadata = await extractPackageMetadata(params.luaSource)
// 3. Store in database
await db.luaPackage.create({
data: {
packageId: params.packageId,
luaSource: params.luaSource,
category: params.category,
metadata,
tenantId: params.tenantId,
version: metadata.version,
name: metadata.name
}
})
}
```
### Phase 3: Migration of Existing Packages (Weeks 3-5)
#### Priority Order
**Tier 1: Simple UI Packages (Week 3)**
- Guestbook Retro
- Forum Classic
- Spotify Clone UI
**Tier 2: Medium Complexity (Week 4)**
- IRC Webchat
- YouTube Clone
- Ecommerce Basic
**Tier 3: Complex Packages (Week 5)**
- Retro Games
- Package Manager UI
- Nerd Mode IDE
#### Migration Template
For each package:
1. Create `packages/lua-ui/{package-name}.lua`
2. Convert TypeScript UI schema to Lua DSL
3. Write migration tests
4. Update package loader to support both formats
5. Gradually deprecate TypeScript version
### Phase 4: Component Abstraction Layer (Weeks 5-6)
#### 4.1 Core React Components Become "Primitives"
Keep minimal React components as building blocks:
- Form controls (Input, Button, Select)
- Layout containers (Box, Stack, Grid)
- Data display (Table, List, Card)
All composition defined in Lua.
#### 4.2 Component Registry
```lua
-- Lua can reference registered React components
local form = Component.create("Form", {
children = {
Component.create("Input", { name = "username", label = "Username" }),
Component.create("Button", { type = "submit", text = "Login" })
}
})
```
Maps to React component registry:
```typescript
const ComponentRegistry = {
Form: FormPrimitive,
Input: InputPrimitive,
Button: ButtonPrimitive,
// ... primitives only
}
```
### Phase 5: Runtime Package Loading (Weeks 6-7)
#### 5.1 Dynamic Package Loader
```typescript
// src/lib/packages/lua/runtime-loader.ts
export async function loadPackageAtRuntime(packageId: string) {
// 1. Fetch from database or cache
const luaPackage = await fetchLuaPackage(packageId)
// 2. Execute Lua to get UI definition
const uiDef = await loadLuaUIPackage(luaPackage.luaSource)
// 3. Generate React components on-the-fly
return generateComponentTree(uiDef)
}
```
#### 5.2 Hot Reload Support
Lua packages can be updated without rebuilding the framework:
- Update Lua code in database
- Invalidate cache
- UI automatically reflects changes
### Phase 6: Developer Experience (Weeks 7-8)
#### 6.1 Lua Package Development Environment
- **Lua Editor** with syntax highlighting (Monaco)
- **Live Preview** of UI changes
- **Validation** feedback
- **Version Control** for packages
#### 6.2 Package Publishing Flow
1. Write Lua UI package in browser
2. Test in sandbox
3. Publish to tenant
4. Package becomes available to all users
## Benefits
### 1. **Radical Reduction in Build Artifacts**
- From 401 React components → ~20-30 primitive components
- UI defined in database, not compiled code
- Faster builds, smaller bundles
### 2. **Runtime Flexibility**
- Update UI without deploying
- Per-tenant customization
- A/B testing at the package level
### 3. **User Empowerment**
- Advanced users can create packages
- Share packages across tenants
- Package marketplace potential
### 4. **True Multi-Tenancy**
- Each tenant can have different package versions
- Custom branding via Lua
- Isolated package updates
### 5. **Simplified Architecture**
```
BEFORE:
TypeScript Components → Build → Bundle → Deploy
AFTER:
Lua Packages → Database → Runtime Load → Render
```
## Implementation Checklist
### Foundation
- [ ] Create Lua UI DSL specification
- [ ] Implement Lua-to-React bridge
- [ ] Add LuaPackage database model
- [ ] Create package validation system
### Core Functionality
- [ ] Implement runtime package loader
- [ ] Create component registry system
- [ ] Build package installation API
- [ ] Add package version management
### Migration
- [ ] Migrate 3 simple packages (Tier 1)
- [ ] Migrate 3 medium packages (Tier 2)
- [ ] Migrate complex packages (Tier 3)
- [ ] Remove deprecated TypeScript packages
### Developer Experience
- [ ] Lua package editor UI
- [ ] Live preview system
- [ ] Package testing framework
- [ ] Documentation & examples
### Production Readiness
- [ ] Performance optimization
- [ ] Caching strategy
- [ ] Error handling & recovery
- [ ] Security review & sandboxing
## Success Metrics
1. **Code Reduction:** 401 → 30 components (-92%)
2. **Package Count:** 8 packages successfully running from Lua
3. **Build Time:** Reduce by >50%
4. **Bundle Size:** Reduce by >60%
5. **Deploy Frequency:** UI updates without deploy
## Timeline
**8 weeks total** for full migration:
- Weeks 1-2: Foundation & DSL
- Weeks 3-5: Package migration
- Weeks 6-7: Runtime & hot reload
- Week 8: DX & polish
## Risk Mitigation
1. **Gradual Migration:** Both systems run in parallel
2. **Feature Flags:** Toggle Lua/TypeScript per package
3. **Rollback Plan:** Keep TypeScript packages until Lua proven
4. **Testing:** Comprehensive e2e tests for each migrated package
5. **Performance:** Monitor Lua execution times, add caching
## Next Steps
1. Get approval for Lua UI DSL spec
2. Implement proof-of-concept with IRC Webchat
3. Measure performance & user experience
4. Decide on full migration vs. hybrid approach

View File

@@ -0,0 +1,100 @@
'use client'
import React from 'react'
import type { LuaUIComponent } from './types/lua-ui-package'
// Component Registry - Maps Lua component types to React components
import { Box, Stack, Typography, Button, TextField } from '@mui/material'
const ComponentRegistry: Record<string, React.ComponentType<any>> = {
Box,
Stack,
Typography,
Button,
Input: TextField,
TextArea: TextField,
Form: 'form' as any, // Use native HTML form
}
/**
* Generate React component tree from Lua UI definition
* This is the bridge between Lua-defined UI and React rendering
*/
export function generateComponentTree(
luaComponent: LuaUIComponent,
key?: string | number
): React.ReactElement {
const Component = ComponentRegistry[luaComponent.type]
if (!Component) {
console.warn(`Unknown component type: ${luaComponent.type}`)
return <div key={key}>Unknown component: {luaComponent.type}</div>
}
// Convert Lua props to React props
const reactProps = convertLuaPropsToReact(luaComponent.props, luaComponent.type)
// Handle children
const children = luaComponent.children?.map((child, index) => generateComponentTree(child, index))
return React.createElement(Component, { ...reactProps, key }, children)
}
/**
* Convert Lua component props to React component props
* Handles special cases like MUI component props
*/
function convertLuaPropsToReact(
luaProps: Record<string, unknown>,
componentType: string
): Record<string, unknown> {
const reactProps: Record<string, unknown> = { ...luaProps }
// Special handling for different component types
switch (componentType) {
case 'Input':
case 'TextArea':
// Convert generic props to MUI TextField props
if (luaProps.type === 'email') {
reactProps.type = 'email'
}
if (luaProps.rows) {
reactProps.multiline = true
reactProps.rows = luaProps.rows
}
break
case 'Button':
// Handle button text
if (luaProps.text) {
// Text will be rendered as children
delete reactProps.text
}
break
case 'Typography':
// Handle typography text
if (luaProps.text) {
delete reactProps.text
}
break
}
return reactProps
}
/**
* Render text content for components
*/
function renderComponentContent(
componentType: string,
props: Record<string, unknown>
): React.ReactNode {
switch (componentType) {
case 'Button':
case 'Typography':
return props.text as string
default:
return null
}
}

View File

@@ -0,0 +1,66 @@
import { executeLuaCode } from '@/lib/lua/functions/execution/execute-lua-code'
import { createLuaEngine } from '@/lib/lua/engine/core/create-lua-engine'
import type { LuaUIPackage } from './types/lua-ui-package'
import type { LuaValue } from '@/types/utility-types'
/**
* Load a Lua UI package from Lua source code
* Executes the Lua code using existing Fengari engine and converts result to TypeScript types
*/
export async function loadLuaUIPackage(luaSource: string): Promise<LuaUIPackage> {
// Create Lua engine instance using existing infrastructure
const engine = createLuaEngine()
// Execute Lua code and get the returned table
const executionResult = await executeLuaCode(engine.L, luaSource, {}, [])
if (!executionResult.success) {
engine.destroy()
throw new Error(`Lua execution failed: ${executionResult.error}`)
}
const result = executionResult.result
// Validate result structure
if (!isLuaUIPackage(result)) {
engine.destroy()
throw new Error('Invalid Lua UI package: missing required fields')
}
// Clean up engine
engine.destroy()
return convertLuaToUIPackage(result)
}
/**
* Type guard to check if value is a valid Lua UI package
*/
function isLuaUIPackage(value: unknown): value is LuaValue {
if (typeof value !== 'object' || value === null) {
return false
}
const pkg = value as Record<string, unknown>
// Check required fields
if (!pkg.metadata || typeof pkg.metadata !== 'object') return false
if (!pkg.pages || !Array.isArray(pkg.pages)) return false
const metadata = pkg.metadata as Record<string, unknown>
if (!metadata.id || typeof metadata.id !== 'string') return false
if (!metadata.version || typeof metadata.version !== 'string') return false
if (!metadata.name || typeof metadata.name !== 'string') return false
return true
}
/**
* Convert Lua value to TypeScript UI package
* Uses existing Lua-to-JS converters from the Fengari engine
*/
function convertLuaToUIPackage(luaValue: LuaValue): LuaUIPackage {
// The fromLua converter already handles this conversion in executeLuaCode
// We just need to cast to the correct TypeScript type
return luaValue as unknown as LuaUIPackage
}

View File

@@ -0,0 +1,61 @@
/**
* TypeScript types for Lua UI packages
* These types define the structure of UI packages written in Lua
*/
export interface LuaUIMetadata {
id: string
version: string
name: string
description: string
author?: string
category: 'ui' | 'action' | 'validation' | 'workflow'
dependencies?: string[]
}
export interface LuaUIPage {
id: string
path: string
title: string
level: number
requiresAuth?: boolean
requiredRole?: string
layout: LuaUIComponent
}
export interface LuaUIComponent {
type: string
props: Record<string, unknown>
children?: LuaUIComponent[]
}
export interface LuaUIAction {
name: string
handler: string | Function
}
export interface LuaUIValidation {
[fieldName: string]: {
type?: string
required?: boolean
minLength?: number
maxLength?: number
min?: number
max?: number
pattern?: string
format?: string
custom?: string | Function
}
}
export interface LuaUIPackage {
metadata: LuaUIMetadata
pages: LuaUIPage[]
actions?: Record<string, Function>
validation?: LuaUIValidation
hooks?: {
onMount?: Function
onUnmount?: Function
beforeRender?: Function
}
}

View File

@@ -0,0 +1,135 @@
-- Example: Simple Form UI Package
-- This demonstrates how UI can be defined in Lua instead of TypeScript/React
return {
metadata = {
id = "example-form",
version = "1.0.0",
name = "Example Form Package",
description = "A simple form demonstrating Lua UI definition",
author = "MetaBuilder",
category = "ui"
},
pages = {
{
id = "page_example_form",
path = "/example-form",
title = "Example Form",
level = 2,
requiresAuth = false,
layout = {
type = "Stack",
props = {
spacing = 3,
padding = 4
},
children = {
-- Header
{
type = "Typography",
props = {
variant = "h4",
text = "Example Form"
}
},
-- Form
{
type = "Form",
props = {
id = "example_form",
onSubmit = "handleFormSubmit"
},
children = {
-- Name Field
{
type = "Input",
props = {
name = "name",
label = "Your Name",
required = true,
placeholder = "Enter your name"
}
},
-- Email Field
{
type = "Input",
props = {
name = "email",
label = "Email Address",
type = "email",
required = true,
validation = {
pattern = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
message = "Please enter a valid email"
}
}
},
-- Message Field
{
type = "TextArea",
props = {
name = "message",
label = "Message",
rows = 4,
placeholder = "Enter your message"
}
},
-- Submit Button
{
type = "Button",
props = {
type = "submit",
variant = "contained",
color = "primary",
text = "Submit"
}
}
}
}
}
}
}
},
actions = {
handleFormSubmit = function(formData)
-- This Lua function handles form submission
print("Form submitted with data:")
for key, value in pairs(formData) do
print(string.format(" %s = %s", key, value))
end
-- Return success response
return {
success = true,
message = "Form submitted successfully!"
}
end
},
validation = {
name = {
type = "string",
minLength = 2,
maxLength = 100,
required = true
},
email = {
type = "string",
format = "email",
required = true
},
message = {
type = "string",
maxLength = 500
}
}
}

View File

@@ -5,15 +5,15 @@ import type { ComponentInstance } from '@/lib/types/builder-types'
import type { User } from '@/lib/types/level-types'
export async function registerPage(page: PageDefinition): Promise<void> {
this.pages.set(page.id, page)
const pageConfig = {
id: page.id,
path: `/_page_${page.id}`,
title: page.title,
level: page.level,
componentTree: page.components,
requiresAuth: page.permissions?.requiresAuth || false,
requiredRole: page.permissions?.requiredRole as any
}
await Database.addPage(pageConfig)
this.pages.set(page.id, page)
const pageConfig = {
id: page.id,
path: `/_page_${page.id}`,
title: page.title,
level: page.level,
componentTree: page.components,
requiresAuth: page.permissions?.requiresAuth || false,
requiredRole: page.permissions?.requiredRole as any,
}
await Database.addPage(pageConfig)
}

View File

@@ -2,10 +2,7 @@ import type { ComponentInstance } from '@/lib/types/builder-types'
import type { User, UserRole } from '@/lib/types/level-types'
import type { PageDefinition } from './page-renderer'
export function createMockPage(
id: string,
options: Partial<PageDefinition> = {}
): PageDefinition {
export function createMockPage(id: string, options: Partial<PageDefinition> = {}): PageDefinition {
return {
id,
level: options.level ?? 1,

View File

@@ -76,8 +76,18 @@ describe('declarative-component-renderer evaluation', () => {
{ name: 'empty string condition', condition: '', context: {}, expected: true },
{ name: 'null condition', condition: null as any, context: {}, expected: true },
{ name: 'undefined condition', condition: undefined as any, context: {}, expected: true },
{ name: 'truthy context value', condition: 'isActive', context: { isActive: true }, expected: true },
{ name: 'falsy context value', condition: 'isActive', context: { isActive: false }, expected: false },
{
name: 'truthy context value',
condition: 'isActive',
context: { isActive: true },
expected: true,
},
{
name: 'falsy context value',
condition: 'isActive',
context: { isActive: false },
expected: false,
},
{ name: 'missing context key', condition: 'missing', context: {}, expected: false },
{ name: 'truthy string value', condition: 'name', context: { name: 'test' }, expected: true },
{ name: 'empty string value', condition: 'name', context: { name: '' }, expected: false },
@@ -117,8 +127,16 @@ describe('declarative-component-renderer evaluation', () => {
{
name: 'object array data source',
dataSource: 'users',
context: { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] },
expected: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
context: {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
},
expected: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
},
])('should resolve $name', ({ dataSource, context, expected }) => {
expect(renderer.resolveDataSource(dataSource, context)).toEqual(expected)

View File

@@ -127,7 +127,10 @@ describe('declarative-component-renderer lifecycle', () => {
})
it('should load Lua scripts from package', () => {
const luaExecuteSpy = vi.spyOn(DeclarativeComponentRenderer.prototype as any, 'executeLuaScript')
const luaExecuteSpy = vi.spyOn(
DeclarativeComponentRenderer.prototype as any,
'executeLuaScript'
)
loadPackageComponents({
luaScripts: [

View File

@@ -9,10 +9,13 @@ describe('schema-utils migration', () => {
{ appName: 'MyApp', modelName: 'User', expected: 'MyApp_User' },
{ appName: 'app-v2', modelName: 'User_Profile', expected: 'app-v2_User_Profile' },
{ appName: '', modelName: 'Model', expected: '_Model' },
])('should generate key "$expected" for app=$appName, model=$modelName', ({ appName, modelName, expected }) => {
const result = getModelKey(appName, modelName)
expect(result).toBe(expected)
})
])(
'should generate key "$expected" for app=$appName, model=$modelName',
({ appName, modelName, expected }) => {
const result = getModelKey(appName, modelName)
expect(result).toBe(expected)
}
)
})
describe('getRecordsKey', () => {

View File

@@ -17,8 +17,16 @@ describe('schema-utils serialization', () => {
describe('getFieldLabel', () => {
it.each([
{ field: createMockField(), expected: 'Email Address', description: 'custom label' },
{ field: { name: 'email', type: 'email' }, expected: 'Email', description: 'auto-capitalized field name' },
{ field: { name: 'firstName', type: 'string' }, expected: 'FirstName', description: 'multi-word field name' },
{
field: { name: 'email', type: 'email' },
expected: 'Email',
description: 'auto-capitalized field name',
},
{
field: { name: 'firstName', type: 'string' },
expected: 'FirstName',
description: 'multi-word field name',
},
])('should return $description', ({ field, expected }) => {
const result = getFieldLabel(field as FieldSchema)
expect(result).toBe(expected)
@@ -94,12 +102,20 @@ describe('schema-utils serialization', () => {
describe('getDefaultValue', () => {
it.each([
{ field: { name: 'count', type: 'number', default: 42 }, expected: 42, description: 'custom default' },
{
field: { name: 'count', type: 'number', default: 42 },
expected: 42,
description: 'custom default',
},
{ field: { name: 'text', type: 'string' }, expected: '', description: 'string type' },
{ field: { name: 'count', type: 'number' }, expected: 0, description: 'number type' },
{ field: { name: 'active', type: 'boolean' }, expected: false, description: 'boolean type' },
{ field: { name: 'birthDate', type: 'date' }, expected: null, description: 'date type' },
{ field: { name: 'createdAt', type: 'datetime' }, expected: null, description: 'datetime type' },
{
field: { name: 'createdAt', type: 'datetime' },
expected: null,
description: 'datetime type',
},
{
field: {
name: 'status',
@@ -198,7 +214,12 @@ describe('schema-utils serialization', () => {
{ searchTerm: 'ALICE', searchFields: ['name'], filters: {}, expectedCount: 1 },
{ searchTerm: 'bob', searchFields: ['name', 'email'], filters: {}, expectedCount: 1 },
{ searchTerm: '', searchFields: ['name'], filters: { status: 'active' }, expectedCount: 2 },
{ searchTerm: 'charlie', searchFields: ['name'], filters: { status: 'active' }, expectedCount: 1 },
{
searchTerm: 'charlie',
searchFields: ['name'],
filters: { status: 'active' },
expectedCount: 1,
},
{ searchTerm: '', searchFields: ['name'], filters: {}, expectedCount: 3 },
])(
'should filter with searchTerm=$searchTerm, fields=$searchFields, filters=$filters',

View File

@@ -7,7 +7,7 @@ import type { FieldSchema } from '@/lib/schema-types'
*/
export const getDefaultValue = (field: FieldSchema): any => {
if (field.default !== undefined) return field.default
switch (field.type) {
case 'string':
case 'text':

View File

@@ -27,7 +27,12 @@ export const validateField = (field: FieldSchema, value: any): string | null =>
}
}
if (field.type === 'string' || field.type === 'text' || field.type === 'email' || field.type === 'url') {
if (
field.type === 'string' ||
field.type === 'text' ||
field.type === 'email' ||
field.type === 'url'
) {
const strValue = String(value)
if (minLength !== undefined && strValue.length < minLength) {
return `${getFieldLabel(field)} must be at least ${minLength} characters`

View File

@@ -5,19 +5,15 @@
* @param direction - Sort direction ('asc' or 'desc')
* @returns Sorted copy of the records array
*/
export const sortRecords = (
records: any[],
field: string,
direction: 'asc' | 'desc'
): any[] => {
export const sortRecords = (records: any[], field: string, direction: 'asc' | 'desc'): any[] => {
return [...records].sort((a, b) => {
const aVal = a[field]
const bVal = b[field]
if (aVal === bVal) return 0
if (aVal === null || aVal === undefined) return 1
if (bVal === null || bVal === undefined) return -1
const comparison = aVal < bVal ? -1 : 1
return direction === 'asc' ? comparison : -comparison
})

View File

@@ -7,10 +7,7 @@ import { validateField } from '../field/validate-field'
* @param record - The record to validate
* @returns Object mapping field names to error messages
*/
export const validateRecord = (
model: ModelSchema,
record: any
): Record<string, string> => {
export const validateRecord = (model: ModelSchema, record: any): Record<string, string> => {
const errors: Record<string, string> = {}
for (const field of model.fields) {

View File

@@ -1,14 +1,14 @@
/**
* Schema utilities - Re-exports from individual function files
*
*
* This file maintains backward compatibility.
* Prefer importing from SchemaUtils class or individual functions:
*
*
* @example
* // Class pattern (recommended)
* import { SchemaUtils } from '@/lib/schema/SchemaUtils'
* SchemaUtils.validateField(field, value)
*
*
* @example
* // Individual function import
* import { validateField } from '@/lib/schema/functions/validate-field'

View File

@@ -22,9 +22,10 @@ export async function requestScreenshotAnalysis(
const data = (await response.json().catch(() => null)) as unknown
if (!response.ok || !isValidResult(data)) {
const message = (typeof data === 'object' && data !== null && 'error' in data && typeof data.error === 'string')
? data.error
: 'Analysis failed'
const message =
typeof data === 'object' && data !== null && 'error' in data && typeof data.error === 'string'
? data.error
: 'Analysis failed'
throw new Error(message)
}

View File

@@ -6,7 +6,8 @@ const basePayload = {
url: 'https://example.com',
viewport: { width: 1280, height: 720 },
textSample: 'Hello world from the screenshot analyzer.',
htmlSample: '<h1>Title</h1><h2>Subtitle</h2><img src="x" alt="desc" /><a href="#">Link</a><button>Go</button>',
htmlSample:
'<h1>Title</h1><h2>Subtitle</h2><img src="x" alt="desc" /><a href="#">Link</a><button>Go</button>',
}
describe('ScreenshotAnalysisService', () => {
@@ -32,7 +33,7 @@ describe('ScreenshotAnalysisService', () => {
htmlSample: '<img src="x" /><button>Save</button>',
})
expect(result.warnings.some((warning) => warning.includes('No H1'))).toBe(true)
expect(result.warnings.some((warning) => warning.includes('missing alt'))).toBe(true)
expect(result.warnings.some(warning => warning.includes('No H1'))).toBe(true)
expect(result.warnings.some(warning => warning.includes('missing alt'))).toBe(true)
})
})

View File

@@ -33,7 +33,7 @@ export class ScreenshotAnalysisService {
h2Count,
h3Count,
imgCount: imgTags.length,
imgMissingAltCount: imgTags.filter((tag) => !/\salt=/.test(tag)).length,
imgMissingAltCount: imgTags.filter(tag => !/\salt=/.test(tag)).length,
linkCount: this.countMatches(/<a\b[^>]*>/gi, htmlSample),
buttonCount: this.countMatches(/<button\b[^>]*>/gi, htmlSample),
formCount: this.countMatches(/<form\b[^>]*>/gi, htmlSample),
@@ -96,7 +96,7 @@ export class ScreenshotAnalysisService {
if (warnings.length === 0) {
lines.push('- None detected')
} else {
warnings.forEach((warning) => lines.push(`- ${warning}`))
warnings.forEach(warning => lines.push(`- ${warning}`))
}
return lines.join('\n')

View File

@@ -11,5 +11,5 @@ import { JAVASCRIPT_XSS_PATTERNS } from './javascript/xss'
export const JAVASCRIPT_PATTERNS: SecurityPattern[] = [
...JAVASCRIPT_INJECTION_PATTERNS,
...JAVASCRIPT_XSS_PATTERNS,
...JAVASCRIPT_MISC_PATTERNS
...JAVASCRIPT_MISC_PATTERNS,
]

View File

@@ -6,48 +6,49 @@ export const JAVASCRIPT_INJECTION_PATTERNS: SecurityPattern[] = [
type: 'dangerous',
severity: 'critical',
message: 'Use of eval() detected - can execute arbitrary code',
recommendation: 'Use safe alternatives like JSON.parse() or Function constructor with strict validation'
recommendation:
'Use safe alternatives like JSON.parse() or Function constructor with strict validation',
},
{
pattern: /Function\s*\(/gi,
type: 'dangerous',
severity: 'high',
message: 'Dynamic Function constructor detected',
recommendation: 'Avoid dynamic code generation or use with extreme caution'
recommendation: 'Avoid dynamic code generation or use with extreme caution',
},
{
pattern: /import\s+.*\s+from\s+['"]https?:/gi,
type: 'dangerous',
severity: 'critical',
message: 'Remote code import detected',
recommendation: 'Only import from trusted, local sources'
recommendation: 'Only import from trusted, local sources',
},
{
pattern: /setTimeout\s*\(\s*['"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'setTimeout with string argument detected',
recommendation: 'Use setTimeout with function reference instead'
recommendation: 'Use setTimeout with function reference instead',
},
{
pattern: /setInterval\s*\(\s*['"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'setInterval with string argument detected',
recommendation: 'Use setInterval with function reference instead'
recommendation: 'Use setInterval with function reference instead',
},
{
pattern: /require\s*\(\s*[^'"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'Dynamic require() detected',
recommendation: 'Use static imports only'
recommendation: 'Use static imports only',
},
{
pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi,
type: 'malicious',
severity: 'critical',
message: 'System command execution attempt detected',
recommendation: 'This is not allowed in browser environment'
}
recommendation: 'This is not allowed in browser environment',
},
]

View File

@@ -6,76 +6,76 @@ export const JAVASCRIPT_MISC_PATTERNS: SecurityPattern[] = [
type: 'suspicious',
severity: 'medium',
message: 'Calling functions with window context',
recommendation: 'Be careful with context manipulation'
recommendation: 'Be careful with context manipulation',
},
{
pattern: /\.apply\s*\(\s*window/gi,
type: 'suspicious',
severity: 'medium',
message: 'Applying functions with window context',
recommendation: 'Be careful with context manipulation'
recommendation: 'Be careful with context manipulation',
},
{
pattern: /__proto__/gi,
type: 'dangerous',
severity: 'critical',
message: 'Prototype pollution attempt detected',
recommendation: 'Never manipulate __proto__ directly'
recommendation: 'Never manipulate __proto__ directly',
},
{
pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi,
type: 'dangerous',
severity: 'critical',
message: 'Prototype manipulation detected',
recommendation: 'Use Object.create() or proper class syntax'
recommendation: 'Use Object.create() or proper class syntax',
},
{
pattern: /localStorage|sessionStorage/gi,
type: 'warning',
severity: 'low',
message: 'Local/session storage usage detected',
recommendation: 'Use useKV hook for persistent data instead'
recommendation: 'Use useKV hook for persistent data instead',
},
{
pattern: /crypto\.subtle|atob|btoa/gi,
type: 'warning',
severity: 'low',
message: 'Cryptographic operation detected',
recommendation: 'Ensure proper key management and secure practices'
recommendation: 'Ensure proper key management and secure practices',
},
{
pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi,
type: 'warning',
severity: 'medium',
message: 'External HTTP request detected',
recommendation: 'Ensure CORS and security headers are properly configured'
recommendation: 'Ensure CORS and security headers are properly configured',
},
{
pattern: /window\.open/gi,
type: 'suspicious',
severity: 'medium',
message: 'window.open detected',
recommendation: 'Be cautious with popup windows'
recommendation: 'Be cautious with popup windows',
},
{
pattern: /location\.href\s*=/gi,
type: 'suspicious',
severity: 'medium',
message: 'Direct location manipulation detected',
recommendation: 'Use React Router or validate URLs carefully'
recommendation: 'Use React Router or validate URLs carefully',
},
{
pattern: /fs\.|path\.|os\./gi,
type: 'malicious',
severity: 'critical',
message: 'Node.js system module usage detected',
recommendation: 'File system access not allowed in browser'
recommendation: 'File system access not allowed in browser',
},
{
pattern: /process\.env|process\.exit/gi,
type: 'suspicious',
severity: 'medium',
message: 'Process manipulation detected',
recommendation: 'Not applicable in browser environment'
}
recommendation: 'Not applicable in browser environment',
},
]

View File

@@ -6,48 +6,48 @@ export const JAVASCRIPT_XSS_PATTERNS: SecurityPattern[] = [
type: 'dangerous',
severity: 'high',
message: 'innerHTML assignment detected - XSS vulnerability risk',
recommendation: 'Use textContent, createElement, or React JSX instead'
recommendation: 'Use textContent, createElement, or React JSX instead',
},
{
pattern: /dangerouslySetInnerHTML/gi,
type: 'dangerous',
severity: 'high',
message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk',
recommendation: 'Sanitize HTML content or use safe alternatives'
recommendation: 'Sanitize HTML content or use safe alternatives',
},
{
pattern: /document\.write\s*\(/gi,
type: 'dangerous',
severity: 'medium',
message: 'document.write() detected - can cause security issues',
recommendation: 'Use DOM manipulation methods instead'
recommendation: 'Use DOM manipulation methods instead',
},
{
pattern: /<script[^>]*>/gi,
type: 'dangerous',
severity: 'critical',
message: 'Script tag injection detected',
recommendation: 'Never inject script tags dynamically'
recommendation: 'Never inject script tags dynamically',
},
{
pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi,
type: 'suspicious',
severity: 'medium',
message: 'Inline event handler detected',
recommendation: 'Use addEventListener or React event handlers'
recommendation: 'Use addEventListener or React event handlers',
},
{
pattern: /javascript:\s*/gi,
type: 'dangerous',
severity: 'high',
message: 'javascript: protocol detected',
recommendation: 'Never use javascript: protocol in URLs'
recommendation: 'Never use javascript: protocol in URLs',
},
{
pattern: /data:\s*text\/html/gi,
type: 'dangerous',
severity: 'high',
message: 'Data URI with HTML detected',
recommendation: 'Avoid data URIs with executable content'
}
recommendation: 'Avoid data URIs with executable content',
},
]

View File

@@ -11,76 +11,76 @@ export const LUA_PATTERNS: SecurityPattern[] = [
type: 'malicious',
severity: 'critical',
message: 'Lua OS module system call detected',
recommendation: 'OS module access is disabled for security'
recommendation: 'OS module access is disabled for security',
},
{
pattern: /io\.(popen|tmpfile|open|input|output|lines)/gi,
type: 'malicious',
severity: 'critical',
message: 'Lua file I/O operation detected',
recommendation: 'File system access is disabled for security'
recommendation: 'File system access is disabled for security',
},
{
pattern: /loadfile|dofile/gi,
type: 'dangerous',
severity: 'critical',
message: 'Lua file loading function detected',
recommendation: 'File loading is disabled for security'
recommendation: 'File loading is disabled for security',
},
{
pattern: /package\.(loadlib|searchpath|cpath)/gi,
type: 'dangerous',
severity: 'critical',
message: 'Lua dynamic library loading detected',
recommendation: 'Dynamic library loading is disabled'
recommendation: 'Dynamic library loading is disabled',
},
{
pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi,
type: 'dangerous',
severity: 'high',
message: 'Lua debug module advanced features detected',
recommendation: 'Limited debug functionality available'
recommendation: 'Limited debug functionality available',
},
{
pattern: /loadstring\s*\(/gi,
type: 'dangerous',
severity: 'high',
message: 'Lua dynamic code execution detected',
recommendation: 'Use with extreme caution'
recommendation: 'Use with extreme caution',
},
{
pattern: /\.\.\s*[[\]]/gi,
type: 'suspicious',
severity: 'medium',
message: 'Potential Lua table manipulation',
recommendation: 'Ensure proper validation'
recommendation: 'Ensure proper validation',
},
{
pattern: /_G\s*\[/gi,
type: 'suspicious',
severity: 'high',
message: 'Global environment manipulation detected',
recommendation: 'Avoid modifying global environment'
recommendation: 'Avoid modifying global environment',
},
{
pattern: /getmetatable|setmetatable/gi,
type: 'suspicious',
severity: 'medium',
message: 'Metatable manipulation detected',
recommendation: 'Use carefully to avoid security issues'
recommendation: 'Use carefully to avoid security issues',
},
{
pattern: /while\s+true\s+do/gi,
type: 'warning',
severity: 'medium',
message: 'Infinite loop detected',
recommendation: 'Ensure proper break conditions exist'
recommendation: 'Ensure proper break conditions exist',
},
{
pattern: /function\s+(\w+)\s*\([^)]*\)\s*\{[^}]*\1\s*\(/gi,
type: 'warning',
severity: 'low',
message: 'Potential recursive function',
recommendation: 'Ensure recursion has proper termination'
}
recommendation: 'Ensure recursion has proper termination',
},
]

View File

@@ -11,27 +11,27 @@ export const SQL_INJECTION_PATTERNS: SecurityPattern[] = [
type: 'malicious',
severity: 'critical',
message: 'SQL injection attempt detected',
recommendation: 'Use parameterized queries'
recommendation: 'Use parameterized queries',
},
{
pattern: /UNION\s+SELECT/gi,
type: 'malicious',
severity: 'critical',
message: 'SQL UNION injection attempt',
recommendation: 'Use parameterized queries'
recommendation: 'Use parameterized queries',
},
{
pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi,
type: 'malicious',
severity: 'critical',
message: 'SQL authentication bypass attempt',
recommendation: 'Never concatenate user input into SQL'
recommendation: 'Never concatenate user input into SQL',
},
{
pattern: /--[\s]*$/gm,
type: 'suspicious',
severity: 'high',
message: 'SQL comment pattern detected',
recommendation: 'May indicate SQL injection attempt'
}
recommendation: 'May indicate SQL injection attempt',
},
]

View File

@@ -23,7 +23,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
severity: 'critical',
message: 'Script tag detected in HTML',
pattern: match[0].substring(0, 50) + '...',
recommendation: 'Remove script tags or use proper React components'
recommendation: 'Remove script tags or use proper React components',
})
}
@@ -36,7 +36,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
severity: 'high',
message: 'Inline event handler in HTML',
pattern: match[0],
recommendation: 'Use React event handlers instead'
recommendation: 'Use React event handlers instead',
})
}
@@ -48,7 +48,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
severity: 'critical',
message: 'javascript: protocol in href',
pattern: 'javascript:',
recommendation: 'Use proper URLs or event handlers'
recommendation: 'Use proper URLs or event handlers',
})
}
@@ -62,7 +62,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
severity: 'medium',
message: 'Iframe without sandbox attribute',
pattern: match[0],
recommendation: 'Add sandbox attribute to iframes for security'
recommendation: 'Add sandbox attribute to iframes for security',
})
}
}
@@ -73,6 +73,6 @@ export const scanHTML = (html: string): SecurityScanResult => {
return {
safe,
severity,
issues
issues,
}
}

View File

@@ -28,7 +28,7 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
recommendation: pattern.recommendation,
})
}
}
@@ -44,7 +44,7 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
recommendation: pattern.recommendation,
})
}
}
@@ -56,6 +56,6 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
sanitizedCode: safe ? code : undefined,
}
}

View File

@@ -23,7 +23,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
severity: 'medium',
message: 'Invalid JSON format',
pattern: 'JSON parse error',
recommendation: 'Ensure JSON is properly formatted'
recommendation: 'Ensure JSON is properly formatted',
})
}
@@ -35,7 +35,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
severity: 'critical',
message: 'Prototype pollution attempt in JSON',
pattern: '__proto__',
recommendation: 'Remove prototype manipulation from JSON'
recommendation: 'Remove prototype manipulation from JSON',
})
}
@@ -46,7 +46,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
severity: 'critical',
message: 'Script tag in JSON data',
pattern: '<script>',
recommendation: 'Remove all HTML/script content from JSON'
recommendation: 'Remove all HTML/script content from JSON',
})
}
@@ -57,6 +57,6 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
safe,
severity,
issues,
sanitizedCode: safe ? jsonString : undefined
sanitizedCode: safe ? jsonString : undefined,
}
}

View File

@@ -26,7 +26,7 @@ export const scanLua = (code: string): SecurityScanResult => {
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
recommendation: pattern.recommendation,
})
}
}
@@ -38,6 +38,6 @@ export const scanLua = (code: string): SecurityScanResult => {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
sanitizedCode: safe ? code : undefined,
}
}

View File

@@ -10,7 +10,7 @@
* @returns Sanitized string
*/
export const sanitizeInput = (
input: string,
input: string,
type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'
): string => {
let sanitized = input

View File

@@ -24,6 +24,6 @@ export const calculateOverallSeverity = (
if (hasHigh) return 'high'
if (hasMedium) return 'medium'
if (hasLow) return 'low'
return 'safe'
}

View File

@@ -30,7 +30,14 @@ describe('security-scanner detection', () => {
},
])(
'should $name',
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
({
code,
expectedSeverity,
expectedSafe,
expectedIssueType,
expectedIssuePattern,
expectedLine,
}) => {
const result = securityScanner.scanJavaScript(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
@@ -38,7 +45,9 @@ describe('security-scanner detection', () => {
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
const matchesPattern = expectedIssuePattern
? item.pattern.includes(expectedIssuePattern)
: true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
@@ -74,28 +83,33 @@ describe('security-scanner detection', () => {
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
const result = securityScanner.scanLua(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
])(
'should $name',
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
const result = securityScanner.scanLua(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
} else {
expect(result.issues.length).toBe(0)
}
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern
? item.pattern.includes(expectedIssuePattern)
: true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
}
})
)
})
describe('scanJSON', () => {
@@ -170,7 +184,8 @@ describe('security-scanner detection', () => {
it.each([
{
name: 'remove script tags and inline handlers from text',
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
input:
'<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
type: 'text' as const,
shouldExclude: ['<script', 'onclick', 'javascript:'],
},

View File

@@ -1,10 +1,10 @@
/**
* Security Scanner
*
*
* Wraps individual scan functions into a unified class interface.
* Each scanning function is implemented as 1 lambda/function per file
* in the functions/ directory.
*
*
* Pattern: Lambda wrapper class - keeps this file short while maintaining
* a convenient class-based API.
*/
@@ -24,7 +24,7 @@ import { getSeverityIcon } from '../functions/helpers/get-severity-icon'
/**
* SecurityScanner - Wrapper class for security scanning functions
*
*
* All methods delegate to individual lambda functions in functions/scanners/
* This keeps the main class file small while providing a unified API.
*/
@@ -45,11 +45,4 @@ export const securityScanner = new SecurityScanner()
export { getSeverityColor, getSeverityIcon }
// Re-export individual functions for direct use
export {
scanJavaScript,
scanLua,
scanJSON,
scanHTML,
scanForVulnerabilities,
sanitizeInput
}
export { scanJavaScript, scanLua, scanJSON, scanHTML, scanForVulnerabilities, sanitizeInput }

View File

@@ -24,7 +24,7 @@ export async function logOperation(
errorMessage,
ipAddress: ctx.ipAddress,
}
try {
addAuditLog(log)
if (process.env.NODE_ENV === 'development') {

View File

@@ -1,11 +1,5 @@
// Types
export type {
OperationType,
ResourceType,
AuditLog,
SecurityContext,
AccessRule
} from './types'
export type { OperationType, ResourceType, AuditLog, SecurityContext, AccessRule } from './types'
// Core functions
export { ACCESS_RULES } from './query/access-rules'
@@ -76,7 +70,7 @@ export class SecureDatabase {
static sanitizeInput = sanitizeInput
static logOperation = logOperation
static executeQuery = executeQuery
// Operations
static getUsers = getUsers
static getUserById = getUserById

View File

@@ -9,9 +9,7 @@ export interface LoginSecurityContextParams {
requestId?: string
}
export const createLoginSecurityContext = (
params: LoginSecurityContextParams
): SecurityContext => {
export const createLoginSecurityContext = (params: LoginSecurityContextParams): SecurityContext => {
const sanitizedIdentifier = sanitizeInput(params.identifier).trim()
const loginKey = sanitizedIdentifier.length > 0 ? sanitizedIdentifier : 'public'

View File

@@ -17,7 +17,10 @@ const parsePositiveInt = (value: string | undefined, fallback: number): number =
const lockoutConfig = {
windowMs: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_WINDOW_MS, DEFAULT_AUTH_LOCKOUT_WINDOW_MS),
maxAttempts: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_MAX_ATTEMPTS, DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS),
maxAttempts: parsePositiveInt(
process.env.MB_AUTH_LOCKOUT_MAX_ATTEMPTS,
DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS
),
lockoutMs: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_MS, DEFAULT_AUTH_LOCKOUT_MS),
}

View File

@@ -8,8 +8,8 @@ import { loginAttemptTracker } from '../login-attempt-tracker'
* Verify user credentials with security checks
*/
export async function verifyCredentials(
ctx: SecurityContext,
username: string,
ctx: SecurityContext,
username: string,
password: string
): Promise<boolean> {
const sanitizedUsername = sanitizeInput(username).trim()

View File

@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
* Get comments with security checks
*/
export async function getComments(ctx: SecurityContext): Promise<Comment[]> {
return executeQuery(
ctx,
'comment',
'READ',
async () => fetchComments(),
'all_comments'
)
return executeQuery(ctx, 'comment', 'READ', async () => fetchComments(), 'all_comments')
}

View File

@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
* Get Lua scripts with security checks
*/
export async function getLuaScripts(ctx: SecurityContext): Promise<LuaScript[]> {
return executeQuery(
ctx,
'luaScript',
'READ',
async () => fetchLuaScripts(),
'all_lua_scripts'
)
return executeQuery(ctx, 'luaScript', 'READ', async () => fetchLuaScripts(), 'all_lua_scripts')
}

View File

@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
* Get model schemas with security checks
*/
export async function getModelSchemas(ctx: SecurityContext): Promise<ModelSchema[]> {
return executeQuery(
ctx,
'modelSchema',
'READ',
async () => fetchSchemas(),
'all_model_schemas'
)
return executeQuery(ctx, 'modelSchema', 'READ', async () => fetchSchemas(), 'all_model_schemas')
}

View File

@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
* Get page configs with security checks
*/
export async function getPageConfigs(ctx: SecurityContext): Promise<PageConfig[]> {
return executeQuery(
ctx,
'pageConfig',
'READ',
async () => fetchPages(),
'all_page_configs'
)
return executeQuery(ctx, 'pageConfig', 'READ', async () => fetchPages(), 'all_page_configs')
}

View File

@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
* Get workflows with security checks
*/
export async function getWorkflows(ctx: SecurityContext): Promise<Workflow[]> {
return executeQuery(
ctx,
'workflow',
'READ',
async () => fetchWorkflows(),
'all_workflows'
)
return executeQuery(ctx, 'workflow', 'READ', async () => fetchWorkflows(), 'all_workflows')
}

View File

@@ -8,7 +8,7 @@ import { sanitizeInput } from '../sanitize-input'
* Create a new user with security checks
*/
export async function createUser(
ctx: SecurityContext,
ctx: SecurityContext,
userData: Omit<User, 'id' | 'createdAt'>
): Promise<User> {
const sanitized = sanitizeInput(userData)
@@ -29,7 +29,7 @@ export async function createUser(
tenantId: tenantId ?? sanitized.tenantId,
isInstanceOwner: sanitized.isInstanceOwner ?? false,
}
return executeQuery(
ctx,
'user',

View File

@@ -12,10 +12,7 @@ export async function getUserById(ctx: SecurityContext, userId: string): Promise
'user',
'READ',
async () =>
Database.getUserById(
userId,
ctx.user.tenantId ? { tenantId: ctx.user.tenantId } : undefined
),
Database.getUserById(userId, ctx.user.tenantId ? { tenantId: ctx.user.tenantId } : undefined),
userId
)
}

View File

@@ -8,8 +8,8 @@ import { sanitizeInput } from '../sanitize-input'
* Update a user with security checks
*/
export async function updateUser(
ctx: SecurityContext,
userId: string,
ctx: SecurityContext,
userId: string,
updates: Partial<User>
): Promise<User> {
const sanitized = sanitizeInput(updates)
@@ -18,7 +18,7 @@ export async function updateUser(
if (tenantId && sanitized.tenantId && sanitized.tenantId !== tenantId) {
throw new Error('Access denied. Cannot change user tenant.')
}
return executeQuery(
ctx,
'user',
@@ -32,10 +32,7 @@ export async function updateUser(
}
await Database.updateUser(userId, sanitized)
const updated = await Database.getUserById(
userId,
tenantId ? { tenantId } : undefined
)
const updated = await Database.getUserById(userId, tenantId ? { tenantId } : undefined)
if (!updated) {
throw new Error('User not found after update')
}

View File

@@ -1,42 +1,74 @@
import type { AccessRule } from './types'
export const ACCESS_RULES: AccessRule[] = [
{ resource: 'user', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
{
resource: 'user',
operation: 'READ',
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
},
{ resource: 'user', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'user', operation: 'UPDATE', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'user', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'workflow', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
{
resource: 'pageConfig',
operation: 'READ',
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
},
{ resource: 'pageConfig', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'modelSchema', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'comment', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'CREATE', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'UPDATE', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'DELETE', allowedRoles: ['moderator', 'admin', 'god', 'supergod'] },
{
resource: 'comment',
operation: 'READ',
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
},
{
resource: 'comment',
operation: 'CREATE',
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
},
{
resource: 'comment',
operation: 'UPDATE',
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
},
{
resource: 'comment',
operation: 'DELETE',
allowedRoles: ['moderator', 'admin', 'god', 'supergod'],
},
{ resource: 'smtpConfig', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'smtpConfig', operation: 'UPDATE', allowedRoles: ['supergod'] },
{ resource: 'credential', operation: 'READ', allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'] },
{ resource: 'credential', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{
resource: 'credential',
operation: 'READ',
allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'],
},
{
resource: 'credential',
operation: 'UPDATE',
allowedRoles: ['user', 'admin', 'god', 'supergod'],
},
{ resource: 'tenant', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'tenant', operation: 'CREATE', allowedRoles: ['supergod'] },
{ resource: 'tenant', operation: 'UPDATE', allowedRoles: ['supergod'] },

View File

@@ -10,21 +10,19 @@ export async function checkAccess(
operation: OperationType,
resourceId?: string
): Promise<boolean> {
const rule = ACCESS_RULES.find(
r => r.resource === resource && r.operation === operation
)
const rule = ACCESS_RULES.find(r => r.resource === resource && r.operation === operation)
if (!rule) {
return false
}
if (!rule.allowedRoles.includes(ctx.user.role)) {
return false
}
if (rule.customCheck) {
return await rule.customCheck(ctx, resourceId)
}
return true
}

View File

@@ -7,15 +7,13 @@ export function checkRateLimit(userId: string): boolean {
const { windowMs, maxRequests } = getRateLimitConfig()
const now = Date.now()
const userRequests = rateLimitMap.get(userId) || []
const recentRequests = userRequests.filter(
timestamp => now - timestamp < windowMs
)
const recentRequests = userRequests.filter(timestamp => now - timestamp < windowMs)
if (recentRequests.length >= maxRequests) {
return false
}
recentRequests.push(now)
rateLimitMap.set(userId, recentRequests)
return true

View File

@@ -15,7 +15,10 @@ const parsePositiveInt = (value: string | null | undefined, fallback: number): n
const getEnvRateLimitConfig = () => ({
windowMs: parsePositiveInt(process.env.MB_RATE_LIMIT_WINDOW_MS, DEFAULT_RATE_LIMIT_WINDOW_MS),
maxRequests: parsePositiveInt(process.env.MB_RATE_LIMIT_MAX_REQUESTS, DEFAULT_MAX_REQUESTS_PER_WINDOW),
maxRequests: parsePositiveInt(
process.env.MB_RATE_LIMIT_MAX_REQUESTS,
DEFAULT_MAX_REQUESTS_PER_WINDOW
),
})
let rateLimitConfig = getEnvRateLimitConfig()
@@ -31,7 +34,8 @@ export async function loadRateLimitConfig(): Promise<void> {
configLoadPromise = (async () => {
try {
const envConfig = getEnvRateLimitConfig()
const { getSystemConfigValue } = await import('@/lib/db/system-config/get-system-config-value')
const { getSystemConfigValue } =
await import('@/lib/db/system-config/get-system-config-value')
const [windowValue, maxRequestsValue] = await Promise.all([
getSystemConfigValue(RATE_LIMIT_WINDOW_KEY),
getSystemConfigValue(MAX_REQUESTS_KEY),

View File

@@ -1,9 +1,21 @@
import type { User } from '../../types/level-types'
export type OperationType = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE'
export type ResourceType = 'user' | 'workflow' | 'luaScript' | 'pageConfig' |
'modelSchema' | 'comment' | 'componentNode' | 'componentConfig' | 'cssCategory' |
'dropdownConfig' | 'tenant' | 'powerTransfer' | 'smtpConfig' | 'credential'
export type ResourceType =
| 'user'
| 'workflow'
| 'luaScript'
| 'pageConfig'
| 'modelSchema'
| 'comment'
| 'componentNode'
| 'componentConfig'
| 'cssCategory'
| 'dropdownConfig'
| 'tenant'
| 'powerTransfer'
| 'smtpConfig'
| 'credential'
export interface AuditLog {
id: string

View File

@@ -1,12 +1,12 @@
/**
* Seed Data Module
*
*
* Initializes the database with seed data including:
* - Default users at each permission level
* - Initial configuration
* - Package metadata
* - System templates
*
*
* This runs once during application initialization
*/
@@ -14,11 +14,11 @@ import { initializeAllSeedData } from '@/seed-data'
/**
* Seeds the database with initial data
*
*
* This function should be called during app initialization
* to populate the database with default users, configurations,
* and system data needed for the application to function.
*
*
* @async
* @returns {Promise<void>}
*/

View File

@@ -1,7 +1,7 @@
/**
* ComponentType - All supported UI component types in the builder
* @description Union type of all available components
* Supports: Layout (Flex, Grid, Container), Input (Input, Select, Switch),
* Supports: Layout (Flex, Grid, Container), Input (Input, Select, Switch),
* Display (Text, Heading, Badge), Interactive (Button, Dialog, Tabs)
*/
export type ComponentType =

View File

@@ -53,8 +53,16 @@ describe('Type Guards', () => {
describe('getErrorMessage', () => {
it.each([
{ value: new Error('error message'), expected: 'error message', description: 'Error instance' },
{ value: { message: 'custom error' }, expected: 'custom error', description: 'error-like object' },
{
value: new Error('error message'),
expected: 'error message',
description: 'Error instance',
},
{
value: { message: 'custom error' },
expected: 'custom error',
description: 'error-like object',
},
{ value: 'string error', expected: 'string error', description: 'string' },
{ value: null, expected: 'An unknown error occurred', description: 'null' },
{ value: undefined, expected: 'An unknown error occurred', description: 'undefined' },
@@ -68,11 +76,21 @@ describe('Type Guards', () => {
describe('hasProperty', () => {
it.each([
{ obj: { name: 'test' }, key: 'name', expected: true, description: 'object with property' },
{ obj: { name: 'test' }, key: 'age', expected: false, description: 'object without property' },
{
obj: { name: 'test' },
key: 'age',
expected: false,
description: 'object without property',
},
{ obj: {}, key: 'name', expected: false, description: 'empty object' },
{ obj: null, key: 'name', expected: false, description: 'null' },
{ obj: undefined, key: 'name', expected: false, description: 'undefined' },
{ obj: 'string', key: 'length', expected: false, description: 'string (primitive, not object)' },
{
obj: 'string',
key: 'length',
expected: false,
description: 'string (primitive, not object)',
},
])('should return $expected for $description', ({ obj, key, expected }) => {
expect(hasProperty(obj, key)).toBe(expected)
})

View File

@@ -1,9 +1,6 @@
/**
* Check if value has a specific property
*/
export function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
export function hasProperty<K extends string>(obj: unknown, key: K): obj is Record<K, unknown> {
return typeof obj === 'object' && obj !== null && key in obj
}

View File

@@ -50,7 +50,7 @@ export interface Comment {
/**
* WorkflowNode - Individual step in a workflow
* @property type - Node type: trigger (event), action (do something),
* @property type - Node type: trigger (event), action (do something),
* condition (if/then), lua (execute script), transform (map data)
* @property position - X/Y coordinates for visual editor
*/

View File

@@ -1,4 +1,4 @@
import { clsx, type ClassValue } from "clsx"
import { clsx, type ClassValue } from 'clsx'
/**
* Utility function to merge class names

View File

@@ -42,7 +42,7 @@ describe('workflow-engine errors', () => {
expect(result.success).toBe(false)
expect(result.error).toContain('Transform failed')
expect(result.logs.filter((log) => log.includes('Retrying node'))).toHaveLength(1)
expect(result.logs.filter(log => log.includes('Retrying node'))).toHaveLength(1)
})
it('propagates Lua script resolution errors', async () => {

View File

@@ -37,7 +37,7 @@ describe('workflow-engine execution', () => {
expect(result.success).toBe(true)
expect(result.outputs.action).toBeUndefined()
expect(Object.keys(result.outputs)).toHaveLength(2)
expect(result.logs.some((log) => log.includes('Condition node returned false'))).toBe(true)
expect(result.logs.some(log => log.includes('Condition node returned false'))).toBe(true)
})
it('passes user context through to Lua nodes', async () => {

View File

@@ -23,7 +23,7 @@ describe('workflow-engine persistence', () => {
expect(result.success).toBe(false)
expect(result.outputs.trigger).toEqual({ value: 3 })
expect(result.outputs.action).toBeUndefined()
expect(result.logs.some((log) => log.includes('Break here'))).toBe(true)
expect(result.logs.some(log => log.includes('Break here'))).toBe(true)
expect(result.error).toContain('Transform failed')
})
@@ -49,7 +49,7 @@ describe('workflow-engine persistence', () => {
expect(result.success).toBe(true)
expect(result.outputs.lua).toBe(99)
expect(result.securityWarnings).toContain('Security issues detected: uses os')
expect(result.logs.some((log) => log.includes('[Lua] lua log'))).toBe(true)
expect(result.logs.some(log => log.includes('[Lua] lua log'))).toBe(true)
expect(mockEngine.destroy).toHaveBeenCalled()
})
})

View File

@@ -48,10 +48,7 @@ function normalizeRetryConfig(config: unknown) {
}
}
function calculateRetryDelayMs(
attempt: number,
config: ReturnType<typeof normalizeRetryConfig>
) {
function calculateRetryDelayMs(attempt: number, config: ReturnType<typeof normalizeRetryConfig>) {
if (config.delayMs <= 0) {
return 0
}

View File

@@ -4,5 +4,5 @@ import type { WorkflowState } from './workflow-state'
* Log a message to workflow state
*/
export function logToWorkflow(state: WorkflowState, ...args: any[]): void {
state.logs.push(args.map((arg) => String(arg)).join(' '))
state.logs.push(args.map(arg => String(arg)).join(' '))
}

View File

@@ -26,11 +26,11 @@ export async function executeLuaCode(
if (result.security.severity === 'critical' || result.security.severity === 'high') {
state.securityWarnings.push(
`Security issues detected: ${result.security.issues.map((i) => i.message).join(', ')}`
`Security issues detected: ${result.security.issues.map(i => i.message).join(', ')}`
)
}
result.execution.logs.forEach((log) => logToWorkflow(state, `[Lua] ${log}`))
result.execution.logs.forEach(log => logToWorkflow(state, `[Lua] ${log}`))
if (!result.execution.success) {
return {

View File

@@ -19,7 +19,7 @@ export async function executeLuaNode(
return await executeLuaCode(luaCode, data, context, state)
}
const script = context.scripts.find((s) => s.id === scriptId)
const script = context.scripts.find(s => s.id === scriptId)
if (!script) {
return {
success: false,

View File

@@ -19,9 +19,7 @@ end
return generateWelcome
`,
parameters: [
{ name: 'username', type: 'string' }
],
parameters: [{ name: 'username', type: 'string' }],
returnType: 'string',
},
{
@@ -37,9 +35,7 @@ end
return formatDate
`,
parameters: [
{ name: 'timestamp', type: 'number' }
],
parameters: [{ name: 'timestamp', type: 'number' }],
returnType: 'string',
},
{
@@ -55,9 +51,7 @@ end
return validateEmail
`,
parameters: [
{ name: 'email', type: 'string' }
],
parameters: [{ name: 'email', type: 'string' }],
returnType: 'boolean',
},
{
@@ -85,7 +79,7 @@ return checkPermission
`,
parameters: [
{ name: 'userRole', type: 'string' },
{ name: 'requiredRole', type: 'string' }
{ name: 'requiredRole', type: 'string' },
],
returnType: 'boolean',
},
@@ -104,7 +98,7 @@ return logPageView
parameters: [
{ name: 'pageId', type: 'string' },
{ name: 'userId', type: 'string' },
{ name: 'timestamp', type: 'number' }
{ name: 'timestamp', type: 'number' },
],
returnType: 'boolean',
},

View File

@@ -19,27 +19,27 @@ export async function initializeWorkflows() {
type: 'condition',
label: 'Validate Input',
config: { action: 'validate_email' },
position: { x: 100, y: 100 }
position: { x: 100, y: 100 },
},
{
id: 'node_create',
type: 'action',
label: 'Create User',
config: { action: 'create_user' },
position: { x: 100, y: 200 }
position: { x: 100, y: 200 },
},
{
id: 'node_notify',
type: 'action',
label: 'Send Welcome Email',
config: { action: 'send_email' },
position: { x: 100, y: 300 }
}
position: { x: 100, y: 300 },
},
],
edges: [
{ id: 'edge_1', source: 'node_validate', target: 'node_create', label: 'valid' },
{ id: 'edge_2', source: 'node_create', target: 'node_notify', label: 'success' }
]
{ id: 'edge_2', source: 'node_create', target: 'node_notify', label: 'success' },
],
},
{
id: 'workflow_page_access',
@@ -52,27 +52,27 @@ export async function initializeWorkflows() {
type: 'condition',
label: 'Check Authentication',
config: { action: 'check_auth' },
position: { x: 100, y: 100 }
position: { x: 100, y: 100 },
},
{
id: 'node_perm_check',
type: 'lua',
label: 'Check Permission',
config: { scriptId: 'script_permission_check' },
position: { x: 100, y: 200 }
position: { x: 100, y: 200 },
},
{
id: 'node_log',
type: 'action',
label: 'Log Access',
config: { action: 'log_page_view' },
position: { x: 100, y: 300 }
}
position: { x: 100, y: 300 },
},
],
edges: [
{ id: 'edge_1', source: 'node_auth_check', target: 'node_perm_check', label: 'authorized' },
{ id: 'edge_2', source: 'node_perm_check', target: 'node_log', label: 'allowed' }
]
{ id: 'edge_2', source: 'node_perm_check', target: 'node_log', label: 'allowed' },
],
},
{
id: 'workflow_comment_submission',
@@ -85,27 +85,37 @@ export async function initializeWorkflows() {
type: 'condition',
label: 'Validate Comment',
config: { action: 'validate_content' },
position: { x: 100, y: 100 }
position: { x: 100, y: 100 },
},
{
id: 'node_save_comment',
type: 'action',
label: 'Save to Database',
config: { action: 'create_comment' },
position: { x: 100, y: 200 }
position: { x: 100, y: 200 },
},
{
id: 'node_notify_success',
type: 'action',
label: 'Show Success',
config: { action: 'show_toast' },
position: { x: 100, y: 300 }
}
position: { x: 100, y: 300 },
},
],
edges: [
{ id: 'edge_1', source: 'node_validate_comment', target: 'node_save_comment', label: 'valid' },
{ id: 'edge_2', source: 'node_save_comment', target: 'node_notify_success', label: 'saved' }
]
{
id: 'edge_1',
source: 'node_validate_comment',
target: 'node_save_comment',
label: 'valid',
},
{
id: 'edge_2',
source: 'node_save_comment',
target: 'node_notify_success',
label: 'saved',
},
],
},
{
id: 'workflow_package_export',
@@ -118,28 +128,28 @@ export async function initializeWorkflows() {
type: 'condition',
label: 'Validate Package',
config: { action: 'validate_package' },
position: { x: 100, y: 100 }
position: { x: 100, y: 100 },
},
{
id: 'node_build_zip',
type: 'action',
label: 'Build Zip',
config: { action: 'build_package_zip' },
position: { x: 100, y: 200 }
position: { x: 100, y: 200 },
},
{
id: 'node_publish_package',
type: 'action',
label: 'Publish Artifact',
config: { action: 'publish_package' },
position: { x: 100, y: 300 }
}
position: { x: 100, y: 300 },
},
],
edges: [
{ id: 'edge_1', source: 'node_validate_package', target: 'node_build_zip', label: 'valid' },
{ id: 'edge_2', source: 'node_build_zip', target: 'node_publish_package', label: 'built' }
]
}
{ id: 'edge_2', source: 'node_build_zip', target: 'node_publish_package', label: 'built' },
],
},
]
for (const workflow of workflows) {

View File

@@ -12,7 +12,7 @@ const packages = [
dataTableMetadata,
formBuilderMetadata,
navMenuMetadata,
notificationCenterMetadata
notificationCenterMetadata,
]
describe('Package System Integration', () => {
@@ -63,14 +63,14 @@ describe('Package System Integration', () => {
throw new Error(`Circular dependency detected: ${pkgId}`)
}
visited.add(pkgId)
const pkg = packages.find(p => p.packageId === pkgId)
if (!pkg) return visited
pkg.dependencies.forEach((depId: string) => {
getDependencies(depId, new Set(visited))
})
return visited
}
@@ -81,7 +81,7 @@ describe('Package System Integration', () => {
it('should have all dependencies reference valid packages', () => {
const allPackageIds = packages.map(pkg => pkg.packageId)
packages.forEach(pkg => {
pkg.dependencies.forEach((depId: string) => {
expect(allPackageIds).toContain(depId)

View File

@@ -24,8 +24,16 @@ export const colors = {
dark: { main: '#34d399', light: '#6ee7b7', dark: '#10b981' },
},
neutral: {
50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8',
400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46',
800: '#27272a', 900: '#18181b', 950: '#09090b',
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b',
},
}

View File

@@ -2,8 +2,20 @@ import { fonts } from './fonts'
export const typography = {
fontFamily: fonts.body,
h1: { fontFamily: fonts.heading, fontWeight: 700, fontSize: '3rem', lineHeight: 1.2, letterSpacing: '-0.02em' },
h2: { fontFamily: fonts.heading, fontWeight: 700, fontSize: '2.25rem', lineHeight: 1.25, letterSpacing: '-0.01em' },
h1: {
fontFamily: fonts.heading,
fontWeight: 700,
fontSize: '3rem',
lineHeight: 1.2,
letterSpacing: '-0.02em',
},
h2: {
fontFamily: fonts.heading,
fontWeight: 700,
fontSize: '2.25rem',
lineHeight: 1.25,
letterSpacing: '-0.01em',
},
h3: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.875rem', lineHeight: 1.3 },
h4: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.5rem', lineHeight: 1.35 },
h5: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.25rem', lineHeight: 1.4 },
@@ -13,9 +25,27 @@ export const typography = {
body1: { fontSize: '1rem', lineHeight: 1.6 },
body2: { fontSize: '0.875rem', lineHeight: 1.5 },
caption: { fontSize: '0.75rem', lineHeight: 1.4 },
overline: { fontWeight: 600, fontSize: '0.75rem', letterSpacing: '0.08em', textTransform: 'uppercase' as const },
overline: {
fontWeight: 600,
fontSize: '0.75rem',
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
},
button: { fontWeight: 500, fontSize: '0.875rem', textTransform: 'none' as const },
code: { fontFamily: fonts.mono, fontSize: '0.875rem', backgroundColor: 'rgba(0,0,0,0.06)', padding: '2px 6px', borderRadius: 4 },
kbd: { fontFamily: fonts.mono, fontSize: '0.75rem', fontWeight: 500, padding: '2px 8px', borderRadius: 4, border: '1px solid' },
code: {
fontFamily: fonts.mono,
fontSize: '0.875rem',
backgroundColor: 'rgba(0,0,0,0.06)',
padding: '2px 6px',
borderRadius: 4,
},
kbd: {
fontFamily: fonts.mono,
fontSize: '0.75rem',
fontWeight: 500,
padding: '2px 8px',
borderRadius: 4,
border: '1px solid',
},
label: { fontSize: '0.875rem', fontWeight: 500, lineHeight: 1.5 },
}

View File

@@ -9,11 +9,18 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
return {
MuiCssBaseline: {
styleOverrides: {
':root': { '--font-body': fonts.body, '--font-heading': fonts.heading, '--font-mono': fonts.mono },
':root': {
'--font-body': fonts.body,
'--font-heading': fonts.heading,
'--font-mono': fonts.mono,
},
html: { scrollBehavior: 'smooth' },
body: { WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale' },
'code, pre, kbd': { fontFamily: fonts.mono },
'::selection': { backgroundColor: isDark ? colors.primary.dark.main : colors.primary.light.main, color: '#fff' },
'::selection': {
backgroundColor: isDark ? colors.primary.dark.main : colors.primary.light.main,
color: '#fff',
},
'::-webkit-scrollbar': { width: 8, height: 8 },
'::-webkit-scrollbar-track': { backgroundColor: isDark ? n[800] : n[100] },
'::-webkit-scrollbar-thumb': { backgroundColor: isDark ? n[600] : n[400], borderRadius: 4 },
@@ -25,12 +32,22 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
root: { borderRadius: 8, textTransform: 'none', fontWeight: 500, padding: '8px 16px' },
sizeSmall: { padding: '4px 12px', fontSize: '0.8125rem' },
sizeLarge: { padding: '12px 24px', fontSize: '0.9375rem' },
containedPrimary: { '&:hover': { boxShadow: `0 4px 12px ${alpha(isDark ? colors.primary.dark.main : colors.primary.light.main, 0.4)}` } },
containedPrimary: {
'&:hover': {
boxShadow: `0 4px 12px ${alpha(isDark ? colors.primary.dark.main : colors.primary.light.main, 0.4)}`,
},
},
},
},
MuiCard: {
defaultProps: { elevation: 0 },
styleOverrides: { root: { borderRadius: 12, border: `1px solid ${isDark ? n[800] : n[200]}`, backgroundImage: 'none' } },
styleOverrides: {
root: {
borderRadius: 12,
border: `1px solid ${isDark ? n[800] : n[200]}`,
backgroundImage: 'none',
},
},
},
MuiCardHeader: {
styleOverrides: {
@@ -39,56 +56,177 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
subheader: { fontSize: '0.875rem', color: isDark ? n[400] : n[600] },
},
},
MuiCardContent: { styleOverrides: { root: { padding: '16px 24px', '&:last-child': { paddingBottom: 24 } } } },
MuiCardActions: { styleOverrides: { root: { padding: '16px 24px', borderTop: `1px solid ${isDark ? n[800] : n[200]}` } } },
MuiPaper: { defaultProps: { elevation: 0 }, styleOverrides: { root: { backgroundImage: 'none' }, outlined: { borderColor: isDark ? n[800] : n[200] } } },
MuiCardContent: {
styleOverrides: { root: { padding: '16px 24px', '&:last-child': { paddingBottom: 24 } } },
},
MuiCardActions: {
styleOverrides: {
root: { padding: '16px 24px', borderTop: `1px solid ${isDark ? n[800] : n[200]}` },
},
},
MuiPaper: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: { backgroundImage: 'none' },
outlined: { borderColor: isDark ? n[800] : n[200] },
},
},
MuiTextField: {
defaultProps: { variant: 'outlined', size: 'small' },
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': { borderRadius: 8, backgroundColor: isDark ? n[900] : '#fff' },
'& .MuiOutlinedInput-root': {
borderRadius: 8,
backgroundColor: isDark ? n[900] : '#fff',
},
'& .MuiOutlinedInput-notchedOutline': { borderColor: isDark ? n[700] : n[300] },
},
},
},
MuiOutlinedInput: { styleOverrides: { root: { borderRadius: 8 } } },
MuiSelect: { styleOverrides: { root: { borderRadius: 8 } } },
MuiChip: { styleOverrides: { root: { borderRadius: 6, fontWeight: 500 }, sizeSmall: { height: 24 } } },
MuiChip: {
styleOverrides: { root: { borderRadius: 6, fontWeight: 500 }, sizeSmall: { height: 24 } },
},
MuiAlert: {
styleOverrides: {
root: { borderRadius: 8, alignItems: 'center' },
standardSuccess: { backgroundColor: alpha(isDark ? colors.success.dark.main : colors.success.light.main, isDark ? 0.15 : 0.1) },
standardError: { backgroundColor: alpha(isDark ? colors.error.dark.main : colors.error.light.main, isDark ? 0.15 : 0.1) },
standardWarning: { backgroundColor: alpha(isDark ? colors.warning.dark.main : colors.warning.light.main, isDark ? 0.15 : 0.1) },
standardInfo: { backgroundColor: alpha(isDark ? colors.info.dark.main : colors.info.light.main, isDark ? 0.15 : 0.1) },
standardSuccess: {
backgroundColor: alpha(
isDark ? colors.success.dark.main : colors.success.light.main,
isDark ? 0.15 : 0.1
),
},
standardError: {
backgroundColor: alpha(
isDark ? colors.error.dark.main : colors.error.light.main,
isDark ? 0.15 : 0.1
),
},
standardWarning: {
backgroundColor: alpha(
isDark ? colors.warning.dark.main : colors.warning.light.main,
isDark ? 0.15 : 0.1
),
},
standardInfo: {
backgroundColor: alpha(
isDark ? colors.info.dark.main : colors.info.light.main,
isDark ? 0.15 : 0.1
),
},
},
},
MuiDialog: { styleOverrides: { paper: { borderRadius: 16 } } },
MuiDialogTitle: { styleOverrides: { root: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.25rem', padding: '24px 24px 16px' } } },
MuiDialogTitle: {
styleOverrides: {
root: {
fontFamily: fonts.heading,
fontWeight: 600,
fontSize: '1.25rem',
padding: '24px 24px 16px',
},
},
},
MuiDialogContent: { styleOverrides: { root: { padding: '16px 24px' } } },
MuiDialogActions: { styleOverrides: { root: { padding: '16px 24px 24px', gap: 8 } } },
MuiTable: { styleOverrides: { root: { borderCollapse: 'separate', borderSpacing: 0 } } },
MuiTableHead: { styleOverrides: { root: { '& .MuiTableCell-root': { fontWeight: 600, backgroundColor: isDark ? n[900] : n[50], borderBottom: `2px solid ${isDark ? n[700] : n[300]}` } } } },
MuiTableCell: { styleOverrides: { root: { borderBottom: `1px solid ${isDark ? n[800] : n[200]}`, padding: '12px 16px' } } },
MuiTableRow: { styleOverrides: { root: { '&:hover': { backgroundColor: isDark ? alpha(n[700], 0.3) : alpha(n[100], 0.5) } } } },
MuiTabs: { styleOverrides: { root: { minHeight: 48 }, indicator: { height: 3, borderRadius: '3px 3px 0 0' } } },
MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, minHeight: 48, padding: '12px 16px' } } },
MuiTooltip: { styleOverrides: { tooltip: { backgroundColor: isDark ? n[700] : n[900], fontSize: '0.75rem', padding: '6px 12px', borderRadius: 6 }, arrow: { color: isDark ? n[700] : n[900] } } },
MuiMenu: { styleOverrides: { paper: { borderRadius: 8, border: `1px solid ${isDark ? n[800] : n[200]}` } } },
MuiMenuItem: { styleOverrides: { root: { padding: '8px 16px', borderRadius: 4, margin: '2px 8px' } } },
MuiDrawer: { styleOverrides: { paper: { borderRight: `1px solid ${isDark ? n[800] : n[200]}` } } },
MuiAppBar: { defaultProps: { elevation: 0 }, styleOverrides: { root: { backgroundColor: isDark ? n[900] : '#fff', borderBottom: `1px solid ${isDark ? n[800] : n[200]}` } } },
MuiTableHead: {
styleOverrides: {
root: {
'& .MuiTableCell-root': {
fontWeight: 600,
backgroundColor: isDark ? n[900] : n[50],
borderBottom: `2px solid ${isDark ? n[700] : n[300]}`,
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: { borderBottom: `1px solid ${isDark ? n[800] : n[200]}`, padding: '12px 16px' },
},
},
MuiTableRow: {
styleOverrides: {
root: { '&:hover': { backgroundColor: isDark ? alpha(n[700], 0.3) : alpha(n[100], 0.5) } },
},
},
MuiTabs: {
styleOverrides: {
root: { minHeight: 48 },
indicator: { height: 3, borderRadius: '3px 3px 0 0' },
},
},
MuiTab: {
styleOverrides: {
root: { textTransform: 'none', fontWeight: 500, minHeight: 48, padding: '12px 16px' },
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
backgroundColor: isDark ? n[700] : n[900],
fontSize: '0.75rem',
padding: '6px 12px',
borderRadius: 6,
},
arrow: { color: isDark ? n[700] : n[900] },
},
},
MuiMenu: {
styleOverrides: {
paper: { borderRadius: 8, border: `1px solid ${isDark ? n[800] : n[200]}` },
},
},
MuiMenuItem: {
styleOverrides: { root: { padding: '8px 16px', borderRadius: 4, margin: '2px 8px' } },
},
MuiDrawer: {
styleOverrides: { paper: { borderRight: `1px solid ${isDark ? n[800] : n[200]}` } },
},
MuiAppBar: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: {
backgroundColor: isDark ? n[900] : '#fff',
borderBottom: `1px solid ${isDark ? n[800] : n[200]}`,
},
},
},
MuiDivider: { styleOverrides: { root: { borderColor: isDark ? n[800] : n[200] } } },
MuiAvatar: { styleOverrides: { root: { fontFamily: fonts.heading, fontWeight: 600 } } },
MuiBadge: { styleOverrides: { badge: { fontWeight: 600, fontSize: '0.6875rem' } } },
MuiLinearProgress: { styleOverrides: { root: { borderRadius: 4, height: 6 } } },
MuiSkeleton: { styleOverrides: { root: { borderRadius: 4, backgroundColor: isDark ? n[800] : n[200] } } },
MuiAccordion: { defaultProps: { elevation: 0 }, styleOverrides: { root: { border: `1px solid ${isDark ? n[800] : n[200]}`, borderRadius: 8, '&:before': { display: 'none' } } } },
MuiAccordionSummary: { styleOverrides: { root: { padding: '0 16px', minHeight: 56 }, content: { fontWeight: 500 } } },
MuiSkeleton: {
styleOverrides: { root: { borderRadius: 4, backgroundColor: isDark ? n[800] : n[200] } },
},
MuiAccordion: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: {
border: `1px solid ${isDark ? n[800] : n[200]}`,
borderRadius: 8,
'&:before': { display: 'none' },
},
},
},
MuiAccordionSummary: {
styleOverrides: { root: { padding: '0 16px', minHeight: 56 }, content: { fontWeight: 500 } },
},
MuiAccordionDetails: { styleOverrides: { root: { padding: '0 16px 16px' } } },
MuiSwitch: { styleOverrides: { root: { width: 46, height: 26, padding: 0 }, switchBase: { padding: 2, '&.Mui-checked': { transform: 'translateX(20px)' } }, thumb: { width: 22, height: 22 }, track: { borderRadius: 13, opacity: 1, backgroundColor: isDark ? n[700] : n[300] } } },
MuiSwitch: {
styleOverrides: {
root: { width: 46, height: 26, padding: 0 },
switchBase: { padding: 2, '&.Mui-checked': { transform: 'translateX(20px)' } },
thumb: { width: 22, height: 22 },
track: { borderRadius: 13, opacity: 1, backgroundColor: isDark ? n[700] : n[300] },
},
},
MuiIconButton: { styleOverrides: { root: { borderRadius: 8 } } },
MuiListItemButton: { styleOverrides: { root: { borderRadius: 8, margin: '2px 8px', padding: '8px 12px' } } },
MuiListItemButton: {
styleOverrides: { root: { borderRadius: 8, margin: '2px 8px', padding: '8px 12px' } },
},
MuiSnackbar: { styleOverrides: { root: { '& .MuiPaper-root': { borderRadius: 8 } } } },
}
}

View File

@@ -8,8 +8,24 @@ import { layout } from './layout'
import { typography } from './typography'
import { getComponentOverrides } from './components'
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const darkTheme = createTheme({
palette: {
@@ -22,9 +38,19 @@ export const darkTheme = createTheme({
success: colors.success.dark,
neutral: colors.neutral,
background: { default: colors.neutral[950], paper: colors.neutral[900] },
text: { primary: colors.neutral[100], secondary: colors.neutral[400], disabled: colors.neutral[600] },
text: {
primary: colors.neutral[100],
secondary: colors.neutral[400],
disabled: colors.neutral[600],
},
divider: colors.neutral[800],
action: { active: colors.neutral[300], hover: alpha(colors.neutral[400], 0.12), selected: alpha(colors.primary.dark.main, 0.2), disabled: colors.neutral[600], disabledBackground: colors.neutral[800] },
action: {
active: colors.neutral[300],
hover: alpha(colors.neutral[400], 0.12),
selected: alpha(colors.primary.dark.main, 0.2),
disabled: colors.neutral[600],
disabledBackground: colors.neutral[800],
},
},
typography,
spacing: layout.spacing,

View File

@@ -8,4 +8,3 @@ export { lightTheme } from './light-theme'
export { darkTheme } from './dark-theme'
export { useTheme, styled, alpha } from '@mui/material/styles'
export type { Theme, SxProps } from '@mui/material/styles'

View File

@@ -8,8 +8,24 @@ import { layout } from './layout'
import { typography } from './typography'
import { getComponentOverrides } from './components'
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const lightTheme = createTheme({
palette: {
@@ -22,9 +38,19 @@ export const lightTheme = createTheme({
success: colors.success.light,
neutral: colors.neutral,
background: { default: '#ffffff', paper: colors.neutral[50] },
text: { primary: colors.neutral[900], secondary: colors.neutral[600], disabled: colors.neutral[400] },
text: {
primary: colors.neutral[900],
secondary: colors.neutral[600],
disabled: colors.neutral[400],
},
divider: colors.neutral[200],
action: { active: colors.neutral[700], hover: alpha(colors.neutral[500], 0.08), selected: alpha(colors.primary.light.main, 0.12), disabled: colors.neutral[400], disabledBackground: colors.neutral[200] },
action: {
active: colors.neutral[700],
hover: alpha(colors.neutral[500], 0.08),
selected: alpha(colors.primary.light.main, 0.12),
disabled: colors.neutral[400],
disabledBackground: colors.neutral[200],
},
},
typography,
spacing: layout.spacing,

View File

@@ -7,8 +7,24 @@ import { layout } from '../layout'
import { typography } from '../typography'
import { getComponentOverrides } from '../components'
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const darkTheme = createTheme({
palette: {
@@ -21,9 +37,19 @@ export const darkTheme = createTheme({
success: colors.success.dark,
neutral: colors.neutral,
background: { default: colors.neutral[950], paper: colors.neutral[900] },
text: { primary: colors.neutral[100], secondary: colors.neutral[400], disabled: colors.neutral[600] },
text: {
primary: colors.neutral[100],
secondary: colors.neutral[400],
disabled: colors.neutral[600],
},
divider: colors.neutral[800],
action: { active: colors.neutral[300], hover: alpha(colors.neutral[400], 0.12), selected: alpha(colors.primary.dark.main, 0.2), disabled: colors.neutral[600], disabledBackground: colors.neutral[800] },
action: {
active: colors.neutral[300],
hover: alpha(colors.neutral[400], 0.12),
selected: alpha(colors.primary.dark.main, 0.2),
disabled: colors.neutral[600],
disabledBackground: colors.neutral[800],
},
},
typography,
spacing: layout.spacing,

View File

@@ -7,8 +7,24 @@ import { layout } from '../layout'
import { typography } from '../typography'
import { getComponentOverrides } from '../components'
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const lightTheme = createTheme({
palette: {
@@ -21,9 +37,19 @@ export const lightTheme = createTheme({
success: colors.success.light,
neutral: colors.neutral,
background: { default: '#ffffff', paper: colors.neutral[50] },
text: { primary: colors.neutral[900], secondary: colors.neutral[600], disabled: colors.neutral[400] },
text: {
primary: colors.neutral[900],
secondary: colors.neutral[600],
disabled: colors.neutral[400],
},
divider: colors.neutral[200],
action: { active: colors.neutral[700], hover: alpha(colors.neutral[500], 0.08), selected: alpha(colors.primary.light.main, 0.12), disabled: colors.neutral[400], disabledBackground: colors.neutral[200] },
action: {
active: colors.neutral[700],
hover: alpha(colors.neutral[500], 0.08),
selected: alpha(colors.primary.light.main, 0.12),
disabled: colors.neutral[400],
disabledBackground: colors.neutral[200],
},
},
typography,
spacing: layout.spacing,