Se corrigen incidencias 8 y 13
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
250
src/components/ui/context-menu.tsx
Normal file
250
src/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ContextMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
|
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
data-slot="context-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
data-slot="context-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
data-slot="context-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot="context-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="context-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
data-slot="context-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index'
|
|||||||
import { Route as PlanesPlanesListRouteRouteImport } from './routes/planes/PlanesListRoute'
|
import { Route as PlanesPlanesListRouteRouteImport } from './routes/planes/PlanesListRoute'
|
||||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||||
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
||||||
|
import { Route as PlanesPlanIdIndexRouteImport } from './routes/planes/$planId/index'
|
||||||
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
||||||
import { Route as PlanesPlanIdAsignaturasRouteRouteImport } from './routes/planes/$planId/asignaturas/route'
|
import { Route as PlanesPlanIdAsignaturasRouteRouteImport } from './routes/planes/$planId/asignaturas/route'
|
||||||
import { Route as PlanesPlanIdDetalleRouteRouteImport } from './routes/planes/$planId/_detalle/route'
|
import { Route as PlanesPlanIdDetalleRouteRouteImport } from './routes/planes/$planId/_detalle/route'
|
||||||
@@ -60,6 +61,11 @@ const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({
|
|||||||
path: '/planes',
|
path: '/planes',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const PlanesPlanIdIndexRoute = PlanesPlanIdIndexRouteImport.update({
|
||||||
|
id: '/planes/$planId/',
|
||||||
|
path: '/planes/$planId/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
||||||
id: '/nuevo',
|
id: '/nuevo',
|
||||||
path: '/nuevo',
|
path: '/nuevo',
|
||||||
@@ -152,6 +158,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
||||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/': typeof PlanesPlanIdIndexRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
'/planes/$planId/datos': typeof PlanesPlanIdDetalleDatosRoute
|
'/planes/$planId/datos': typeof PlanesPlanIdDetalleDatosRoute
|
||||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
@@ -170,7 +177,7 @@ export interface FileRoutesByTo {
|
|||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
|
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
|
||||||
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasIndexRoute
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasIndexRoute
|
||||||
@@ -194,6 +201,7 @@ export interface FileRoutesById {
|
|||||||
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
||||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
|
||||||
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/': typeof PlanesPlanIdIndexRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
'/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
'/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
'/planes/$planId/_detalle/datos': typeof PlanesPlanIdDetalleDatosRoute
|
'/planes/$planId/_detalle/datos': typeof PlanesPlanIdDetalleDatosRoute
|
||||||
@@ -218,6 +226,7 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/$planId/asignaturas'
|
| '/planes/$planId/asignaturas'
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId/'
|
||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/datos'
|
| '/planes/$planId/datos'
|
||||||
| '/planes/$planId/documento'
|
| '/planes/$planId/documento'
|
||||||
@@ -259,6 +268,7 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId/_detalle'
|
| '/planes/$planId/_detalle'
|
||||||
| '/planes/$planId/asignaturas'
|
| '/planes/$planId/asignaturas'
|
||||||
| '/planes/_lista/nuevo'
|
| '/planes/_lista/nuevo'
|
||||||
|
| '/planes/$planId/'
|
||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/asignaturas/_lista'
|
| '/planes/$planId/asignaturas/_lista'
|
||||||
| '/planes/$planId/_detalle/datos'
|
| '/planes/$planId/_detalle/datos'
|
||||||
@@ -281,6 +291,7 @@ export interface RootRouteChildren {
|
|||||||
PlanesPlanesListRouteRoute: typeof PlanesPlanesListRouteRoute
|
PlanesPlanesListRouteRoute: typeof PlanesPlanesListRouteRoute
|
||||||
PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren
|
||||||
PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
|
PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
|
||||||
|
PlanesPlanIdIndexRoute: typeof PlanesPlanIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -327,6 +338,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/planes/$planId/': {
|
||||||
|
id: '/planes/$planId/'
|
||||||
|
path: '/planes/$planId'
|
||||||
|
fullPath: '/planes/$planId/'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/planes/_lista/nuevo': {
|
'/planes/_lista/nuevo': {
|
||||||
id: '/planes/_lista/nuevo'
|
id: '/planes/_lista/nuevo'
|
||||||
path: '/nuevo'
|
path: '/nuevo'
|
||||||
@@ -510,6 +528,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren,
|
PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren,
|
||||||
PlanesPlanIdAsignaturasRouteRoute:
|
PlanesPlanIdAsignaturasRouteRoute:
|
||||||
PlanesPlanIdAsignaturasRouteRouteWithChildren,
|
PlanesPlanIdAsignaturasRouteRouteWithChildren,
|
||||||
|
PlanesPlanIdIndexRoute: PlanesPlanIdIndexRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function DatosGeneralesPage() {
|
|||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/iaplan',
|
to: '/planes/$planId/iaplan',
|
||||||
params: {
|
params: {
|
||||||
planId: '1', // o dinámico
|
planId: planId, // o dinámico
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
prefill: descripcion,
|
prefill: descripcion,
|
||||||
|
|||||||
@@ -5,12 +5,19 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Hash,
|
Hash,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Rocket,
|
Save,
|
||||||
BookOpen,
|
|
||||||
CheckCircle2,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/components/ui/context-menu'
|
||||||
|
import { usePlan } from '@/data/hooks/usePlans'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@@ -18,10 +25,55 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
|
const { data } = usePlan(planId)
|
||||||
|
|
||||||
|
// Estados locales para manejar la edición "en vivo" antes de persistir
|
||||||
|
const [nombrePlan, setNombrePlan] = useState('')
|
||||||
|
const [nivelPlan, setNivelPlan] = useState('')
|
||||||
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setNombrePlan(data.nombre || '')
|
||||||
|
setNivelPlan(data.nivel || '')
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const niveles = [
|
||||||
|
'Licenciatura',
|
||||||
|
'Maestría',
|
||||||
|
'Doctorado',
|
||||||
|
'Diplomado',
|
||||||
|
'Especialidad',
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault() // Evita el salto de línea
|
||||||
|
e.currentTarget.blur() // Quita el foco, lo que dispara el onBlur y "guarda" en el estado
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
console.log('Guardando en DB...', { nombrePlan, nivelPlan })
|
||||||
|
// Aquí iría tu mutation
|
||||||
|
setIsDirty(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* 1. Header Superior con Sombra (Volver a planes) */}
|
{/* Botón Flotante de Guardar */}
|
||||||
|
{isDirty && (
|
||||||
|
<div className="animate-in fade-in slide-in-from-bottom-4 fixed right-8 bottom-8 z-50 duration-300">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="gap-2 rounded-full bg-teal-600 px-6 shadow-xl hover:bg-teal-700"
|
||||||
|
>
|
||||||
|
<Save size={16} /> Guardar cambios del Plan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 1. Header Superior */}
|
||||||
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
||||||
<div className="px-6 py-2">
|
<div className="px-6 py-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -33,50 +85,70 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Contenido Principal con Padding */}
|
|
||||||
<div className="mx-auto max-w-[1600px] space-y-8 p-8">
|
<div className="mx-auto max-w-[1600px] space-y-8 p-8">
|
||||||
{/* Header del Plan y Badges */}
|
{/* Header del Plan */}
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
||||||
Plan de Estudios 2024
|
<span>{nivelPlan} en</span>
|
||||||
|
<span
|
||||||
|
role="textbox"
|
||||||
|
tabIndex={0}
|
||||||
|
contentEditable
|
||||||
|
suppressContentEditableWarning
|
||||||
|
spellCheck={false} // Quita el subrayado rojo de error ortográfico
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={(e) => setNombrePlan(e.currentTarget.textContent || '')}
|
||||||
|
className="cursor-text border-b border-transparent decoration-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500"
|
||||||
|
style={{ WebkitTextDecoration: 'none', textDecoration: 'none' }} // Doble seguridad contra subrayados
|
||||||
|
>
|
||||||
|
{nombrePlan}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-lg font-medium text-slate-500">
|
<p className="mt-1 text-lg font-medium text-slate-500">
|
||||||
Ingeniería en Sistemas Computacionales
|
{data?.carreras?.facultades?.nombre}{' '}
|
||||||
|
{data?.carreras?.nombre_corto}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Badges de la derecha */}
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{/* <Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
||||||
|
<CheckCircle2 size={12} /> {data?.estados_plan?.etiqueta}
|
||||||
|
</Badge> */}
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
className={`gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100`}
|
||||||
className="gap-1 border-blue-100 bg-blue-50 px-3 text-blue-700"
|
|
||||||
>
|
>
|
||||||
<Rocket size={12} /> Ingeniería
|
{data?.estados_plan?.etiqueta}
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="gap-1 border-orange-100 bg-orange-50 px-3 text-orange-700"
|
|
||||||
>
|
|
||||||
<BookOpen size={12} /> Licenciatura
|
|
||||||
</Badge>
|
|
||||||
<Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
|
||||||
<CheckCircle2 size={12} /> En Revisión
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Cards de Información (Nivel, Duración, etc.) */}
|
{/* 3. Cards de Información con Context Menu */}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuTrigger>
|
||||||
|
{/* Eliminamos el div extra y aplicamos el estilo directamente al trigger si es necesario,
|
||||||
|
pero con asChild, la InfoCard será el trigger real */}
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<GraduationCap className="text-slate-400" />}
|
icon={<GraduationCap className="text-slate-400" />}
|
||||||
label="Nivel"
|
label="Nivel"
|
||||||
value="Superior"
|
value={nivelPlan}
|
||||||
|
isEditable
|
||||||
/>
|
/>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-48">
|
||||||
|
{niveles.map((n) => (
|
||||||
|
<ContextMenuItem key={n} onClick={() => setNivelPlan(n)}>
|
||||||
|
{n}
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Clock className="text-slate-400" />}
|
icon={<Clock className="text-slate-400" />}
|
||||||
label="Duración"
|
label="Duración"
|
||||||
value="9 Semestres"
|
value={`${data?.numero_ciclos || 0} Ciclos`}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Hash className="text-slate-400" />}
|
icon={<Hash className="text-slate-400" />}
|
||||||
@@ -86,7 +158,7 @@ function RouteComponent() {
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<CalendarDays className="text-slate-400" />}
|
icon={<CalendarDays className="text-slate-400" />}
|
||||||
label="Creación"
|
label="Creación"
|
||||||
value="14 ene 2024"
|
value={data?.creado_en?.split('T')[0]} // Cortamos la fecha para que no sea tan larga
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,7 +189,6 @@ function RouteComponent() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. Contenido del Tab */}
|
|
||||||
<main className="animate-in fade-in pt-2 duration-500">
|
<main className="animate-in fade-in pt-2 duration-500">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
@@ -126,24 +197,37 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-componente para las tarjetas de información
|
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
isEditable,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string | number | undefined
|
||||||
|
isEditable?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm">
|
<div
|
||||||
<div className="rounded-lg border bg-white p-2 shadow-sm">{icon}</div>
|
className={`flex h-[72px] w-full items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm transition-all ${
|
||||||
<div>
|
isEditable
|
||||||
<p className="mb-1 text-[10px] leading-none font-bold tracking-wider text-slate-400 uppercase">
|
? 'cursor-context-menu hover:border-teal-200 hover:bg-white'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-white shadow-sm">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
{' '}
|
||||||
|
{/* min-w-0 es vital para que el truncate funcione en flex */}
|
||||||
|
<p className="mb-0.5 truncate text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
<p className="truncate text-sm font-semibold text-slate-700">
|
||||||
|
{value || '---'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -163,9 +247,7 @@ function Tab({
|
|||||||
to={to}
|
to={to}
|
||||||
params={params}
|
params={params}
|
||||||
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
||||||
activeProps={{
|
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
||||||
className: 'border-teal-600 text-teal-700 font-bold',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
Reference in New Issue
Block a user