feat: refactor Providers and theme management; implement Lua engine with execution and destruction capabilities

This commit is contained in:
2025-12-25 18:55:30 +00:00
parent e2101950d6
commit 9170fd2bb3
14 changed files with 266 additions and 147 deletions

View File

@@ -0,0 +1,12 @@
import { createContext } from 'react'
import type { User } from '@/lib/level-types'
export interface AuthContextType {
user: User | null
isLoading: boolean
login: (username: string, password: string) => Promise<void>
logout: () => Promise<void>
register: (username: string, email: string, password: string) => Promise<void>
}
export const AuthContext = createContext<AuthContextType | undefined>(undefined)

View File

@@ -1,70 +1,4 @@
'use client'
import { ThemeProvider as MuiThemeProvider, CssBaseline } from '@mui/material'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, useMemo, createContext, useContext } from 'react'
import { lightTheme, darkTheme } from '@/theme/mui-theme'
type ThemeMode = 'light' | 'dark' | 'system'
interface ThemeContextType {
mode: ThemeMode
setMode: (mode: ThemeMode) => void
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within Providers')
}
return context
}
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1,
},
},
})
)
const [mode, setMode] = useState<ThemeMode>('system')
const theme = useMemo(() => {
if (mode === 'system') {
// Detect system preference
const isDark = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false
return isDark ? darkTheme : lightTheme
}
return mode === 'dark' ? darkTheme : lightTheme
}, [mode])
const toggleTheme = () => {
setMode(current => {
if (current === 'light') return 'dark'
if (current === 'dark') return 'system'
return 'light'
})
}
return (
<ThemeContext.Provider value={{ mode, setMode, toggleTheme }}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export { Providers } from './providers/providers-component'
export { useTheme } from './providers/use-theme'

View File

@@ -0,0 +1,53 @@
'use client'
import { useMemo, useState } from 'react'
import { CssBaseline, ThemeProvider as MuiThemeProvider } from '@mui/material'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { lightTheme, darkTheme } from '@/theme/mui-theme'
import { ThemeContext, type ThemeMode } from './theme-context'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1,
},
},
})
)
const [mode, setMode] = useState<ThemeMode>('system')
const theme = useMemo(() => {
if (mode === 'system') {
// Detect system preference
const isDark = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false
return isDark ? darkTheme : lightTheme
}
return mode === 'dark' ? darkTheme : lightTheme
}, [mode])
const toggleTheme = () => {
setMode(current => {
if (current === 'light') return 'dark'
if (current === 'dark') return 'system'
return 'light'
})
}
return (
<ThemeContext.Provider value={{ mode, setMode, toggleTheme }}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</MuiThemeProvider>
</ThemeContext.Provider>
)
}

View File

@@ -0,0 +1,11 @@
import { createContext } from 'react'
export type ThemeMode = 'light' | 'dark' | 'system'
export interface ThemeContextType {
mode: ThemeMode
setMode: (mode: ThemeMode) => void
toggleTheme: () => void
}
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

View File

@@ -0,0 +1,10 @@
import { useContext } from 'react'
import { ThemeContext } from './theme-context'
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within Providers')
}
return context
}

View File

