Auto update
diff --git a/src/components/shadcn-studio/tabs/tabs-27.tsx b/src/components/shadcn-studio/tabs/tabs-27.tsx
new file mode 100644
index 0000000..9d2ec8b
--- /dev/null
+++ b/src/components/shadcn-studio/tabs/tabs-27.tsx
@@ -0,0 +1,72 @@
+import { Tabs, TabsContent, TabsContents, TabsList, TabsTrigger } from '@/components/ui/motion-tabs'
+
+const tabs = [
+ {
+ name: 'Explore',
+ value: 'explore',
+ content: (
+ <>
+ Discover fresh ideas, trending topics, and hidden gems
+ curated just for you. Start exploring and let your curiosity lead the way!
+ >
+ )
+ },
+ {
+ name: 'Favorites',
+ value: 'favorites',
+ content: (
+ <>
+ All your favorites are saved here. Revisit articles,
+ collections, and moments you love, any time you want a little inspiration.
+ >
+ )
+ },
+ {
+ name: 'Surprise Me',
+ value: 'surprise',
+ content: (
+ <>
+ Surprise! Here's something unexpected—a fun fact, a
+ quirky tip, or a daily challenge. Come back for a new surprise every day!
+ >
+ )
+ }
+]
+
+const AnimatedTabsDemo = () => {
+ return (
+
+
+
+ {tabs.map(tab => (
+
+ {tab.name}
+
+ ))}
+
+
+
+ {tabs.map(tab => (
+
+ {tab.content}
+
+ ))}
+
+
+
+
+ Inspired by{' '}
+
+ Animate UI
+
+
+
+ )
+}
+
+export default AnimatedTabsDemo
diff --git a/src/components/ui/motion-highlight.tsx b/src/components/ui/motion-highlight.tsx
new file mode 100644
index 0000000..11931a2
--- /dev/null
+++ b/src/components/ui/motion-highlight.tsx
@@ -0,0 +1,549 @@
+import * as React from 'react'
+
+import type { Transition } from 'motion/react'
+import { AnimatePresence, motion } from 'motion/react'
+
+import { cn } from '@/lib/utils'
+
+type MotionHighlightMode = 'children' | 'parent'
+
+type Bounds = {
+ top: number
+ left: number
+ width: number
+ height: number
+}
+
+type MotionHighlightContextType
= {
+ mode: MotionHighlightMode
+ activeValue: T | null
+ setActiveValue: (value: T | null) => void
+ setBounds: (bounds: DOMRect) => void
+ clearBounds: () => void
+ id: string
+ hover: boolean
+ className?: string
+ activeClassName?: string
+ setActiveClassName: (className: string) => void
+ transition?: Transition
+ disabled?: boolean
+ enabled?: boolean
+ exitDelay?: number
+ forceUpdateBounds?: boolean
+}
+
+const MotionHighlightContext = React.createContext<
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ MotionHighlightContextType | undefined
+>(undefined)
+
+function useMotionHighlight(): MotionHighlightContextType {
+ const context = React.useContext(MotionHighlightContext)
+
+ if (!context) {
+ throw new Error('useMotionHighlight must be used within a MotionHighlightProvider')
+ }
+
+ return context as unknown as MotionHighlightContextType
+}
+
+type BaseMotionHighlightProps = {
+ mode?: MotionHighlightMode
+ value?: T | null
+ defaultValue?: T | null
+ onValueChange?: (value: T | null) => void
+ className?: string
+ transition?: Transition
+ hover?: boolean
+ disabled?: boolean
+ enabled?: boolean
+ exitDelay?: number
+}
+
+type ParentModeMotionHighlightProps = {
+ boundsOffset?: Partial
+ containerClassName?: string
+ forceUpdateBounds?: boolean
+}
+
+type ControlledParentModeMotionHighlightProps = BaseMotionHighlightProps &
+ ParentModeMotionHighlightProps & {
+ mode: 'parent'
+ controlledItems: true
+ children: React.ReactNode
+ }
+
+type ControlledChildrenModeMotionHighlightProps = BaseMotionHighlightProps & {
+ mode?: 'children' | undefined
+ controlledItems: true
+ children: React.ReactNode
+}
+
+type UncontrolledParentModeMotionHighlightProps = BaseMotionHighlightProps &
+ ParentModeMotionHighlightProps & {
+ mode: 'parent'
+ controlledItems?: false
+ itemsClassName?: string
+ children: React.ReactElement | React.ReactElement[]
+ }
+
+type UncontrolledChildrenModeMotionHighlightProps = BaseMotionHighlightProps & {
+ mode?: 'children'
+ controlledItems?: false
+ itemsClassName?: string
+ children: React.ReactElement | React.ReactElement[]
+}
+
+type MotionHighlightProps = React.ComponentProps<'div'> &
+ (
+ | ControlledParentModeMotionHighlightProps
+ | ControlledChildrenModeMotionHighlightProps
+ | UncontrolledParentModeMotionHighlightProps
+ | UncontrolledChildrenModeMotionHighlightProps
+ )
+
+function MotionHighlight({ ref, ...props }: MotionHighlightProps) {
+ const {
+ children,
+ value,
+ defaultValue,
+ onValueChange,
+ className,
+ transition = { type: 'spring', stiffness: 350, damping: 35 },
+ hover = false,
+ enabled = true,
+ controlledItems,
+ disabled = false,
+ exitDelay = 0.2,
+ mode = 'children'
+ } = props
+
+ const localRef = React.useRef(null)
+
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
+
+ const [activeValue, setActiveValue] = React.useState(value ?? defaultValue ?? null)
+ const [boundsState, setBoundsState] = React.useState(null)
+ const [activeClassNameState, setActiveClassNameState] = React.useState('')
+
+ const safeSetActiveValue = React.useCallback(
+ (id: T | null) => {
+ setActiveValue(prev => (prev === id ? prev : id))
+ if (id !== activeValue) onValueChange?.(id as T)
+ },
+ [activeValue, onValueChange]
+ )
+
+ const safeSetBounds = React.useCallback(
+ (bounds: DOMRect) => {
+ if (!localRef.current) return
+
+ const boundsOffset = (props as ParentModeMotionHighlightProps)?.boundsOffset ?? {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0
+ }
+
+ const containerRect = localRef.current.getBoundingClientRect()
+
+ const newBounds: Bounds = {
+ top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),
+ left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),
+ width: bounds.width + (boundsOffset.width ?? 0),
+ height: bounds.height + (boundsOffset.height ?? 0)
+ }
+
+ setBoundsState(prev => {
+ if (
+ prev &&
+ prev.top === newBounds.top &&
+ prev.left === newBounds.left &&
+ prev.width === newBounds.width &&
+ prev.height === newBounds.height
+ ) {
+ return prev
+ }
+
+ return newBounds
+ })
+ },
+ [props]
+ )
+
+ const clearBounds = React.useCallback(() => {
+ setBoundsState(prev => (prev === null ? prev : null))
+ }, [])
+
+ React.useEffect(() => {
+ if (value !== undefined) setActiveValue(value)
+ else if (defaultValue !== undefined) setActiveValue(defaultValue)
+ }, [value, defaultValue])
+
+ const id = React.useId()
+
+ React.useEffect(() => {
+ if (mode !== 'parent') return
+ const container = localRef.current
+
+ if (!container) return
+
+ const onScroll = () => {
+ if (!activeValue) return
+ const activeEl = container.querySelector(`[data-value="${activeValue}"][data-highlight="true"]`)
+
+ if (activeEl) safeSetBounds(activeEl.getBoundingClientRect())
+ }
+
+ container.addEventListener('scroll', onScroll, { passive: true })
+
+ return () => container.removeEventListener('scroll', onScroll)
+ }, [mode, activeValue, safeSetBounds])
+
+ const render = React.useCallback(
+ (children: React.ReactNode) => {
+ if (mode === 'parent') {
+ return (
+
+
+ {boundsState && (
+
+ )}
+
+ {children}
+
+ )
+ }
+
+ return children
+ },
+ [mode, props, boundsState, transition, exitDelay, className, activeClassNameState]
+ )
+
+ return (
+
+ {enabled
+ ? controlledItems
+ ? render(children)
+ : render(
+ React.Children.map(children, (child, index) => (
+
+ {child}
+
+ ))
+ )
+ : children}
+
+ )
+}
+
+function getNonOverridingDataAttributes(
+ element: React.ReactElement,
+ dataAttributes: Record
+): Record {
+ return Object.keys(dataAttributes).reduce>((acc, key) => {
+ if ((element.props as Record)[key] === undefined) {
+ acc[key] = dataAttributes[key]
+ }
+
+ return acc
+ }, {})
+}
+
+type ExtendedChildProps = React.ComponentProps<'div'> & {
+ id?: string
+ ref?: React.Ref
+ 'data-active'?: string
+ 'data-value'?: string
+ 'data-disabled'?: boolean
+ 'data-highlight'?: boolean
+ 'data-slot'?: string
+}
+
+type MotionHighlightItemProps = React.ComponentProps<'div'> & {
+ children: React.ReactElement
+ id?: string
+ value?: string
+ className?: string
+ transition?: Transition
+ activeClassName?: string
+ disabled?: boolean
+ exitDelay?: number
+ asChild?: boolean
+ forceUpdateBounds?: boolean
+}
+
+function MotionHighlightItem({
+ ref,
+ children,
+ id,
+ value,
+ className,
+ transition,
+ disabled = false,
+ activeClassName,
+ exitDelay,
+ asChild = false,
+ forceUpdateBounds,
+ ...props
+}: MotionHighlightItemProps) {
+ const itemId = React.useId()
+
+ const {
+ activeValue,
+ setActiveValue,
+ mode,
+ setBounds,
+ clearBounds,
+ hover,
+ enabled,
+ className: contextClassName,
+ transition: contextTransition,
+ id: contextId,
+ disabled: contextDisabled,
+ exitDelay: contextExitDelay,
+ forceUpdateBounds: contextForceUpdateBounds,
+ setActiveClassName
+ } = useMotionHighlight()
+
+ const element = children as React.ReactElement
+ const childValue = id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId
+ const isActive = activeValue === childValue
+ const isDisabled = disabled === undefined ? contextDisabled : disabled
+ const itemTransition = transition ?? contextTransition
+
+ const localRef = React.useRef(null)
+
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
+
+ React.useEffect(() => {
+ if (mode !== 'parent') return
+ let rafId: number
+ let previousBounds: Bounds | null = null
+ const shouldUpdateBounds = forceUpdateBounds === true || (contextForceUpdateBounds && forceUpdateBounds !== false)
+
+ const updateBounds = () => {
+ if (!localRef.current) return
+
+ const bounds = localRef.current.getBoundingClientRect()
+
+ if (shouldUpdateBounds) {
+ if (
+ previousBounds &&
+ previousBounds.top === bounds.top &&
+ previousBounds.left === bounds.left &&
+ previousBounds.width === bounds.width &&
+ previousBounds.height === bounds.height
+ ) {
+ rafId = requestAnimationFrame(updateBounds)
+
+ return
+ }
+
+ previousBounds = bounds
+ rafId = requestAnimationFrame(updateBounds)
+ }
+
+ setBounds(bounds)
+ }
+
+ if (isActive) {
+ updateBounds()
+ setActiveClassName(activeClassName ?? '')
+ } else if (!activeValue) clearBounds()
+
+ if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId)
+ }, [
+ mode,
+ isActive,
+ activeValue,
+ setBounds,
+ clearBounds,
+ activeClassName,
+ setActiveClassName,
+ forceUpdateBounds,
+ contextForceUpdateBounds
+ ])
+
+ if (!React.isValidElement(children)) return children
+
+ const dataAttributes = {
+ 'data-active': isActive ? 'true' : 'false',
+ 'aria-selected': isActive,
+ 'data-disabled': isDisabled,
+ 'data-value': childValue,
+ 'data-highlight': true
+ }
+
+ const commonHandlers = hover
+ ? {
+ onMouseEnter: (e: React.MouseEvent) => {
+ setActiveValue(childValue)
+ element.props.onMouseEnter?.(e)
+ },
+ onMouseLeave: (e: React.MouseEvent) => {
+ setActiveValue(null)
+ element.props.onMouseLeave?.(e)
+ }
+ }
+ : {
+ onClick: (e: React.MouseEvent) => {
+ setActiveValue(childValue)
+ element.props.onClick?.(e)
+ }
+ }
+
+ if (asChild) {
+ if (mode === 'children') {
+ return React.cloneElement(
+ element,
+ {
+ key: childValue,
+ ref: localRef,
+ className: cn('relative', element.props.className),
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ 'data-slot': 'motion-highlight-item-container'
+ }),
+ ...commonHandlers,
+ ...props
+ },
+ <>
+
+ {isActive && !isDisabled && (
+
+ )}
+
+
+
+ {children}
+
+ >
+ )
+ }
+
+ return React.cloneElement(element, {
+ ref: localRef,
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ 'data-slot': 'motion-highlight-item'
+ }),
+ ...commonHandlers
+ })
+ }
+
+ return enabled ? (
+
+ {mode === 'children' && (
+
+ {isActive && !isDisabled && (
+
+ )}
+
+ )}
+
+ {React.cloneElement(element, {
+ className: cn('relative z-[1]', element.props.className),
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ 'data-slot': 'motion-highlight-item'
+ })
+ })}
+
+ ) : (
+ children
+ )
+}
+
+export {
+ MotionHighlight,
+ MotionHighlightItem,
+ useMotionHighlight,
+ type MotionHighlightProps,
+ type MotionHighlightItemProps
+}
diff --git a/src/components/ui/motion-tabs.tsx b/src/components/ui/motion-tabs.tsx
new file mode 100644
index 0000000..df6502e
--- /dev/null
+++ b/src/components/ui/motion-tabs.tsx
@@ -0,0 +1,261 @@
+'use client'
+
+import * as React from 'react'
+
+import { motion, type Transition, type HTMLMotionProps } from 'motion/react'
+
+import { cn } from '@/lib/utils'
+import { MotionHighlight, MotionHighlightItem } from '@/components/ui/motion-highlight'
+
+type TabsContextType = {
+ activeValue: T
+ handleValueChange: (value: T) => void
+ registerTrigger: (value: T, node: HTMLElement | null) => void
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const TabsContext = React.createContext | undefined>(undefined)
+
+function useTabs(): TabsContextType {
+ const context = React.useContext(TabsContext)
+
+ if (!context) {
+ throw new Error('useTabs must be used within a TabsProvider')
+ }
+
+ return context
+}
+
+type BaseTabsProps = React.ComponentProps<'div'> & {
+ children: React.ReactNode
+}
+
+type UnControlledTabsProps = BaseTabsProps & {
+ defaultValue?: T
+ value?: never
+ onValueChange?: never
+}
+
+type ControlledTabsProps = BaseTabsProps & {
+ value: T
+ onValueChange?: (value: T) => void
+ defaultValue?: never
+}
+
+type TabsProps = UnControlledTabsProps | ControlledTabsProps
+
+function Tabs({
+ defaultValue,
+ value,
+ onValueChange,
+ children,
+ className,
+ ...props
+}: TabsProps) {
+ const [activeValue, setActiveValue] = React.useState(defaultValue ?? undefined)
+ const triggersRef = React.useRef(new Map())
+ const initialSet = React.useRef(false)
+ const isControlled = value !== undefined
+
+ React.useEffect(() => {
+ if (!isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current) {
+ const firstTab = Array.from(triggersRef.current.keys())[0]
+
+ setActiveValue(firstTab as T)
+ initialSet.current = true
+ }
+ }, [activeValue, isControlled])
+
+ const registerTrigger = (value: string, node: HTMLElement | null) => {
+ if (node) {
+ triggersRef.current.set(value, node)
+
+ if (!isControlled && activeValue === undefined && !initialSet.current) {
+ setActiveValue(value as T)
+ initialSet.current = true
+ }
+ } else {
+ triggersRef.current.delete(value)
+ }
+ }
+
+ const handleValueChange = (val: T) => {
+ if (!isControlled) setActiveValue(val)
+ else onValueChange?.(val)
+ }
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+type TabsListProps = React.ComponentProps<'div'> & {
+ children: React.ReactNode
+ activeClassName?: string
+ transition?: Transition
+}
+
+function TabsList({
+ children,
+ className,
+ activeClassName,
+ transition = {
+ type: 'spring',
+ stiffness: 200,
+ damping: 25
+ },
+ ...props
+}: TabsListProps) {
+ const { activeValue } = useTabs()
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+type TabsTriggerProps = HTMLMotionProps<'button'> & {
+ value: string
+ children: React.ReactNode
+}
+
+function TabsTrigger({ ref, value, children, className, ...props }: TabsTriggerProps) {
+ const { activeValue, handleValueChange, registerTrigger } = useTabs()
+
+ const localRef = React.useRef(null)
+
+ React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement)
+
+ React.useEffect(() => {
+ registerTrigger(value, localRef.current)
+
+ return () => registerTrigger(value, null)
+ }, [value, registerTrigger])
+
+ return (
+
+ handleValueChange(value)}
+ data-state={activeValue === value ? 'active' : 'inactive'}
+ className={cn(
+ 'ring-offset-background focus-visible:ring-ring data-[state=active]:text-foreground z-[1] inline-flex size-full cursor-pointer items-center justify-center rounded-sm px-2 py-1 text-sm font-medium whitespace-nowrap transition-transform focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+ )
+}
+
+type TabsContentsProps = React.ComponentProps<'div'> & {
+ children: React.ReactNode
+ transition?: Transition
+}
+
+function TabsContents({
+ children,
+ className,
+ transition = {
+ type: 'spring',
+ stiffness: 300,
+ damping: 30,
+ bounce: 0,
+ restDelta: 0.01
+ },
+ ...props
+}: TabsContentsProps) {
+ const { activeValue } = useTabs()
+ const childrenArray = React.Children.toArray(children)
+
+ const activeIndex = childrenArray.findIndex(
+ (child): child is React.ReactElement<{ value: string }> =>
+ React.isValidElement(child) &&
+ typeof child.props === 'object' &&
+ child.props !== null &&
+ 'value' in child.props &&
+ child.props.value === activeValue
+ )
+
+ return (
+
+
+ {childrenArray.map((child, index) => (
+
+ {child}
+
+ ))}
+
+
+ )
+}
+
+type TabsContentProps = HTMLMotionProps<'div'> & {
+ value: string
+ children: React.ReactNode
+}
+
+function TabsContent({ children, value, className, ...props }: TabsContentProps) {
+ const { activeValue } = useTabs()
+ const isActive = activeValue === value
+
+ return (
+
+ {children}
+
+ )
+}
+
+export {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ TabsContents,
+ TabsContent,
+ useTabs,
+ type TabsContextType,
+ type TabsProps,
+ type TabsListProps,
+ type TabsTriggerProps,
+ type TabsContentsProps,
+ type TabsContentProps
+}
diff --git a/src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx b/src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
deleted file mode 100644
index ba66cb1..0000000
--- a/src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { useNavigate } from '@tanstack/react-router'
-
-import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
-
-import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
-import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
-import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
-import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
-import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
-import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
-import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
-import { defineStepper } from '@/components/stepper'
-import { Dialog, DialogContent } from '@/components/ui/dialog'
-
-const Wizard = defineStepper(
- {
- id: 'metodo',
- title: 'Método',
- description: 'Manual, IA o Clonado',
- },
- {
- id: 'basicos',
- title: 'Datos básicos',
- description: 'Nombre y estructura',
- },
- {
- id: 'configuracion',
- title: 'Configuración',
- description: 'Detalles según modo',
- },
- {
- id: 'resumen',
- title: 'Resumen',
- description: 'Confirmar creación',
- },
-)
-
-const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
-
-export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
- const navigate = useNavigate()
- const role = auth_get_current_user_role()
-
- const {
- wizard,
- setWizard,
- canContinueDesdeMetodo,
- canContinueDesdeBasicos,
- canContinueDesdeConfig,
- simularGeneracionIA,
- crearAsignatura,
- } = useNuevaAsignaturaWizard(planId)
-
- const handleClose = () => {
- navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
- }
-
- return (
-
- )
-}
diff --git a/src/features/asignaturas/new/catalogs.ts b/src/features/asignaturas/new/catalogs.ts
deleted file mode 100644
index 97ea60a..0000000
--- a/src/features/asignaturas/new/catalogs.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { TipoAsignatura } from "./types";
-
-export const ESTRUCTURAS_SEP = [
- { id: "sep-lic-2025", label: "Licenciatura SEP v2025" },
- { id: "sep-pos-2023", label: "Posgrado SEP v2023" },
- { id: "ulsa-int-2024", label: "Estándar Interno ULSA 2024" },
-];
-
-export const TIPOS_MATERIA: Array<{ value: TipoAsignatura; label: string }> = [
- { value: "OBLIGATORIA", label: "Obligatoria" },
- { value: "OPTATIVA", label: "Optativa" },
- { value: "TRONCAL", label: "Troncal / Eje común" },
- { value: "OTRO", label: "Otro" },
-];
-
-export const FACULTADES = [
- { id: "ing", nombre: "Facultad de Ingeniería" },
- { id: "med", nombre: "Facultad de Medicina" },
- { id: "neg", nombre: "Facultad de Negocios" },
-];
-
-export const CARRERAS = [
- { id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
- { id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
- { id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
- { id: "act", nombre: "Actuaría", facultadId: "neg" },
-];
-
-export const PLANES_MOCK = [
- { id: "p1", nombre: "Plan 2010 Sistemas", carreraId: "sis" },
- { id: "p2", nombre: "Plan 2016 Sistemas", carreraId: "sis" },
- { id: "p3", nombre: "Plan 2015 Industrial", carreraId: "ind" },
-];
-
-export const MATERIAS_MOCK = [
- {
- id: "m1",
- nombre: "Programación Orientada a Objetos",
- creditos: 8,
- clave: "POO-101",
- },
- { id: "m2", nombre: "Cálculo Diferencial", creditos: 6, clave: "MAT-101" },
- { id: "m3", nombre: "Ética Profesional", creditos: 4, clave: "HUM-302" },
- {
- id: "m4",
- nombre: "Bases de Datos Avanzadas",
- creditos: 8,
- clave: "BD-201",
- },
-];
-
-export const ARCHIVOS_SISTEMA_MOCK = [
- { id: "doc1", name: "Sílabo_Base_Ingenieria.pdf" },
- { id: "doc2", name: "Competencias_Egreso_2025.docx" },
- { id: "doc3", name: "Reglamento_Academico.pdf" },
-];
diff --git a/src/features/asignaturas/new/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/new/hooks/useNuevaAsignaturaWizard.ts
deleted file mode 100644
index dd5c14c..0000000
--- a/src/features/asignaturas/new/hooks/useNuevaAsignaturaWizard.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { useState } from "react";
-
-import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
-
-export function useNuevaAsignaturaWizard(planId: string) {
- const [wizard, setWizard] = useState({
- step: 1,
- planId,
- modoCreacion: null,
- datosBasicos: {
- nombre: "",
- clave: "",
- tipo: "OBLIGATORIA",
- creditos: 0,
- horasSemana: 0,
- estructuraId: "",
- },
- clonInterno: {},
- clonTradicional: {
- archivoWordAsignaturaId: null,
- archivosAdicionalesIds: [],
- },
- iaConfig: {
- descripcionEnfoque: "",
- notasAdicionales: "",
- archivosExistentesIds: [],
- },
- resumen: {},
- isLoading: false,
- errorMessage: null,
- });
-
- const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
- wizard.modoCreacion === "IA" ||
- (wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
-
- const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
- wizard.datosBasicos.creditos > 0 &&
- !!wizard.datosBasicos.estructuraId;
-
- const canContinueDesdeConfig = (() => {
- if (wizard.modoCreacion === "MANUAL") return true;
- if (wizard.modoCreacion === "IA") {
- return !!wizard.iaConfig?.descripcionEnfoque;
- }
- if (wizard.modoCreacion === "CLONADO") {
- if (wizard.subModoClonado === "INTERNO") {
- return !!wizard.clonInterno?.asignaturaOrigenId;
- }
- if (wizard.subModoClonado === "TRADICIONAL") {
- return !!wizard.clonTradicional?.archivoWordAsignaturaId;
- }
- }
- return false;
- })();
-
- const simularGeneracionIA = async () => {
- setWizard((w) => ({ ...w, isLoading: true }));
- await new Promise((r) => setTimeout(r, 1500));
- setWizard((w) => ({
- ...w,
- isLoading: false,
- resumen: {
- previewAsignatura: {
- nombre: w.datosBasicos.nombre,
- objetivo:
- "Aplicar los fundamentos teóricos para la resolución de problemas...",
- unidades: 5,
- bibliografiaCount: 3,
- } as AsignaturaPreview,
- },
- }));
- };
-
- const crearAsignatura = async (onCreated: () => void) => {
- setWizard((w) => ({ ...w, isLoading: true }));
- await new Promise((r) => setTimeout(r, 1000));
- onCreated();
- };
-
- return {
- wizard,
- setWizard,
- canContinueDesdeMetodo,
- canContinueDesdeBasicos,
- canContinueDesdeConfig,
- simularGeneracionIA,
- crearAsignatura,
- };
-}
diff --git a/src/features/asignaturas/new/types.ts b/src/features/asignaturas/new/types.ts
deleted file mode 100644
index 799b0d5..0000000
--- a/src/features/asignaturas/new/types.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
-export type SubModoClonado = "INTERNO" | "TRADICIONAL";
-export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
-
-export type AsignaturaPreview = {
- nombre: string;
- objetivo: string;
- unidades: number;
- bibliografiaCount: number;
-};
-
-export type NewSubjectWizardState = {
- step: 1 | 2 | 3 | 4;
- planId: string;
- modoCreacion: ModoCreacion | null;
- subModoClonado?: SubModoClonado;
- datosBasicos: {
- nombre: string;
- clave?: string;
- tipo: TipoAsignatura;
- creditos: number;
- horasSemana?: number;
- estructuraId: string;
- };
- clonInterno?: {
- facultadId?: string;
- carreraId?: string;
- planOrigenId?: string;
- asignaturaOrigenId?: string | null;
- };
- clonTradicional?: {
- archivoWordAsignaturaId: string | null;
- archivosAdicionalesIds: Array;
- };
- iaConfig?: {
- descripcionEnfoque: string;
- notasAdicionales: string;
- archivosExistentesIds: Array;
- };
- resumen: {
- previewAsignatura?: AsignaturaPreview;
- };
- isLoading: boolean;
- errorMessage: string | null;
-};
diff --git a/src/features/planes/new/NuevoPlanModalContainer.tsx b/src/features/planes/new/NuevoPlanModalContainer.tsx
deleted file mode 100644
index 8f985da..0000000
--- a/src/features/planes/new/NuevoPlanModalContainer.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { useNavigate } from '@tanstack/react-router'
-import * as Icons from 'lucide-react'
-
-import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
-
-import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm'
-import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel'
-import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
-import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard'
-import { WizardControls } from '@/components/planes/wizard/WizardControls'
-import { WizardHeader } from '@/components/planes/wizard/WizardHeader'
-import { defineStepper } from '@/components/stepper'
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card'
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-
-// Mock de permisos/rol
-const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
-
-const Wizard = defineStepper(
- {
- id: 'modo',
- title: 'Método',
- description: 'Selecciona cómo crearás el plan',
- },
- {
- id: 'basicos',
- title: 'Datos básicos',
- description: 'Nombre, carrera, nivel y ciclos',
- },
- { id: 'detalles', title: 'Detalles', description: 'IA, clonado o archivos' },
- { id: 'resumen', title: 'Resumen', description: 'Confirma y crea el plan' },
-)
-
-export default function NuevoPlanModalContainer() {
- const navigate = useNavigate()
- const role = auth_get_current_user_role()
-
- const {
- wizard,
- setWizard,
- carrerasFiltradas,
- canContinueDesdeModo,
- canContinueDesdeBasicos,
- canContinueDesdeDetalles,
- generarPreviewIA,
- } = useNuevoPlanWizard()
-
- const handleClose = () => {
- navigate({ to: '/planes', resetScroll: false })
- }
-
- const crearPlan = async () => {
- setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
- await new Promise((r) => setTimeout(r, 900))
- const nuevoId = (() => {
- if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001'
- if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001'
- if (wizard.subModoClonado === 'INTERNO') return 'plan_new_clone_001'
- return 'plan_new_import_001'
- })()
- navigate({ to: `/planes/${nuevoId}` })
- }
-
- return (
-
- )
-}
diff --git a/src/features/planes/new/catalogs.ts b/src/features/planes/new/catalogs.ts
deleted file mode 100644
index 1298106..0000000
--- a/src/features/planes/new/catalogs.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { TipoCiclo } from "./types";
-
-export const FACULTADES = [
- { id: "ing", nombre: "Facultad de Ingeniería" },
- {
- id: "med",
- nombre: "Facultad de Medicina en medicina en medicina en medicina",
- },
- { id: "neg", nombre: "Facultad de Negocios" },
-];
-
-export const CARRERAS = [
- { id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
- { id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
- { id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
- { id: "act", nombre: "Actuaría", facultadId: "neg" },
-];
-
-export const NIVELES = [
- "Licenciatura",
- "Especialidad",
- "Maestría",
- "Doctorado",
-];
-export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [
- { value: "SEMESTRE", label: "Semestre" },
- { value: "CUATRIMESTRE", label: "Cuatrimestre" },
- { value: "TRIMESTRE", label: "Trimestre" },
-];
-
-export const PLANES_EXISTENTES = [
- {
- id: "plan-2021-sis",
- nombre: "ISC 2021",
- estado: "Aprobado",
- anio: 2021,
- facultadId: "ing",
- carreraId: "sis",
- },
- {
- id: "plan-2020-ind",
- nombre: "I. Industrial 2020",
- estado: "Aprobado",
- anio: 2020,
- facultadId: "ing",
- carreraId: "ind",
- },
- {
- id: "plan-2019-med",
- nombre: "Medicina 2019",
- estado: "Vigente",
- anio: 2019,
- facultadId: "med",
- carreraId: "medico",
- },
-];
diff --git a/src/features/planes/new/hooks/useNuevoPlanWizard.ts b/src/features/planes/new/hooks/useNuevoPlanWizard.ts
deleted file mode 100644
index ea20d55..0000000
--- a/src/features/planes/new/hooks/useNuevoPlanWizard.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useMemo, useState } from "react";
-
-import { CARRERAS } from "../catalogs";
-
-import type { NewPlanWizardState, PlanPreview } from "../types";
-
-export function useNuevoPlanWizard() {
- const [wizard, setWizard] = useState({
- step: 1,
- modoCreacion: null,
- datosBasicos: {
- nombrePlan: "",
- carreraId: "",
- facultadId: "",
- nivel: "",
- tipoCiclo: "SEMESTRE",
- numCiclos: 8,
- },
- clonInterno: { planOrigenId: null },
- clonTradicional: {
- archivoWordPlanId: null,
- archivoMapaExcelId: null,
- archivoAsignaturasExcelId: null,
- },
- iaConfig: {
- descripcionEnfoque: "",
- poblacionObjetivo: "",
- notasAdicionales: "",
- archivosReferencia: [],
- },
- resumen: {},
- isLoading: false,
- errorMessage: null,
- });
-
- const carrerasFiltradas = useMemo(() => {
- const fac = wizard.datosBasicos.facultadId;
- return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS;
- }, [wizard.datosBasicos.facultadId]);
-
- const canContinueDesdeModo = wizard.modoCreacion === "MANUAL" ||
- wizard.modoCreacion === "IA" ||
- (wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
-
- const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan &&
- !!wizard.datosBasicos.carreraId &&
- !!wizard.datosBasicos.facultadId &&
- !!wizard.datosBasicos.nivel &&
- wizard.datosBasicos.numCiclos > 0;
-
- const canContinueDesdeDetalles = (() => {
- if (wizard.modoCreacion === "MANUAL") return true;
- if (wizard.modoCreacion === "IA") {
- return !!wizard.iaConfig?.descripcionEnfoque;
- }
- if (wizard.modoCreacion === "CLONADO") {
- if (wizard.subModoClonado === "INTERNO") {
- return !!wizard.clonInterno?.planOrigenId;
- }
- if (wizard.subModoClonado === "TRADICIONAL") {
- const t = wizard.clonTradicional;
- if (!t) return false;
- const tieneWord = !!t.archivoWordPlanId;
- const tieneAlMenosUnExcel = !!t.archivoMapaExcelId ||
- !!t.archivoAsignaturasExcelId;
- return tieneWord && tieneAlMenosUnExcel;
- }
- }
- return false;
- })();
-
- const generarPreviewIA = async () => {
- setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }));
- await new Promise((r) => setTimeout(r, 800));
- const preview: PlanPreview = {
- nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
- nivel: wizard.datosBasicos.nivel || "Licenciatura",
- tipoCiclo: wizard.datosBasicos.tipoCiclo,
- numCiclos: wizard.datosBasicos.numCiclos,
- numAsignaturasAprox: wizard.datosBasicos.numCiclos * 6,
- secciones: [
- { id: "obj", titulo: "Objetivos", resumen: "Borrador de objetivos…" },
- { id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" },
- ],
- };
- setWizard((w) => ({
- ...w,
- isLoading: false,
- resumen: { previewPlan: preview },
- }));
- };
-
- return {
- wizard,
- setWizard,
- carrerasFiltradas,
- canContinueDesdeModo,
- canContinueDesdeBasicos,
- canContinueDesdeDetalles,
- generarPreviewIA,
- };
-}
diff --git a/src/features/planes/new/types.ts b/src/features/planes/new/types.ts
deleted file mode 100644
index c07a6fa..0000000
--- a/src/features/planes/new/types.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE";
-export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
-export type SubModoClonado = "INTERNO" | "TRADICIONAL";
-
-export type PlanPreview = {
- nombrePlan: string;
- nivel: string;
- tipoCiclo: TipoCiclo;
- numCiclos: number;
- numAsignaturasAprox?: number;
- secciones?: Array<{ id: string; titulo: string; resumen: string }>;
-};
-
-export type NewPlanWizardState = {
- step: 1 | 2 | 3 | 4;
- modoCreacion: ModoCreacion | null;
- subModoClonado?: SubModoClonado;
- datosBasicos: {
- nombrePlan: string;
- carreraId: string;
- facultadId: string;
- nivel: string;
- tipoCiclo: TipoCiclo;
- numCiclos: number;
- };
- clonInterno?: { planOrigenId: string | null };
- clonTradicional?: {
- archivoWordPlanId: string | null;
- archivoMapaExcelId: string | null;
- archivoAsignaturasExcelId: string | null;
- };
- iaConfig?: {
- descripcionEnfoque: string;
- poblacionObjetivo: string;
- notasAdicionales: string;
- archivosReferencia: Array;
- };
- resumen: { previewPlan?: PlanPreview };
- isLoading: boolean;
- errorMessage: string | null;
-};
diff --git a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts
index b94d332..5bc8ece 100644
--- a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts
+++ b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts
@@ -9,12 +9,12 @@ export function useNuevoPlanWizard() {
step: 1,
modoCreacion: null,
datosBasicos: {
- nombrePlan: "",
- carreraId: "",
- facultadId: "",
- nivel: "",
- tipoCiclo: "",
- numCiclos: undefined,
+ nombrePlan: "Medicina",
+ carreraId: "medico",
+ facultadId: "med",
+ nivel: "Licenciatura",
+ tipoCiclo: "SEMESTRE",
+ numCiclos: 8,
},
clonInterno: { planOrigenId: null },
clonTradicional: {
diff --git a/src/routes/planes/$planId/asignaturas/_lista/nueva.tsx b/src/routes/planes/$planId/asignaturas/_lista/nueva.tsx
index 9d49955..e5c797b 100644
--- a/src/routes/planes/$planId/asignaturas/_lista/nueva.tsx
+++ b/src/routes/planes/$planId/asignaturas/_lista/nueva.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
-import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/new/NuevaAsignaturaModalContainer'
+import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/nueva/NuevaAsignaturaModalContainer'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/_lista/nueva',
diff --git a/src/routes/planes/_lista/nuevo.tsx b/src/routes/planes/_lista/nuevo.tsx
index 82c2cdd..50cfa2b 100644
--- a/src/routes/planes/_lista/nuevo.tsx
+++ b/src/routes/planes/_lista/nuevo.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
-import NuevoPlanModalContainer from '@/features/planes/new/NuevoPlanModalContainer'
+import NuevoPlanModalContainer from '@/features/planes/nuevo/NuevoPlanModalContainer'
export const Route = createFileRoute('/planes/_lista/nuevo')({
component: NuevoPlanModalContainer,