Documentation Index Fetch the complete documentation index at: https://mintlify.com/EdgarJr30/proyecto-de-grado-cms/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The MLM CMMS frontend uses a component-based architecture with reusable UI primitives, feature-specific components, and layout elements.
Component Organization
components/
├── ui/ # Base UI primitives
│ ├── button.tsx
│ ├── input.tsx
│ ├── select.tsx
│ ├── Modal.tsx
│ ├── Spinner.tsx
│ └── filters/ # Filter components
├── layout/ # Layout components
│ ├── Header.tsx
│ └── Sidebar.tsx
├── navigation/ # Navigation components
├── common/ # Shared components
├── dashboard/ # Dashboard widgets
├── reports/ # Report components
├── notifications/ # Notification components
├── pwa/ # PWA-specific components
└── app/ # App-level components
├── ThemedAppRoot.tsx
└── AppRouterContent.tsx
UI Primitives
A flexible button component with variants:
src/components/ui/button.tsx
import * as React from "react"
import { cn } from "../../utils/cn"
export interface ButtonProps extends React . ButtonHTMLAttributes < HTMLButtonElement > {
variant ?: "default" | "outline"
}
export const Button = React . forwardRef < HTMLButtonElement , ButtonProps >(
({ className , variant = "default" , type = "button" , ... props }, ref ) => {
const baseStyle =
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 cursor-pointer"
const variants = {
default: "bg-blue-600 text-white hover:bg-blue-500" ,
outline: "border border-gray-300 text-gray-700 hover:bg-gray-100" ,
}
return (
< button
ref = { ref }
type = { type }
className = { cn ( baseStyle , variants [ variant ], className ) }
{ ... props }
/>
)
}
)
Button . displayName = "Button"
import { Button } from './components/ui/button' ;
function MyComponent () {
return (
< div >
< Button variant = "default" > Save </ Button >
< Button variant = "outline" > Cancel </ Button >
</ div >
);
}
src/components/ui/input.tsx
import * as React from "react"
import { cn } from "../../utils/cn"
export interface InputProps
extends React . InputHTMLAttributes < HTMLInputElement > {}
export const Input = React . forwardRef < HTMLInputElement , InputProps >(
({ className , type = "text" , ... props }, ref ) => {
return (
< input
type = { type }
className = { cn (
"flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm" ,
"focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" ,
"disabled:cursor-not-allowed disabled:opacity-50" ,
className
) }
ref = { ref }
{ ... props }
/>
)
}
)
Input . displayName = "Input"
Modal Component
A reusable modal for dialogs and forms:
src/components/ui/Modal.tsx
interface ModalProps {
isOpen : boolean ;
onClose : () => void ;
title ?: string ;
children : React . ReactNode ;
size ?: 'sm' | 'md' | 'lg' | 'xl' ;
}
export function Modal ({ isOpen , onClose , title , children , size = 'md' } : ModalProps ) {
if ( ! isOpen ) return null ;
return (
< div className = "fixed inset-0 z-50 flex items-center justify-center" >
< div className = "fixed inset-0 bg-black/50" onClick = { onClose } />
< div className = { cn ( "relative bg-white rounded-lg shadow-xl" , sizeClasses [ size ]) } >
{ title && (
< div className = "border-b px-6 py-4" >
< h2 className = "text-xl font-semibold" > { title } </ h2 >
</ div >
) }
< div className = "p-6" > { children } </ div >
</ div >
</ div >
);
}
Spinner Component
src/components/ui/Spinner.tsx
export function Spinner ({ size = 'md' } : { size ?: 'sm' | 'md' | 'lg' }) {
const sizeClasses = {
sm: 'w-4 h-4' ,
md: 'w-8 h-8' ,
lg: 'w-12 h-12' ,
};
return (
< div className = { cn ( 'animate-spin rounded-full border-2 border-gray-300 border-t-blue-600' , sizeClasses [ size ]) } />
);
}
Layout Components
App Root
The top-level app component:
src/components/app/ThemedAppRoot.tsx
import { ToastContainer } from 'react-toastify' ;
import { MotionConfig } from 'framer-motion' ;
import { useTheme } from '../../context/ThemeContext' ;
import AppRouterContent from './AppRouterContent' ;
export default function ThemedAppRoot () {
const { isDark } = useTheme ();
return (
< MotionConfig reducedMotion = "user" >
< ToastContainer
position = "bottom-right"
autoClose = { 3000 }
theme = { isDark ? 'dark' : 'light' }
/>
< AppRouterContent />
</ MotionConfig >
);
}
Protected Route Wrapper
import { Navigate } from 'react-router-dom' ;
import { useAuth } from '../../context/AuthContext' ;
import { ScreenLoader } from '../ui/ScreenLoader' ;
export function ProtectedRoute ({ children } : { children : React . ReactNode }) {
const { loading , isAuthenticated } = useAuth ();
if ( loading ) return < ScreenLoader /> ;
if ( ! isAuthenticated ) return < Navigate to = "/login" replace /> ;
return <> { children } </> ;
}
Filter Components
Reusable filter bar for data tables:
src/components/ui/filters/FilterBar.tsx
interface FilterBarProps {
filters : FilterConfig [];
values : Record < string , any >;
onChange : ( key : string , value : any ) => void ;
onReset : () => void ;
}
export function FilterBar ({ filters , values , onChange , onReset } : FilterBarProps ) {
return (
< div className = "flex gap-4 items-center" >
{ filters . map (( filter ) => (
< FilterInput
key = { filter . key }
config = { filter }
value = { values [ filter . key ] }
onChange = { ( value ) => onChange ( filter . key , value ) }
/>
)) }
< Button variant = "outline" onClick = { onReset } >
Limpiar filtros
</ Button >
</ div >
);
}
Context Providers
Components that provide global state:
Auth Context
Permissions Context
src/context/AuthContext.tsx
export const AuthProvider : React . FC <{ children : React . ReactNode }> = ({
children ,
}) => {
const [ loading , setLoading ] = useState ( true );
const [ isAuthenticated , setIsAuthenticated ] = useState ( false );
const refresh = useCallback ( async () => {
const { data : { session } } = await supabase . auth . getSession ();
setIsAuthenticated ( !! session );
setLoading ( false );
}, []);
useEffect (() => {
refresh ();
const { data : { subscription } } = supabase . auth . onAuthStateChange (() => {
refresh ();
});
return () => subscription . unsubscribe ();
}, [ refresh ]);
return (
< AuthContext.Provider value = { { loading , isAuthenticated , refresh } } >
{ children }
</ AuthContext.Provider >
);
};
src/rbac/PermissionsContext.tsx
export function PermissionsProvider ({ children } : { children : React . ReactNode }) {
const [ codes , setCodes ] = useState < string []>([]);
const [ roles , setRoles ] = useState < string []>([]);
const [ ready , setReady ] = useState ( false );
const refresh = useCallback ( async () => {
const { data : perms } = await supabase . rpc ( 'my_permissions' );
const { data : { user } } = await supabase . auth . getUser ();
if ( user ) {
const { data : userRoles } = await supabase
. from ( 'roles' )
. select ( 'name, user_roles!inner(user_id)' )
. eq ( 'user_roles.user_id' , user . id );
setRoles ( userRoles ?. map ( r => r . name ) ?? []);
}
setCodes ( perms ?. map (( p : any ) => p . code ) ?? []);
setReady ( true );
}, []);
const has = useCallback (
( q : string | string []) => {
if ( Array . isArray ( q )) return q . some (( code ) => codes . includes ( code ));
return codes . includes ( q );
},
[ codes ]
);
return (
< PermissionsContext.Provider value = { { codes , roles , ready , has , refresh } } >
{ children }
</ PermissionsContext.Provider >
);
}
Animation Components
Motion primitives using Framer Motion:
src/components/ui/motionPrimitives.tsx
import { motion } from 'framer-motion' ;
export const FadeIn = motion . div ;
export const SlideIn = ({ children , direction = 'up' } : SlideInProps ) => (
< motion.div
initial = { { opacity: 0 , y: direction === 'up' ? 20 : - 20 } }
animate = { { opacity: 1 , y: 0 } }
exit = { { opacity: 0 , y: direction === 'up' ? 20 : - 20 } }
>
{ children }
</ motion.div >
);
The app respects prefers-reduced-motion through the MotionConfig wrapper.
Permission-Aware Components
Components that render based on permissions:
import { Can } from '../rbac/PermissionsContext' ;
function TicketActions ({ ticket } : { ticket : Ticket }) {
return (
< div >
< Can perm = "work_orders:update" >
< Button onClick = { handleEdit } > Edit </ Button >
</ Can >
< Can perm = "work_orders:delete" >
< Button variant = "outline" onClick = { handleDelete } > Delete </ Button >
</ Can >
</ div >
);
}
Common form patterns:
import { useState } from 'react' ;
import { Input } from './ui/input' ;
import { Button } from './ui/button' ;
function TicketForm ({ onSubmit } : { onSubmit : ( data : TicketData ) => void }) {
const [ title , setTitle ] = useState ( '' );
const [ description , setDescription ] = useState ( '' );
const handleSubmit = ( e : React . FormEvent ) => {
e . preventDefault ();
onSubmit ({ title , description });
};
return (
< form onSubmit = { handleSubmit } >
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium mb-1" > Título </ label >
< Input
value = { title }
onChange = { ( e ) => setTitle ( e . target . value ) }
required
/>
</ div >
< div >
< label className = "block text-sm font-medium mb-1" > Descripción </ label >
< textarea
value = { description }
onChange = { ( e ) => setDescription ( e . target . value ) }
className = "w-full rounded-md border border-gray-300 p-2"
/>
</ div >
< Button type = "submit" > Crear Ticket </ Button >
</ div >
</ form >
);
}
Utility Functions
Class Name Merging
import { clsx , type ClassValue } from 'clsx' ;
import { twMerge } from 'tailwind-merge' ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
Use cn() to merge Tailwind classes safely, avoiding conflicts.
Best Practices
Use forwardRef for form components
Extract reusable patterns into components
Keep components focused and composable
Each component should do one thing well. Compose smaller components into larger features.
Use TypeScript interfaces for props
interface ButtonProps extends React . ButtonHTMLAttributes < HTMLButtonElement > {
variant ?: 'default' | 'outline' ;
size ?: 'sm' | 'md' | 'lg' ;
}
Styling
The application uses Tailwind CSS v4 for styling:
< div className = "flex items-center justify-between p-4 bg-white rounded-lg shadow-md" >
< h2 className = "text-xl font-semibold text-gray-900" > Title </ h2 >
< Button variant = "default" > Action </ Button >
</ div >
Theme Support
Components respond to theme context:
import { useTheme } from '../context/ThemeContext' ;
function ThemedComponent () {
const { isDark } = useTheme ();
return (
< div className = { isDark ? 'bg-gray-900 text-white' : 'bg-white text-gray-900' } >
Content
</ div >
);
}
Next Steps
Structure Review the overall architecture
Routing Learn about routing patterns
Services Understand data access