@@ -11,6 +11,9 @@ import { Database, CssCategory } from '@/lib/database'
import { Plus, X, Pencil, Trash, FloppyDisk } from '@phosphor-icons/react'
import { toast } from 'sonner'
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.[\]()%#!,=+-]+$/
const uniqueClasses = (classes: string[]) => Array.from(new Set(classes))
export function CssClassManager() {
const [categories, setCategories] = useState<CssCategory[]>([])
const [isEditing, setIsEditing] = useState(false)
@@ -18,61 +21,121 @@ export function CssClassManager() {
const [categoryName, setCategoryName] = useState('')
const [classes, setClasses] = useState<string[]>([])
const [newClass, setNewClass] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const [classSearchQuery, setClassSearchQuery] = useState('')
useEffect(() => {
loadCategories()
}, [])
const normalizedSearch = searchQuery.trim().toLowerCase()
const filteredCategories = normalizedSearch
? categories.filter((category) =>
category.name.toLowerCase().includes(normalizedSearch) ||
category.classes.some((cls) => cls.toLowerCase().includes(normalizedSearch))
)
: categories
const totalClassCount = categories.reduce((total, category) => total + category.classes.length, 0)
const newClassTokens = newClass.trim().split(/\s+/).filter(Boolean)
const uniqueNewClassTokens = Array.from(new Set(newClassTokens))
const invalidNewClassTokens = uniqueNewClassTokens.filter((token) => !CLASS_TOKEN_PATTERN.test(token))
const duplicateNewClassTokens = uniqueNewClassTokens.filter((token) => classes.includes(token))
const canAddClass =
uniqueNewClassTokens.length > 0 &&
invalidNewClassTokens.length === 0 &&
uniqueNewClassTokens.some((token) => !classes.includes(token))
const normalizedClassSearch = classSearchQuery.trim().toLowerCase()
const filteredEditorClasses = normalizedClassSearch
? classes.filter((cls) => cls.toLowerCase().includes(normalizedClassSearch))
: classes
const loadCategories = async () => {
const cats = await Database.getCssClasses()
setCategories(cats)
const normalized = cats.map((category) => ({
...category,
classes: uniqueClasses(category.classes),
}))
const sorted = normalized.slice().sort((a, b) => a.name.localeCompare(b.name))
setCategories(sorted)
}
const startEdit = (category?: CssCategory) => {
if (category) {
setEditingCategory(category)
setCategoryName(category.name)
setClasses([...category.classes])
setClasses(uniqueClasses(category.classes))
} else {
setEditingCategory(null)
setCategoryName('')
setClasses([])
}
setNewClass('')
setClassSearchQuery('')
setIsEditing(true)
}
const addClass = () => {
if (newClass.trim()) {
setClasses(current => [...current, newClass.trim()])
setNewClass('')
if (uniqueNewClassTokens.length === 0) {
return
}
if (invalidNewClassTokens.length > 0) {
toast.error(`Invalid class name: ${invalidNewClassTokens.join(', ')}`)
return
}
const newTokens = uniqueNewClassTokens.filter((token) => !classes.includes(token))
if (newTokens.length === 0) {
toast.info('Those classes already exist in this category')
return
}
setClasses((current) => uniqueClasses([...current, ...newTokens]))
setNewClass('')
}
const removeClass = (index: number) => {
setClasses(current => current.filter((_, i) => i !== index))
const removeClass = (cssClass: string) => {
setClasses(current => current.filter((cls) => cls !== cssClass))
}
const handleSave = async () => {
if (!categoryName || classes.length === 0) {
const trimmedName = categoryName.trim()
const normalizedClasses = uniqueClasses(classes)
if (!trimmedName || normalizedClasses.length === 0) {
toast.error('Please provide a category name and at least one class')
return
}
const nameConflict = categories.some((category) =>
category.name.toLowerCase() === trimmedName.toLowerCase() &&
category.name !== editingCategory?.name
)
if (nameConflict) {
toast.error('A category with this name already exists')
return
}
const newCategory: CssCategory = {
name: categoryName,
classes,
name: trimmedName,
classes: normalizedClasses,
}
if (editingCategory) {
await Database.updateCssCategory(categoryName, classes)
toast.success('Category updated successfully')
} else {
await Database.addCssCategory(newCategory)
toast.success('Category created successfully')
}
try {
if (editingCategory) {
await Database.updateCssCategory(editingCategory.name, newCategory)
toast.success('Category updated successfully')
} else {
await Database.addCssCategory(newCategory)
toast.success('Category created successfully')
}
setIsEditing(false)
loadCategories()
setIsEditing(false)
loadCategories()
} catch (error) {
toast.error('Failed to save category')
}
}
const handleDelete = async (categoryName: string) => {

View File

@@ -13,9 +13,9 @@
import * as fengari from 'fengari-web'
import type { LuaExecutionContext, LuaExecutionResult } from './functions/types'
import { setupContextAPI } from './functions/setup/setup-context-api'
import { executeLuaCode } from './functions/execution/execute-lua-code'
import { execute } from './functions/engine/execute'
import { destroy } from './functions/engine/destroy'
const lua = fengari.lua
const lauxlib = fengari.lauxlib
const lualib = fengari.lualib
@@ -26,8 +26,8 @@ export type { LuaExecutionContext, LuaExecutionResult }
* LuaEngine class wraps individual Lua execution lambdas
*/
export class LuaEngine {
private L: any
private logs: string[] = []
L: any
logs: string[] = []
constructor() {
this.L = lauxlib.luaL_newstate()
@@ -35,33 +35,11 @@ export class LuaEngine {
setupContextAPI(this.L, this.logs)
}
/**
* Execute Lua code with a context
* @param code - Lua code to execute
* @param context - Execution context
* @returns Execution result
*/
async execute(code: string, context: LuaExecutionContext = {}): Promise<LuaExecutionResult> {
this.logs.length = 0
return executeLuaCode(this.L, code, context, this.logs)
}
/** Execute Lua code with a context */
execute = execute
/**
* Destroy the Lua state
*/
destroy(): void {
if (this.L) {
lua.lua_close(this.L)
}
}
}
/**
* Factory function to create a new LuaEngine instance
* @returns New LuaEngine instance
*/
export const createLuaEngine = (): LuaEngine => {
return new LuaEngine()
/** Destroy the Lua state */
destroy = destroy
}
// Re-export individual functions for direct imports

View File

@@ -0,0 +1,9 @@
import { LuaEngine } from './LuaEngine'
/**
* Factory function to create a new LuaEngine instance
* @returns New LuaEngine instance
*/
export const createLuaEngine = (): LuaEngine => {
return new LuaEngine()
}

View File

@@ -0,0 +1,13 @@
import * as fengari from 'fengari-web'
import type { LuaEngine } from '../../LuaEngine'
const lua = fengari.lua
/**
* Destroy the Lua state
*/
export function destroy(this: LuaEngine): void {
if (this.L) {
lua.lua_close(this.L)
}
}

View File

@@ -0,0 +1,18 @@
import type { LuaExecutionContext, LuaExecutionResult } from '../types'
import type { LuaEngine } from '../../LuaEngine'
import { executeLuaCode } from '../execution/execute-lua-code'
/**
* Execute Lua code with a context
* @param code - Lua code to execute
* @param context - Execution context
* @returns Execution result
*/
export async function execute(
this: LuaEngine,
code: string,
context: LuaExecutionContext = {}
): Promise<LuaExecutionResult> {
this.logs.length = 0
return executeLuaCode(this.L, code, context, this.logs)
}

View File

@@ -1 +1,2 @@
export * from './LuaEngine'
export { createLuaEngine } from './create-lua-engine'

View File

@@ -0,0 +1,3 @@
import { packageGlue, type PackageGlue } from './package-glue'
export const getPackageGlue = (): PackageGlue => packageGlue

View File

@@ -35,33 +35,5 @@ export {
isPackageInstalled,
uninstallPackage,
}
/**
* PackageGlue - Wrapper class for package registry helpers
*
* Each method delegates to a single-function module.
*/
export class PackageGlue {
buildPackageRegistry = buildPackageRegistry
getPackage = getPackage
getPackagesByCategory = getPackagesByCategory
getPackageComponents = getPackageComponents
getPackageScripts = getPackageScripts
getPackageScriptFiles = getPackageScriptFiles
getAllPackageScripts = getAllPackageScripts
getPackageExamples = getPackageExamples
checkDependencies = checkDependencies
installPackageComponents = installPackageComponents
installPackageScripts = installPackageScripts
installPackage = installPackage
uninstallPackage = uninstallPackage
getInstalledPackages = getInstalledPackages
isPackageInstalled = isPackageInstalled
exportAllPackagesForSeed = exportAllPackagesForSeed
}
export const packageGlue = new PackageGlue()
export function getPackageGlue(): PackageGlue {
return packageGlue
}
export { PackageGlue, packageGlue } from './package-glue'
export { getPackageGlue } from './get-package-glue'

View File

@@ -0,0 +1,42 @@
import { buildPackageRegistry } from './build-package-registry'
import { checkDependencies } from './check-dependencies'
import { exportAllPackagesForSeed } from './export-all-packages-for-seed'
import { getAllPackageScripts } from './get-all-package-scripts'
import { getInstalledPackages } from './get-installed-packages'
import { getPackage } from './get-package'
import { getPackageComponents } from './get-package-components'
import { getPackageExamples } from './get-package-examples'
import { getPackageScriptFiles } from './get-package-script-files'
import { getPackageScripts } from './get-package-scripts'
import { getPackagesByCategory } from './get-packages-by-category'
import { installPackage } from './install-package'
import { installPackageComponents } from './install-package-components'
import { installPackageScripts } from './install-package-scripts'
import { isPackageInstalled } from './is-package-installed'
import { uninstallPackage } from './uninstall-package'
/**
* PackageGlue - Wrapper class for package registry helpers
*
* Each method delegates to a single-function module.
*/
export class PackageGlue {
buildPackageRegistry = buildPackageRegistry
getPackage = getPackage
getPackagesByCategory = getPackagesByCategory
getPackageComponents = getPackageComponents
getPackageScripts = getPackageScripts
getPackageScriptFiles = getPackageScriptFiles
getAllPackageScripts = getAllPackageScripts
getPackageExamples = getPackageExamples
checkDependencies = checkDependencies
installPackageComponents = installPackageComponents
installPackageScripts = installPackageScripts
installPackage = installPackage
uninstallPackage = uninstallPackage
getInstalledPackages = getInstalledPackages
isPackageInstalled = isPackageInstalled
exportAllPackagesForSeed = exportAllPackagesForSeed
}
export const packageGlue = new PackageGlue()