@tleblancureta/proto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core-web/src/ProtoApp.tsx +163 -0
- package/core-web/src/components/Shell.tsx +276 -0
- package/core-web/src/components/shell/EmptyState.tsx +33 -0
- package/core-web/src/components/shell/FocusView.tsx +55 -0
- package/core-web/src/components/shell/Toolbar.tsx +233 -0
- package/core-web/src/components/shell/persistence.ts +20 -0
- package/core-web/src/components/shell/types.ts +14 -0
- package/core-web/src/components/ui/avatar.tsx +18 -0
- package/core-web/src/components/ui/badge.tsx +28 -0
- package/core-web/src/components/ui/button.tsx +40 -0
- package/core-web/src/components/ui/card.tsx +32 -0
- package/core-web/src/components/ui/inline-edit.tsx +120 -0
- package/core-web/src/components/ui/input.tsx +18 -0
- package/core-web/src/components/ui/scroll-area.tsx +12 -0
- package/core-web/src/components/ui/separator.tsx +23 -0
- package/core-web/src/components/ui/shell-dialog.tsx +79 -0
- package/core-web/src/components/ui/skeleton.tsx +9 -0
- package/core-web/src/components/ui/textarea.tsx +17 -0
- package/core-web/src/components/widgets/agent/Generative.tsx +74 -0
- package/core-web/src/components/widgets/agent/Primitives.tsx +225 -0
- package/core-web/src/components/widgets/agent/actions.ts +52 -0
- package/core-web/src/hooks/useAuth.ts +80 -0
- package/core-web/src/hooks/useData.ts +44 -0
- package/core-web/src/hooks/useMountEffect.ts +10 -0
- package/core-web/src/hooks/useTheme.ts +37 -0
- package/core-web/src/index.ts +52 -0
- package/core-web/src/lib/api.ts +231 -0
- package/core-web/src/lib/config.ts +14 -0
- package/core-web/src/lib/define-widget.ts +71 -0
- package/core-web/src/lib/drag.ts +45 -0
- package/core-web/src/lib/supabase.ts +6 -0
- package/core-web/src/lib/utils.ts +6 -0
- package/core-web/src/lib/widgetCache.ts +29 -0
- package/core-web/src/vite-env.d.ts +1 -0
- package/dist/core-mcp/src/app.d.ts +40 -0
- package/dist/core-mcp/src/app.d.ts.map +1 -0
- package/dist/core-mcp/src/app.js +141 -0
- package/dist/core-mcp/src/app.js.map +1 -0
- package/dist/core-mcp/src/define-tool.d.ts +70 -0
- package/dist/core-mcp/src/define-tool.d.ts.map +1 -0
- package/dist/core-mcp/src/define-tool.js +38 -0
- package/dist/core-mcp/src/define-tool.js.map +1 -0
- package/dist/core-mcp/src/entity-tools.d.ts +27 -0
- package/dist/core-mcp/src/entity-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/entity-tools.js +99 -0
- package/dist/core-mcp/src/entity-tools.js.map +1 -0
- package/dist/core-mcp/src/index.d.ts +36 -0
- package/dist/core-mcp/src/index.d.ts.map +1 -0
- package/dist/core-mcp/src/index.js +116 -0
- package/dist/core-mcp/src/index.js.map +1 -0
- package/dist/core-mcp/src/supabase.d.ts +7 -0
- package/dist/core-mcp/src/supabase.d.ts.map +1 -0
- package/dist/core-mcp/src/supabase.js +18 -0
- package/dist/core-mcp/src/supabase.js.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts +44 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.js +23 -0
- package/dist/core-mcp/src/tools/_helpers.js.map +1 -0
- package/dist/core-mcp/src/tools/ui.d.ts +9 -0
- package/dist/core-mcp/src/tools/ui.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/ui.js +100 -0
- package/dist/core-mcp/src/tools/ui.js.map +1 -0
- package/dist/core-mcp/src/workflow-tools.d.ts +41 -0
- package/dist/core-mcp/src/workflow-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/workflow-tools.js +382 -0
- package/dist/core-mcp/src/workflow-tools.js.map +1 -0
- package/dist/core-shared/src/define-entity.d.ts +73 -0
- package/dist/core-shared/src/define-entity.d.ts.map +1 -0
- package/dist/core-shared/src/define-entity.js +47 -0
- package/dist/core-shared/src/define-entity.js.map +1 -0
- package/dist/core-shared/src/define-workflow.d.ts +111 -0
- package/dist/core-shared/src/define-workflow.d.ts.map +1 -0
- package/dist/core-shared/src/define-workflow.js +92 -0
- package/dist/core-shared/src/define-workflow.js.map +1 -0
- package/dist/core-shared/src/index.d.ts +5 -0
- package/dist/core-shared/src/index.d.ts.map +1 -0
- package/dist/core-shared/src/index.js +7 -0
- package/dist/core-shared/src/index.js.map +1 -0
- package/dist/core-shared/src/scheduling.d.ts +69 -0
- package/dist/core-shared/src/scheduling.d.ts.map +1 -0
- package/dist/core-shared/src/scheduling.js +39 -0
- package/dist/core-shared/src/scheduling.js.map +1 -0
- package/dist/core-shared/src/schemas.d.ts +51 -0
- package/dist/core-shared/src/schemas.d.ts.map +1 -0
- package/dist/core-shared/src/schemas.js +18 -0
- package/dist/core-shared/src/schemas.js.map +1 -0
- package/dist/core-web/src/ProtoApp.d.ts +19 -0
- package/dist/core-web/src/ProtoApp.d.ts.map +1 -0
- package/dist/core-web/src/ProtoApp.js +92 -0
- package/dist/core-web/src/ProtoApp.js.map +1 -0
- package/dist/core-web/src/components/Shell.d.ts +46 -0
- package/dist/core-web/src/components/Shell.d.ts.map +1 -0
- package/dist/core-web/src/components/Shell.js +104 -0
- package/dist/core-web/src/components/Shell.js.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts +13 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.js +7 -0
- package/dist/core-web/src/components/shell/EmptyState.js.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts +16 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.js +12 -0
- package/dist/core-web/src/components/shell/FocusView.js.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts +35 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.js +42 -0
- package/dist/core-web/src/components/shell/Toolbar.js.map +1 -0
- package/dist/core-web/src/components/shell/persistence.d.ts +8 -0
- package/dist/core-web/src/components/shell/persistence.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/persistence.js +20 -0
- package/dist/core-web/src/components/shell/persistence.js.map +1 -0
- package/dist/core-web/src/components/shell/types.d.ts +13 -0
- package/dist/core-web/src/components/shell/types.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/types.js +2 -0
- package/dist/core-web/src/components/shell/types.js.map +1 -0
- package/dist/core-web/src/components/ui/avatar.d.ts +5 -0
- package/dist/core-web/src/components/ui/avatar.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/avatar.js +9 -0
- package/dist/core-web/src/components/ui/avatar.js.map +1 -0
- package/dist/core-web/src/components/ui/badge.d.ts +13 -0
- package/dist/core-web/src/components/ui/badge.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/badge.js +13 -0
- package/dist/core-web/src/components/ui/badge.js.map +1 -0
- package/dist/core-web/src/components/ui/button.d.ts +22 -0
- package/dist/core-web/src/components/ui/button.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/button.js +21 -0
- package/dist/core-web/src/components/ui/button.js.map +1 -0
- package/dist/core-web/src/components/ui/card.d.ts +7 -0
- package/dist/core-web/src/components/ui/card.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/card.js +13 -0
- package/dist/core-web/src/components/ui/card.js.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts +20 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.js +63 -0
- package/dist/core-web/src/components/ui/inline-edit.js.map +1 -0
- package/dist/core-web/src/components/ui/input.d.ts +4 -0
- package/dist/core-web/src/components/ui/input.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/input.js +7 -0
- package/dist/core-web/src/components/ui/input.js.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts +4 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.js +7 -0
- package/dist/core-web/src/components/ui/scroll-area.js.map +1 -0
- package/dist/core-web/src/components/ui/separator.d.ts +7 -0
- package/dist/core-web/src/components/ui/separator.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/separator.js +7 -0
- package/dist/core-web/src/components/ui/separator.js.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts +16 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.js +36 -0
- package/dist/core-web/src/components/ui/shell-dialog.js.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts +3 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.js +7 -0
- package/dist/core-web/src/components/ui/skeleton.js.map +1 -0
- package/dist/core-web/src/components/ui/textarea.d.ts +4 -0
- package/dist/core-web/src/components/ui/textarea.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/textarea.js +7 -0
- package/dist/core-web/src/components/ui/textarea.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts +13 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js +42 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts +79 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js +116 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts +3 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.js +33 -0
- package/dist/core-web/src/components/widgets/agent/actions.js.map +1 -0
- package/dist/core-web/src/hooks/useAuth.d.ts +25 -0
- package/dist/core-web/src/hooks/useAuth.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useAuth.js +53 -0
- package/dist/core-web/src/hooks/useAuth.js.map +1 -0
- package/dist/core-web/src/hooks/useData.d.ts +10 -0
- package/dist/core-web/src/hooks/useData.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useData.js +37 -0
- package/dist/core-web/src/hooks/useData.js.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts +6 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.js +10 -0
- package/dist/core-web/src/hooks/useMountEffect.js.map +1 -0
- package/dist/core-web/src/hooks/useTheme.d.ts +6 -0
- package/dist/core-web/src/hooks/useTheme.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useTheme.js +31 -0
- package/dist/core-web/src/hooks/useTheme.js.map +1 -0
- package/dist/core-web/src/index.d.ts +33 -0
- package/dist/core-web/src/index.d.ts.map +1 -0
- package/dist/core-web/src/index.js +38 -0
- package/dist/core-web/src/index.js.map +1 -0
- package/dist/core-web/src/lib/api.d.ts +60 -0
- package/dist/core-web/src/lib/api.d.ts.map +1 -0
- package/dist/core-web/src/lib/api.js +204 -0
- package/dist/core-web/src/lib/api.js.map +1 -0
- package/dist/core-web/src/lib/config.d.ts +10 -0
- package/dist/core-web/src/lib/config.d.ts.map +1 -0
- package/dist/core-web/src/lib/config.js +10 -0
- package/dist/core-web/src/lib/config.js.map +1 -0
- package/dist/core-web/src/lib/define-widget.d.ts +52 -0
- package/dist/core-web/src/lib/define-widget.d.ts.map +1 -0
- package/dist/core-web/src/lib/define-widget.js +14 -0
- package/dist/core-web/src/lib/define-widget.js.map +1 -0
- package/dist/core-web/src/lib/drag.d.ts +20 -0
- package/dist/core-web/src/lib/drag.d.ts.map +1 -0
- package/dist/core-web/src/lib/drag.js +33 -0
- package/dist/core-web/src/lib/drag.js.map +1 -0
- package/dist/core-web/src/lib/supabase.d.ts +2 -0
- package/dist/core-web/src/lib/supabase.d.ts.map +1 -0
- package/dist/core-web/src/lib/supabase.js +5 -0
- package/dist/core-web/src/lib/supabase.js.map +1 -0
- package/dist/core-web/src/lib/utils.d.ts +3 -0
- package/dist/core-web/src/lib/utils.d.ts.map +1 -0
- package/dist/core-web/src/lib/utils.js +6 -0
- package/dist/core-web/src/lib/utils.js.map +1 -0
- package/dist/core-web/src/lib/widgetCache.d.ts +18 -0
- package/dist/core-web/src/lib/widgetCache.d.ts.map +1 -0
- package/dist/core-web/src/lib/widgetCache.js +28 -0
- package/dist/core-web/src/lib/widgetCache.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +2 -0
- package/dist/shared.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useState, useCallback, type ReactNode } from 'react'
|
|
2
|
+
import { useMountEffect } from '../../hooks/useMountEffect'
|
|
3
|
+
import { Button } from '../ui/button'
|
|
4
|
+
import { PlusIcon, RotateCcwIcon, SunIcon, MoonIcon, MonitorIcon, UserIcon, LogOutIcon, Building2Icon, ChevronDownIcon, CheckIcon, XIcon, HomeIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react'
|
|
5
|
+
import { useTheme, type Theme } from '../../hooks/useTheme'
|
|
6
|
+
import type { ActiveEntity, WidgetType } from './types'
|
|
7
|
+
|
|
8
|
+
interface CatalogEntry {
|
|
9
|
+
type: WidgetType
|
|
10
|
+
title: string
|
|
11
|
+
icon: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
widgetCount: number
|
|
16
|
+
cockpitMode: boolean
|
|
17
|
+
activeEntity: ActiveEntity | null | undefined
|
|
18
|
+
onDeactivateEntity: (() => void) | undefined
|
|
19
|
+
onReset: () => void
|
|
20
|
+
onAddWidget: (type: WidgetType) => void
|
|
21
|
+
widgetCatalog: CatalogEntry[]
|
|
22
|
+
onOpenSettings?: () => void
|
|
23
|
+
editingLayout?: boolean
|
|
24
|
+
onToggleEditLayout?: () => void
|
|
25
|
+
openEntities?: ActiveEntity[]
|
|
26
|
+
onSelectEntity?: (e: ActiveEntity) => void
|
|
27
|
+
onCloseTab?: (e: ActiveEntity) => void
|
|
28
|
+
role?: string | null
|
|
29
|
+
companies?: Array<{ id: string; name: string }>
|
|
30
|
+
effectiveCompanyId?: string
|
|
31
|
+
setCompanyId?: (id: string) => void
|
|
32
|
+
onSignOut?: () => void
|
|
33
|
+
userEmail?: string
|
|
34
|
+
rightActions?: ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Toolbar({
|
|
38
|
+
widgetCount, cockpitMode, activeEntity, onDeactivateEntity, onReset, onAddWidget,
|
|
39
|
+
widgetCatalog, onOpenSettings, editingLayout, onToggleEditLayout,
|
|
40
|
+
openEntities, onSelectEntity, onCloseTab,
|
|
41
|
+
role, companies, effectiveCompanyId, setCompanyId, onSignOut, userEmail,
|
|
42
|
+
rightActions,
|
|
43
|
+
}: Props) {
|
|
44
|
+
type Dropdown = 'catalog' | 'profile' | 'company' | null
|
|
45
|
+
const [openDropdown, setOpenDropdown] = useState<Dropdown>(null)
|
|
46
|
+
const showCatalog = openDropdown === 'catalog'
|
|
47
|
+
const showProfile = openDropdown === 'profile'
|
|
48
|
+
const showCompany = openDropdown === 'company'
|
|
49
|
+
const toggleDropdown = (d: Dropdown) => setOpenDropdown(prev => prev === d ? null : d)
|
|
50
|
+
|
|
51
|
+
useMountEffect(() => {
|
|
52
|
+
const handler = (e: KeyboardEvent) => {
|
|
53
|
+
if (e.key === 'Escape') setOpenDropdown(null)
|
|
54
|
+
}
|
|
55
|
+
document.addEventListener('keydown', handler)
|
|
56
|
+
return () => document.removeEventListener('keydown', handler)
|
|
57
|
+
})
|
|
58
|
+
const currentCompany = companies?.find(c => c.id === effectiveCompanyId)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="sticky top-0 z-20 bg-background/80 backdrop-blur border-b border-border px-3 py-1.5 flex items-center justify-between gap-2">
|
|
62
|
+
<div className="flex items-center gap-1 min-w-0 flex-1">
|
|
63
|
+
<button
|
|
64
|
+
onClick={onDeactivateEntity}
|
|
65
|
+
className={`p-1.5 rounded-md transition-colors shrink-0 ${!cockpitMode ? 'bg-accent text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent'}`}
|
|
66
|
+
aria-label="Inicio"
|
|
67
|
+
title="Inicio"
|
|
68
|
+
>
|
|
69
|
+
<HomeIcon className="w-3.5 h-3.5" />
|
|
70
|
+
</button>
|
|
71
|
+
{openEntities && openEntities.length > 0 ? (
|
|
72
|
+
<div className="flex items-center gap-0.5 min-w-0 overflow-x-auto scrollbar-thin">
|
|
73
|
+
{openEntities.map(e => {
|
|
74
|
+
const isActive = !!(cockpitMode && activeEntity && activeEntity.type === e.type && activeEntity.id === e.id)
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={`${e.type}-${e.id}`}
|
|
78
|
+
role="tab"
|
|
79
|
+
aria-selected={isActive}
|
|
80
|
+
className={`group flex items-center gap-1.5 pl-2 pr-1 py-1 rounded-md border text-[11px] cursor-pointer transition-colors shrink-0 max-w-[200px] ${
|
|
81
|
+
isActive
|
|
82
|
+
? 'bg-primary/10 border-primary/40 text-foreground'
|
|
83
|
+
: 'bg-background border-border text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
|
84
|
+
}`}
|
|
85
|
+
onClick={() => onSelectEntity?.(e)}
|
|
86
|
+
>
|
|
87
|
+
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${isActive ? 'bg-primary animate-pulse' : 'bg-muted-foreground/40'}`} />
|
|
88
|
+
<span className="truncate">{e.label}</span>
|
|
89
|
+
<span
|
|
90
|
+
role="button"
|
|
91
|
+
tabIndex={0}
|
|
92
|
+
onClick={(ev) => { ev.stopPropagation(); onCloseTab?.(e) }}
|
|
93
|
+
onKeyDown={(ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.stopPropagation(); onCloseTab?.(e) } }}
|
|
94
|
+
className={`p-0.5 rounded-full hover:text-foreground hover:bg-accent shrink-0 transition-opacity ${isActive ? 'text-muted-foreground/60' : 'text-muted-foreground/50 opacity-0 group-hover:opacity-100'}`}
|
|
95
|
+
aria-label={`Close ${e.label}`}
|
|
96
|
+
>
|
|
97
|
+
<XIcon className="w-3 h-3" />
|
|
98
|
+
</span>
|
|
99
|
+
</button>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<span className="text-xs text-muted-foreground pl-1">{widgetCount} widgets</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="flex items-center gap-1">
|
|
109
|
+
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={onReset}>
|
|
110
|
+
<RotateCcwIcon className="w-3 h-3" /> Reset
|
|
111
|
+
</Button>
|
|
112
|
+
{onToggleEditLayout && !cockpitMode && (
|
|
113
|
+
<Button
|
|
114
|
+
variant={editingLayout ? 'default' : 'ghost'}
|
|
115
|
+
size="sm"
|
|
116
|
+
className={`h-7 text-xs gap-1 ${editingLayout ? 'bg-primary text-primary-foreground' : ''}`}
|
|
117
|
+
onClick={onToggleEditLayout}
|
|
118
|
+
>
|
|
119
|
+
{editingLayout ? <><CheckIcon className="w-3 h-3" /> Listo</> : <><LayoutGridIcon className="w-3 h-3" /> Editar</>}
|
|
120
|
+
</Button>
|
|
121
|
+
)}
|
|
122
|
+
{role === 'admin' && companies && companies.length > 1 && setCompanyId && (
|
|
123
|
+
<div className="relative">
|
|
124
|
+
<Button
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="sm"
|
|
127
|
+
className="h-7 text-xs gap-1.5 max-w-[180px]"
|
|
128
|
+
onClick={() => toggleDropdown('company')}
|
|
129
|
+
>
|
|
130
|
+
<Building2Icon className="w-3 h-3 shrink-0" />
|
|
131
|
+
<span className="truncate">{currentCompany?.name || 'Empresa'}</span>
|
|
132
|
+
<ChevronDownIcon className="w-3 h-3 shrink-0 opacity-60" />
|
|
133
|
+
</Button>
|
|
134
|
+
{showCompany && (
|
|
135
|
+
<>
|
|
136
|
+
<div className="fixed inset-0 z-20" onClick={() => setOpenDropdown(null)} />
|
|
137
|
+
<div className="absolute right-0 top-8 bg-card border border-border rounded-lg shadow-lg p-1 z-30 w-60 max-h-80 overflow-y-auto scrollbar-thin">
|
|
138
|
+
<div className="px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground/60">Empresas</div>
|
|
139
|
+
{companies.map(c => {
|
|
140
|
+
const active = c.id === effectiveCompanyId
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
key={c.id}
|
|
144
|
+
onClick={() => { setCompanyId(c.id); setOpenDropdown(null) }}
|
|
145
|
+
className={`w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2 ${active ? 'bg-accent/50' : ''}`}
|
|
146
|
+
>
|
|
147
|
+
<Building2Icon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
|
148
|
+
<span className="flex-1 truncate">{c.name}</span>
|
|
149
|
+
{active && <CheckIcon className="w-3.5 h-3.5 text-primary shrink-0" />}
|
|
150
|
+
</button>
|
|
151
|
+
)
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{rightActions}
|
|
159
|
+
<div className="relative">
|
|
160
|
+
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={() => toggleDropdown('catalog')}>
|
|
161
|
+
<PlusIcon className="w-3 h-3" /> Agregar
|
|
162
|
+
</Button>
|
|
163
|
+
{showCatalog && (
|
|
164
|
+
<>
|
|
165
|
+
<div className="fixed inset-0 z-20" onClick={() => setOpenDropdown(null)} />
|
|
166
|
+
<div className="absolute right-0 top-8 bg-card border border-border rounded-lg shadow-lg p-1 z-30 w-40">
|
|
167
|
+
{widgetCatalog.map(w => (
|
|
168
|
+
<button
|
|
169
|
+
key={w.type}
|
|
170
|
+
onClick={() => { onAddWidget(w.type); setOpenDropdown(null) }}
|
|
171
|
+
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
|
|
172
|
+
>
|
|
173
|
+
<span>{w.icon}</span> {w.title}
|
|
174
|
+
</button>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
{onSignOut && (
|
|
181
|
+
<div className="relative ml-1">
|
|
182
|
+
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => toggleDropdown('profile')} aria-label="Perfil">
|
|
183
|
+
<UserIcon className="w-3.5 h-3.5" />
|
|
184
|
+
</Button>
|
|
185
|
+
{showProfile && (
|
|
186
|
+
<>
|
|
187
|
+
<div className="fixed inset-0 z-20" onClick={() => setOpenDropdown(null)} />
|
|
188
|
+
<div className="absolute right-0 top-8 bg-card border border-border rounded-lg shadow-lg p-1 z-30 w-48">
|
|
189
|
+
{userEmail && (
|
|
190
|
+
<div className="px-3 py-1.5 text-[11px] text-muted-foreground truncate border-b border-border/50 mb-1">{userEmail}</div>
|
|
191
|
+
)}
|
|
192
|
+
{onOpenSettings && (
|
|
193
|
+
<button
|
|
194
|
+
onClick={() => { setOpenDropdown(null); onOpenSettings() }}
|
|
195
|
+
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
|
|
196
|
+
>
|
|
197
|
+
<SettingsIcon className="w-3.5 h-3.5" /> Configuracion
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
<ThemeToggleRow />
|
|
201
|
+
<div className="border-t border-border/50 mt-1 pt-1">
|
|
202
|
+
<button
|
|
203
|
+
onClick={() => { setOpenDropdown(null); onSignOut() }}
|
|
204
|
+
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
|
|
205
|
+
>
|
|
206
|
+
<LogOutIcon className="w-3.5 h-3.5" /> Salir
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ThemeToggleRow() {
|
|
220
|
+
const { theme, setTheme } = useTheme()
|
|
221
|
+
const next: Record<Theme, Theme> = { light: 'dark', dark: 'system', system: 'light' }
|
|
222
|
+
const Icon = theme === 'dark' ? MoonIcon : theme === 'light' ? SunIcon : MonitorIcon
|
|
223
|
+
const label = theme === 'dark' ? 'Oscuro' : theme === 'light' ? 'Claro' : 'Auto'
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<button
|
|
227
|
+
onClick={() => setTheme(next[theme])}
|
|
228
|
+
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
|
|
229
|
+
>
|
|
230
|
+
<Icon className="w-3.5 h-3.5" /> Tema: {label}
|
|
231
|
+
</button>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WidgetInstance } from './types'
|
|
2
|
+
|
|
3
|
+
const KEY = 'proto-shell'
|
|
4
|
+
|
|
5
|
+
export function loadShellState(): { widgets: WidgetInstance[]; layouts: any } | null {
|
|
6
|
+
try {
|
|
7
|
+
const raw = localStorage.getItem(KEY)
|
|
8
|
+
return raw ? JSON.parse(raw) : null
|
|
9
|
+
} catch { return null }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function saveShellState(widgets: WidgetInstance[], layouts: any) {
|
|
13
|
+
try {
|
|
14
|
+
localStorage.setItem(KEY, JSON.stringify({ widgets, layouts }))
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function clearShellState() {
|
|
19
|
+
localStorage.removeItem(KEY)
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const Avatar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div ref={ref} className={cn('relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full', className)} {...props} />
|
|
7
|
+
)
|
|
8
|
+
)
|
|
9
|
+
Avatar.displayName = 'Avatar'
|
|
10
|
+
|
|
11
|
+
const AvatarFallback = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
12
|
+
({ className, ...props }, ref) => (
|
|
13
|
+
<div ref={ref} className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)} {...props} />
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
AvatarFallback.displayName = 'AvatarFallback'
|
|
17
|
+
|
|
18
|
+
export { Avatar, AvatarFallback }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const variantStyles = {
|
|
5
|
+
default: 'bg-primary text-primary-foreground',
|
|
6
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
7
|
+
destructive: 'bg-destructive/10 text-destructive',
|
|
8
|
+
outline: 'border border-border text-foreground',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
12
|
+
variant?: keyof typeof variantStyles
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Badge({ className, variant = 'default', ...props }: BadgeProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
className={cn(
|
|
19
|
+
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
20
|
+
variantStyles[variant],
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Badge }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const variantStyles = {
|
|
5
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
6
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
7
|
+
outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',
|
|
8
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
9
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
10
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sizeStyles = {
|
|
14
|
+
default: 'h-9 px-4 py-2',
|
|
15
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
16
|
+
lg: 'h-11 rounded-md px-8',
|
|
17
|
+
icon: 'h-9 w-9',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
21
|
+
variant?: keyof typeof variantStyles
|
|
22
|
+
size?: keyof typeof sizeStyles
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
26
|
+
({ className, variant = 'default', size = 'default', ...props }, ref) => (
|
|
27
|
+
<button
|
|
28
|
+
className={cn(
|
|
29
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:w-4 [&_svg]:h-4',
|
|
30
|
+
variantStyles[variant],
|
|
31
|
+
sizeStyles[size],
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
ref={ref}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
Button.displayName = 'Button'
|
|
40
|
+
export { Button }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div ref={ref} className={cn('rounded-xl border bg-card text-card-foreground', className)} {...props} />
|
|
7
|
+
)
|
|
8
|
+
)
|
|
9
|
+
Card.displayName = 'Card'
|
|
10
|
+
|
|
11
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
12
|
+
({ className, ...props }, ref) => (
|
|
13
|
+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-4', className)} {...props} />
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
CardHeader.displayName = 'CardHeader'
|
|
17
|
+
|
|
18
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<div ref={ref} className={cn('p-4 pt-0', className)} {...props} />
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
CardContent.displayName = 'CardContent'
|
|
24
|
+
|
|
25
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
26
|
+
({ className, ...props }, ref) => (
|
|
27
|
+
<div ref={ref} className={cn('flex items-center p-4 pt-0', className)} {...props} />
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
CardFooter.displayName = 'CardFooter'
|
|
31
|
+
|
|
32
|
+
export { Card, CardHeader, CardContent, CardFooter }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
import { Pencil, Check, X } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface InlineEditProps {
|
|
5
|
+
value: string | null | undefined
|
|
6
|
+
onSave: (next: string) => Promise<void> | void
|
|
7
|
+
placeholder?: string
|
|
8
|
+
className?: string
|
|
9
|
+
inputClassName?: string
|
|
10
|
+
/** Render the display value when not editing. Defaults to plain text. */
|
|
11
|
+
display?: (value: string) => React.ReactNode
|
|
12
|
+
/** Allow empty string to clear the field. Defaults to true. */
|
|
13
|
+
allowEmpty?: boolean
|
|
14
|
+
/** Input type (text, date, number). Defaults to text. */
|
|
15
|
+
type?: 'text' | 'date' | 'number'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hover to reveal a pencil icon, click to edit inline. Enter or check icon
|
|
20
|
+
* saves, Escape or X discards. Keeps display markup flexible via `display`.
|
|
21
|
+
*/
|
|
22
|
+
export function InlineEdit({
|
|
23
|
+
value,
|
|
24
|
+
onSave,
|
|
25
|
+
placeholder = '—',
|
|
26
|
+
className = '',
|
|
27
|
+
inputClassName = '',
|
|
28
|
+
display,
|
|
29
|
+
allowEmpty = true,
|
|
30
|
+
type = 'text',
|
|
31
|
+
}: InlineEditProps) {
|
|
32
|
+
const [editing, setEditing] = useState(false)
|
|
33
|
+
const [draft, setDraft] = useState(value ?? '')
|
|
34
|
+
const [saving, setSaving] = useState(false)
|
|
35
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
36
|
+
|
|
37
|
+
// Sync draft when value changes externally while not editing
|
|
38
|
+
const prevValueRef = useRef(value)
|
|
39
|
+
if (prevValueRef.current !== value) {
|
|
40
|
+
prevValueRef.current = value
|
|
41
|
+
if (!editing) setDraft(value ?? '')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Callback ref: auto-focus and select when the input mounts (editing starts)
|
|
45
|
+
const inputCallbackRef = useCallback((node: HTMLInputElement | null) => {
|
|
46
|
+
(inputRef as React.MutableRefObject<HTMLInputElement | null>).current = node
|
|
47
|
+
if (node) {
|
|
48
|
+
node.focus()
|
|
49
|
+
node.select()
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const commit = async () => {
|
|
54
|
+
const next = draft.trim()
|
|
55
|
+
if (!allowEmpty && next === '') { setEditing(false); return }
|
|
56
|
+
if (next === (value ?? '')) { setEditing(false); return }
|
|
57
|
+
try {
|
|
58
|
+
setSaving(true)
|
|
59
|
+
await onSave(next)
|
|
60
|
+
setEditing(false)
|
|
61
|
+
} finally {
|
|
62
|
+
setSaving(false)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cancel = () => {
|
|
67
|
+
setDraft(value ?? '')
|
|
68
|
+
setEditing(false)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (editing) {
|
|
72
|
+
return (
|
|
73
|
+
<span className={`inline-flex items-center gap-1 ${className}`}>
|
|
74
|
+
<input
|
|
75
|
+
ref={inputCallbackRef}
|
|
76
|
+
type={type}
|
|
77
|
+
value={draft}
|
|
78
|
+
disabled={saving}
|
|
79
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
80
|
+
onKeyDown={(e) => {
|
|
81
|
+
if (e.key === 'Enter') commit()
|
|
82
|
+
if (e.key === 'Escape') cancel()
|
|
83
|
+
}}
|
|
84
|
+
onBlur={commit}
|
|
85
|
+
className={`bg-background border border-primary/50 rounded px-1.5 py-0.5 outline-none focus:border-primary ${inputClassName}`}
|
|
86
|
+
/>
|
|
87
|
+
<button
|
|
88
|
+
onMouseDown={(e) => { e.preventDefault(); commit() }}
|
|
89
|
+
className="text-emerald-600 hover:text-emerald-500 shrink-0"
|
|
90
|
+
disabled={saving}
|
|
91
|
+
aria-label="Guardar"
|
|
92
|
+
>
|
|
93
|
+
<Check className="h-3 w-3" />
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
onMouseDown={(e) => { e.preventDefault(); cancel() }}
|
|
97
|
+
className="text-muted-foreground hover:text-foreground shrink-0"
|
|
98
|
+
disabled={saving}
|
|
99
|
+
aria-label="Cancelar"
|
|
100
|
+
>
|
|
101
|
+
<X className="h-3 w-3" />
|
|
102
|
+
</button>
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const shown = value && value.trim() !== '' ? value : placeholder
|
|
108
|
+
return (
|
|
109
|
+
<span
|
|
110
|
+
className={`group inline-flex items-center gap-1 cursor-text ${className}`}
|
|
111
|
+
onClick={() => setEditing(true)}
|
|
112
|
+
role="button"
|
|
113
|
+
tabIndex={0}
|
|
114
|
+
onKeyDown={(e) => { if (e.key === 'Enter') setEditing(true) }}
|
|
115
|
+
>
|
|
116
|
+
<span className="truncate">{display && value ? display(value) : shown}</span>
|
|
117
|
+
<Pencil className="h-3 w-3 opacity-0 group-hover:opacity-60 transition-opacity shrink-0" />
|
|
118
|
+
</span>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
5
|
+
({ className, type, ...props }, ref) => (
|
|
6
|
+
<input
|
|
7
|
+
type={type}
|
|
8
|
+
className={cn(
|
|
9
|
+
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
Input.displayName = 'Input'
|
|
18
|
+
export { Input }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const ScrollArea = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, children, ...props }, ref) => (
|
|
6
|
+
<div ref={ref} className={cn('relative overflow-auto', className)} {...props}>
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
)
|
|
11
|
+
ScrollArea.displayName = 'ScrollArea'
|
|
12
|
+
export { ScrollArea }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
orientation?: 'horizontal' | 'vertical'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
9
|
+
({ className, orientation = 'horizontal', ...props }, ref) => (
|
|
10
|
+
<div
|
|
11
|
+
ref={ref}
|
|
12
|
+
role="separator"
|
|
13
|
+
className={cn(
|
|
14
|
+
'shrink-0 bg-border',
|
|
15
|
+
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
Separator.displayName = 'Separator'
|
|
23
|
+
export { Separator }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { XIcon } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
interface ShellDialogProps {
|
|
7
|
+
open: boolean
|
|
8
|
+
onClose: () => void
|
|
9
|
+
title?: ReactNode
|
|
10
|
+
description?: ReactNode
|
|
11
|
+
children: ReactNode
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dialog scoped to the shell (#shell-root) instead of the full viewport,
|
|
17
|
+
* so the chat panel stays visible. Backdrop-blur + fade-in, shadcn vibes.
|
|
18
|
+
*/
|
|
19
|
+
export function ShellDialog({ open, onClose, title, description, children, className }: ShellDialogProps) {
|
|
20
|
+
const [mounted, setMounted] = useState(false)
|
|
21
|
+
const [target, setTarget] = useState<HTMLElement | null>(null)
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setTarget(document.getElementById('shell-root'))
|
|
25
|
+
}, [open])
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!open) { setMounted(false); return }
|
|
29
|
+
const id = requestAnimationFrame(() => setMounted(true))
|
|
30
|
+
return () => cancelAnimationFrame(id)
|
|
31
|
+
}, [open])
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!open) return
|
|
35
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
|
36
|
+
window.addEventListener('keydown', onKey)
|
|
37
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
38
|
+
}, [open, onClose])
|
|
39
|
+
|
|
40
|
+
if (!open || !target) return null
|
|
41
|
+
|
|
42
|
+
return createPortal(
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
'absolute inset-0 z-50 flex items-center justify-center p-6',
|
|
46
|
+
'bg-background/60 backdrop-blur-sm transition-opacity duration-150',
|
|
47
|
+
mounted ? 'opacity-100' : 'opacity-0'
|
|
48
|
+
)}
|
|
49
|
+
onClick={onClose}
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
onClick={(e) => e.stopPropagation()}
|
|
53
|
+
className={cn(
|
|
54
|
+
'relative bg-card border border-border rounded-lg shadow-lg',
|
|
55
|
+
'w-full max-w-lg max-h-[90%] flex flex-col',
|
|
56
|
+
'transition-all duration-150',
|
|
57
|
+
mounted ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0',
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<button
|
|
62
|
+
onClick={onClose}
|
|
63
|
+
className="absolute right-3 top-3 text-muted-foreground/60 hover:text-foreground"
|
|
64
|
+
aria-label="Cerrar"
|
|
65
|
+
>
|
|
66
|
+
<XIcon className="w-4 h-4" />
|
|
67
|
+
</button>
|
|
68
|
+
{(title || description) && (
|
|
69
|
+
<div className="px-5 pt-5 pb-3 border-b border-border/50 shrink-0">
|
|
70
|
+
{title && <h2 className="text-base font-semibold">{title}</h2>}
|
|
71
|
+
{description && <p className="text-xs text-muted-foreground mt-1">{description}</p>}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
<div className="p-5 flex-1 min-h-0 overflow-y-auto scrollbar-thin">{children}</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>,
|
|
77
|
+
target
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<textarea
|
|
7
|
+
className={cn(
|
|
8
|
+
'flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
9
|
+
className
|
|
10
|
+
)}
|
|
11
|
+
ref={ref}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
Textarea.displayName = 'Textarea'
|
|
17
|
+
export { Textarea }
|