@leverege/imaginarium-ui 1.0.0-beta.1

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.
Files changed (143) hide show
  1. package/LICENSE.md +3 -0
  2. package/README.md +4 -0
  3. package/dist/Card-CFK_M3Te.js +192 -0
  4. package/dist/Card-CFK_M3Te.js.map +1 -0
  5. package/dist/Card-Z-6q-k3a.cjs +2 -0
  6. package/dist/Card-Z-6q-k3a.cjs.map +1 -0
  7. package/dist/ImaginariumApp-CCSmWwRB.js +278 -0
  8. package/dist/ImaginariumApp-CCSmWwRB.js.map +1 -0
  9. package/dist/ImaginariumApp-DoCgiASz.cjs +2 -0
  10. package/dist/ImaginariumApp-DoCgiASz.cjs.map +1 -0
  11. package/dist/ImaginariumUI.d.ts +40 -0
  12. package/dist/ImaginariumUI.d.ts.map +1 -0
  13. package/dist/PluginSetup-Cauc3iMv.cjs +2 -0
  14. package/dist/PluginSetup-Cauc3iMv.cjs.map +1 -0
  15. package/dist/PluginSetup-DnpUFDph.js +459 -0
  16. package/dist/PluginSetup-DnpUFDph.js.map +1 -0
  17. package/dist/PluginSetup.d.ts +11 -0
  18. package/dist/PluginSetup.d.ts.map +1 -0
  19. package/dist/PluginTypes-Bt6202F8.js +7 -0
  20. package/dist/PluginTypes-Bt6202F8.js.map +1 -0
  21. package/dist/PluginTypes-DOX3G1ip.cjs +2 -0
  22. package/dist/PluginTypes-DOX3G1ip.cjs.map +1 -0
  23. package/dist/api/ImaginariumApiContext.d.ts +13 -0
  24. package/dist/api/ImaginariumApiContext.d.ts.map +1 -0
  25. package/dist/auth/LoginPage.d.ts +13 -0
  26. package/dist/auth/LoginPage.d.ts.map +1 -0
  27. package/dist/auth/RequireAuth.d.ts +23 -0
  28. package/dist/auth/RequireAuth.d.ts.map +1 -0
  29. package/dist/auth/index.d.ts +3 -0
  30. package/dist/auth/index.d.ts.map +1 -0
  31. package/dist/components/ui/Button.d.ts +12 -0
  32. package/dist/components/ui/Button.d.ts.map +1 -0
  33. package/dist/components/ui/Card.d.ts +9 -0
  34. package/dist/components/ui/Card.d.ts.map +1 -0
  35. package/dist/components/ui/Dialog.d.ts +20 -0
  36. package/dist/components/ui/Dialog.d.ts.map +1 -0
  37. package/dist/components/ui/Input.d.ts +4 -0
  38. package/dist/components/ui/Input.d.ts.map +1 -0
  39. package/dist/components/ui/Label.d.ts +6 -0
  40. package/dist/components/ui/Label.d.ts.map +1 -0
  41. package/dist/components/ui/Toast.d.ts +16 -0
  42. package/dist/components/ui/Toast.d.ts.map +1 -0
  43. package/dist/components/ui/Toaster.d.ts +2 -0
  44. package/dist/components/ui/Toaster.d.ts.map +1 -0
  45. package/dist/components/ui/Tooltip.d.ts +8 -0
  46. package/dist/components/ui/Tooltip.d.ts.map +1 -0
  47. package/dist/hooks/useToast.d.ts +45 -0
  48. package/dist/hooks/useToast.d.ts.map +1 -0
  49. package/dist/index.cjs +2 -0
  50. package/dist/index.cjs.map +1 -0
  51. package/dist/index.d.ts +18 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +122 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/lib/utils.d.ts +3 -0
  56. package/dist/lib/utils.d.ts.map +1 -0
  57. package/dist/plugins/PluginTypes.d.ts +4 -0
  58. package/dist/plugins/PluginTypes.d.ts.map +1 -0
  59. package/dist/plugins/index.d.ts +4 -0
  60. package/dist/plugins/index.d.ts.map +1 -0
  61. package/dist/plugins/registry.d.ts +8 -0
  62. package/dist/plugins/registry.d.ts.map +1 -0
  63. package/dist/plugins/types.d.ts +21 -0
  64. package/dist/plugins/types.d.ts.map +1 -0
  65. package/dist/plugins.cjs +2 -0
  66. package/dist/plugins.cjs.map +1 -0
  67. package/dist/plugins.js +14 -0
  68. package/dist/plugins.js.map +1 -0
  69. package/dist/registry--NcHYEqp.js +31 -0
  70. package/dist/registry--NcHYEqp.js.map +1 -0
  71. package/dist/registry-Nyaf2ket.cjs +2 -0
  72. package/dist/registry-Nyaf2ket.cjs.map +1 -0
  73. package/dist/shell/AppNav.d.ts +17 -0
  74. package/dist/shell/AppNav.d.ts.map +1 -0
  75. package/dist/shell/ImaginariumApp.d.ts +15 -0
  76. package/dist/shell/ImaginariumApp.d.ts.map +1 -0
  77. package/dist/shell/Shell.d.ts +7 -0
  78. package/dist/shell/Shell.d.ts.map +1 -0
  79. package/dist/shell/index.d.ts +4 -0
  80. package/dist/shell/index.d.ts.map +1 -0
  81. package/dist/shell.cjs +2 -0
  82. package/dist/shell.cjs.map +1 -0
  83. package/dist/shell.js +7 -0
  84. package/dist/shell.js.map +1 -0
  85. package/dist/system/ImagineSystemNavItem.d.ts +9 -0
  86. package/dist/system/ImagineSystemNavItem.d.ts.map +1 -0
  87. package/dist/system/ImagineSystemPage.d.ts +8 -0
  88. package/dist/system/ImagineSystemPage.d.ts.map +1 -0
  89. package/dist/system/ImagineSystemView.d.ts +12 -0
  90. package/dist/system/ImagineSystemView.d.ts.map +1 -0
  91. package/dist/system/PluginSetup.d.ts +23 -0
  92. package/dist/system/PluginSetup.d.ts.map +1 -0
  93. package/dist/system/index.d.ts +7 -0
  94. package/dist/system/index.d.ts.map +1 -0
  95. package/dist/system/types.d.ts +22 -0
  96. package/dist/system/types.d.ts.map +1 -0
  97. package/dist/system.cjs +2 -0
  98. package/dist/system.cjs.map +1 -0
  99. package/dist/system.js +11 -0
  100. package/dist/system.js.map +1 -0
  101. package/dist/theme/tailwind-preset.cjs +2 -0
  102. package/dist/theme/tailwind-preset.cjs.map +1 -0
  103. package/dist/theme/tailwind-preset.d.ts +4 -0
  104. package/dist/theme/tailwind-preset.d.ts.map +1 -0
  105. package/dist/theme/tailwind-preset.js +62 -0
  106. package/dist/theme/tailwind-preset.js.map +1 -0
  107. package/lib/index.cjs +2 -0
  108. package/lib/index.js +122 -0
  109. package/package.json +82 -0
  110. package/src/ImaginariumUI.tsx +98 -0
  111. package/src/PluginSetup.ts +30 -0
  112. package/src/api/ImaginariumApiContext.tsx +54 -0
  113. package/src/auth/LoginPage.tsx +84 -0
  114. package/src/auth/RequireAuth.tsx +72 -0
  115. package/src/auth/index.ts +2 -0
  116. package/src/components/ui/Button.tsx +49 -0
  117. package/src/components/ui/Card.tsx +58 -0
  118. package/src/components/ui/Dialog.tsx +105 -0
  119. package/src/components/ui/Input.tsx +22 -0
  120. package/src/components/ui/Label.tsx +19 -0
  121. package/src/components/ui/Toast.tsx +106 -0
  122. package/src/components/ui/Toaster.tsx +29 -0
  123. package/src/components/ui/Tooltip.tsx +26 -0
  124. package/src/hooks/useToast.ts +130 -0
  125. package/src/index.ts +63 -0
  126. package/src/lib/utils.ts +6 -0
  127. package/src/plugins/PluginTypes.ts +3 -0
  128. package/src/plugins/index.ts +10 -0
  129. package/src/plugins/leverege-plugin.d.ts +20 -0
  130. package/src/plugins/registry.ts +31 -0
  131. package/src/plugins/types.ts +19 -0
  132. package/src/shell/AppNav.tsx +63 -0
  133. package/src/shell/ImaginariumApp.tsx +78 -0
  134. package/src/shell/Shell.tsx +46 -0
  135. package/src/shell/index.ts +3 -0
  136. package/src/system/ImagineSystemNavItem.ts +9 -0
  137. package/src/system/ImagineSystemPage.tsx +7 -0
  138. package/src/system/ImagineSystemView.tsx +423 -0
  139. package/src/system/PluginSetup.ts +57 -0
  140. package/src/system/index.ts +14 -0
  141. package/src/system/types.ts +24 -0
  142. package/src/theme/index.css +133 -0
  143. package/src/theme/tailwind-preset.ts +62 -0
@@ -0,0 +1,130 @@
1
+ import * as React from 'react'
2
+
3
+ import type { ToastActionElement, ToastProps } from '../components/ui/Toast'
4
+
5
+ const TOAST_LIMIT = 1
6
+ const TOAST_REMOVE_DELAY = 1_000_000
7
+
8
+ type ToasterToast = ToastProps & {
9
+ id: string
10
+ title?: React.ReactNode
11
+ description?: React.ReactNode
12
+ action?: ToastActionElement
13
+ }
14
+
15
+ const actionTypes = {
16
+ ADD_TOAST: 'ADD_TOAST',
17
+ UPDATE_TOAST: 'UPDATE_TOAST',
18
+ DISMISS_TOAST: 'DISMISS_TOAST',
19
+ REMOVE_TOAST: 'REMOVE_TOAST',
20
+ } as const
21
+
22
+ let count = 0
23
+ function genId() {
24
+ count = ( count + 1 ) % Number.MAX_SAFE_INTEGER
25
+ return count.toString()
26
+ }
27
+
28
+ type ActionType = typeof actionTypes
29
+ type Action =
30
+ | { type: ActionType['ADD_TOAST']; toast: ToasterToast }
31
+ | { type: ActionType['UPDATE_TOAST']; toast: Partial<ToasterToast> }
32
+ | { type: ActionType['DISMISS_TOAST']; toastId?: ToasterToast['id'] }
33
+ | { type: ActionType['REMOVE_TOAST']; toastId?: ToasterToast['id'] }
34
+
35
+ interface State {
36
+ toasts: ToasterToast[]
37
+ }
38
+
39
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
40
+
41
+ const addToRemoveQueue = ( toastId: string ) => {
42
+ if ( toastTimeouts.has( toastId ) ) { return }
43
+ const timeout = setTimeout( () => {
44
+ toastTimeouts.delete( toastId )
45
+ dispatch( { type: 'REMOVE_TOAST', toastId } )
46
+ }, TOAST_REMOVE_DELAY )
47
+ toastTimeouts.set( toastId, timeout )
48
+ }
49
+
50
+ export const reducer = ( state: State, action: Action ): State => {
51
+ switch ( action.type ) {
52
+ case 'ADD_TOAST':
53
+ return {
54
+ ...state,
55
+ toasts: [ action.toast, ...state.toasts ].slice( 0, TOAST_LIMIT ),
56
+ }
57
+ case 'UPDATE_TOAST':
58
+ return {
59
+ ...state,
60
+ toasts: state.toasts.map( t => ( t.id === action.toast.id ? { ...t, ...action.toast } : t ) ),
61
+ }
62
+ case 'DISMISS_TOAST': {
63
+ const { toastId } = action
64
+ if ( toastId ) {
65
+ addToRemoveQueue( toastId )
66
+ } else {
67
+ state.toasts.forEach( t => addToRemoveQueue( t.id ) )
68
+ }
69
+ return {
70
+ ...state,
71
+ toasts: state.toasts.map( t =>
72
+ t.id === toastId || toastId === undefined ? { ...t, open: false } : t,
73
+ ),
74
+ }
75
+ }
76
+ case 'REMOVE_TOAST':
77
+ if ( action.toastId === undefined ) {
78
+ return { ...state, toasts: [] }
79
+ }
80
+ return { ...state, toasts: state.toasts.filter( t => t.id !== action.toastId ) }
81
+ }
82
+ }
83
+
84
+ const listeners: Array<( state: State ) => void> = []
85
+ let memoryState: State = { toasts: [] }
86
+
87
+ function dispatch( action: Action ) {
88
+ memoryState = reducer( memoryState, action )
89
+ listeners.forEach( l => l( memoryState ) )
90
+ }
91
+
92
+ type Toast = Omit<ToasterToast, 'id'>
93
+
94
+ function toast( { ...props }: Toast ) {
95
+ const id = genId()
96
+ const update = ( p: ToasterToast ) =>
97
+ dispatch( { type: 'UPDATE_TOAST', toast: { ...p, id } } )
98
+ const dismiss = () => dispatch( { type: 'DISMISS_TOAST', toastId: id } )
99
+
100
+ dispatch( {
101
+ type: 'ADD_TOAST',
102
+ toast: {
103
+ ...props,
104
+ id,
105
+ open: true,
106
+ onOpenChange: ( open ) => {
107
+ if ( !open ) { dismiss() }
108
+ },
109
+ },
110
+ } )
111
+ return { id, dismiss, update }
112
+ }
113
+
114
+ function useToast() {
115
+ const [ state, setState ] = React.useState<State>( memoryState )
116
+ React.useEffect( () => {
117
+ listeners.push( setState )
118
+ return () => {
119
+ const index = listeners.indexOf( setState )
120
+ if ( index > -1 ) { listeners.splice( index, 1 ) }
121
+ }
122
+ }, [ state ] )
123
+ return {
124
+ ...state,
125
+ toast,
126
+ dismiss: ( toastId?: string ) => dispatch( { type: 'DISMISS_TOAST', toastId } ),
127
+ }
128
+ }
129
+
130
+ export { useToast, toast }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ export * from './plugins'
2
+ export * from './shell'
3
+ export * from './system'
4
+ export * from './auth'
5
+
6
+ export { ImaginariumUI, type ImaginariumUIConfig } from './ImaginariumUI'
7
+
8
+ export {
9
+ PageType,
10
+ NavItemType,
11
+ ProviderType,
12
+ } from './plugins/PluginTypes'
13
+
14
+ export { Button, buttonVariants, type ButtonProps } from './components/ui/Button'
15
+ export {
16
+ Card,
17
+ CardHeader,
18
+ CardFooter,
19
+ CardTitle,
20
+ CardDescription,
21
+ CardContent,
22
+ } from './components/ui/Card'
23
+ export {
24
+ Dialog,
25
+ DialogPortal,
26
+ DialogOverlay,
27
+ DialogClose,
28
+ DialogTrigger,
29
+ DialogContent,
30
+ DialogHeader,
31
+ DialogFooter,
32
+ DialogTitle,
33
+ DialogDescription,
34
+ } from './components/ui/Dialog'
35
+ export { Input } from './components/ui/Input'
36
+ export { Label } from './components/ui/Label'
37
+ export {
38
+ Tooltip,
39
+ TooltipTrigger,
40
+ TooltipContent,
41
+ TooltipProvider,
42
+ } from './components/ui/Tooltip'
43
+ export {
44
+ ToastProvider,
45
+ ToastViewport,
46
+ Toast,
47
+ ToastTitle,
48
+ ToastDescription,
49
+ ToastClose,
50
+ ToastAction,
51
+ type ToastProps,
52
+ type ToastActionElement,
53
+ } from './components/ui/Toast'
54
+ export { Toaster } from './components/ui/Toaster'
55
+
56
+ export { useToast, toast } from './hooks/useToast'
57
+ export {
58
+ ImaginariumApiProvider,
59
+ useImaginariumApi,
60
+ type ImaginariumApiContextValue,
61
+ type ImaginariumApiProviderProps,
62
+ } from './api/ImaginariumApiContext'
63
+ export { cn } from './lib/utils'
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn( ...inputs: ClassValue[] ) {
5
+ return twMerge( clsx( inputs ) )
6
+ }
@@ -0,0 +1,3 @@
1
+ export const PageType = 'imaginarium.ui.Page'
2
+ export const NavItemType = 'imaginarium.ui.NavItem'
3
+ export const ProviderType = 'imaginarium.ui.Provider'
@@ -0,0 +1,10 @@
1
+ export { NavItemType, PageType, ProviderType } from './PluginTypes'
2
+ export type { NavItemPlugin, PagePlugin, ProviderPlugin } from './types'
3
+ export {
4
+ addNavItem,
5
+ addPage,
6
+ addProvider,
7
+ getNavItems,
8
+ getPages,
9
+ getProviders,
10
+ } from './registry'
@@ -0,0 +1,20 @@
1
+ declare module '@leverege/plugin' {
2
+ type Mutator = ( type: string, plugin: unknown ) => unknown | null
3
+
4
+ interface PluginsApi {
5
+ add( type: string, plugin: unknown ): unknown
6
+ addPlugin( type: string, plugin: unknown ): unknown
7
+ remove( type: string, plugin: unknown ): boolean
8
+ get<T = unknown>( type: string, criteria?: unknown ): T[]
9
+ addMutator( mutator: Mutator ): void
10
+ removeMutator( mutator: Mutator ): void
11
+ }
12
+
13
+ interface ConfigApi {
14
+ get<T = unknown>( resource: string, key: string, dValue?: T ): T
15
+ set( resource: string, key: string, value: unknown ): void
16
+ }
17
+
18
+ export const Plugins: PluginsApi
19
+ export const Config: ConfigApi
20
+ }
@@ -0,0 +1,31 @@
1
+ import { Plugins } from '@leverege/plugin'
2
+ import { NavItemType, PageType, ProviderType } from './PluginTypes'
3
+ import type { NavItemPlugin, PagePlugin, ProviderPlugin } from './types'
4
+
5
+ export function addPage( plugin: PagePlugin ) {
6
+ Plugins.add( PageType, plugin )
7
+ }
8
+
9
+ export function addNavItem( plugin: NavItemPlugin ) {
10
+ Plugins.add( NavItemType, plugin )
11
+ }
12
+
13
+ export function addProvider( plugin: ProviderPlugin ) {
14
+ Plugins.add( ProviderType, plugin )
15
+ }
16
+
17
+ export function getPages(): PagePlugin[] {
18
+ return Plugins.get<PagePlugin>( PageType )
19
+ }
20
+
21
+ export function getNavItems(): NavItemPlugin[] {
22
+ const items = Plugins.get<NavItemPlugin>( NavItemType ).slice()
23
+ items.sort( ( a, b ) => ( a.sort ?? 0 ) - ( b.sort ?? 0 ) )
24
+ return items
25
+ }
26
+
27
+ export function getProviders(): ProviderPlugin[] {
28
+ const items = Plugins.get<ProviderPlugin>( ProviderType ).slice()
29
+ items.sort( ( a, b ) => ( a.sort ?? 0 ) - ( b.sort ?? 0 ) )
30
+ return items
31
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComponentType, ReactNode } from 'react'
2
+
3
+ export interface PagePlugin {
4
+ path: string
5
+ element: ReactNode | ComponentType
6
+ exact?: boolean
7
+ }
8
+
9
+ export interface NavItemPlugin {
10
+ path: string
11
+ label: string
12
+ icon?: ComponentType<{ className?: string }>
13
+ sort?: number
14
+ }
15
+
16
+ export interface ProviderPlugin {
17
+ component: ComponentType<{ children: ReactNode }>
18
+ sort?: number
19
+ }
@@ -0,0 +1,63 @@
1
+ import type { ComponentType } from 'react'
2
+ import { Link, useLocation } from 'wouter'
3
+
4
+ import { Button } from '../components/ui/Button'
5
+ import { getNavItems } from '../plugins/registry'
6
+
7
+ /**
8
+ * Accepted icon forms (anywhere we render an inline icon):
9
+ * - React component: `Settings` from lucide-react
10
+ * - URL string: `/logo.png`, `https://...`, `data:image/...`, `blob:...`
11
+ * - CSS class string: `fa-solid fa-gear`, `material-icons` (rendered on a <span>)
12
+ */
13
+ export type IconRef = ComponentType<{ className?: string }> | string
14
+
15
+ export interface AppNavProps {
16
+ title?: string
17
+ titleIcon?: IconRef
18
+ rightSlot?: React.ReactNode
19
+ }
20
+
21
+ const URL_RE = /^(https?:|data:|blob:|\/|\.\/|\.\.\/)/
22
+
23
+ function renderIcon( icon: IconRef | undefined, className: string ) {
24
+ if ( !icon ) { return null }
25
+ if ( typeof icon === 'string' ) {
26
+ if ( URL_RE.test( icon ) ) {
27
+ return <img src={icon} alt="" className={className} />
28
+ }
29
+ return <span className={`${icon} ${className}`} aria-hidden="true" />
30
+ }
31
+ const Icon = icon
32
+ return <Icon className={className} />
33
+ }
34
+
35
+ export function AppNav( { title = 'Imaginarium', titleIcon, rightSlot }: AppNavProps ) {
36
+ const [ location ] = useLocation()
37
+ const items = getNavItems()
38
+
39
+ return (
40
+ <nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
41
+ <div className="container mx-auto flex h-14 items-center px-4">
42
+ <div className="mr-8 flex items-center gap-2 font-semibold text-lg">
43
+ {renderIcon( titleIcon, 'h-6 w-6' )}
44
+ <span>{title}</span>
45
+ </div>
46
+ <div className="flex items-center gap-1">
47
+ {items.map( ( { label, path, icon: Icon } ) => {
48
+ const isActive = location === path || location.startsWith( `${path}/` )
49
+ return (
50
+ <Link key={path} href={path}>
51
+ <Button variant={isActive ? 'secondary' : 'ghost'} size="sm" className="gap-2">
52
+ {Icon ? <Icon className="h-4 w-4" /> : null}
53
+ {label}
54
+ </Button>
55
+ </Link>
56
+ )
57
+ } )}
58
+ </div>
59
+ {rightSlot ? <div className="ml-auto flex items-center gap-2">{rightSlot}</div> : null}
60
+ </div>
61
+ </nav>
62
+ )
63
+ }
@@ -0,0 +1,78 @@
1
+ import { useMemo, type ReactNode } from 'react'
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3
+ import { Config } from '@leverege/plugin'
4
+
5
+ import { TooltipProvider } from '../components/ui/Tooltip'
6
+ import { Toaster } from '../components/ui/Toaster'
7
+ import { ImaginariumApiProvider } from '../api/ImaginariumApiContext'
8
+ import { RequireAuth } from '../auth/RequireAuth'
9
+ import { AppNav, type AppNavProps, type IconRef } from './AppNav'
10
+ import { Shell, type ShellProps } from './Shell'
11
+
12
+ export interface ImaginariumAppProps {
13
+ apiBaseUrl?: string
14
+ queryClient?: QueryClient
15
+ title?: string
16
+ titleIcon?: IconRef
17
+ navRightSlot?: AppNavProps['rightSlot']
18
+ shellFallback?: ShellProps['fallback']
19
+ children?: ReactNode
20
+ }
21
+
22
+ const defaultQueryClient = new QueryClient( {
23
+ defaultOptions: {
24
+ queries: { retry: false, refetchOnWindowFocus: false },
25
+ },
26
+ } )
27
+
28
+ function GatedShell( { title, titleIcon, navRightSlot, shellFallback, children }: {
29
+ title?: string
30
+ titleIcon?: IconRef
31
+ navRightSlot?: AppNavProps['rightSlot']
32
+ shellFallback?: ShellProps['fallback']
33
+ children?: ReactNode
34
+ } ) {
35
+ return (
36
+ <div className="min-h-screen bg-background text-foreground">
37
+ <AppNav title={title} titleIcon={titleIcon} rightSlot={navRightSlot} />
38
+ <Shell fallback={shellFallback}>{children}</Shell>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export function ImaginariumApp( {
44
+ apiBaseUrl = '/api',
45
+ queryClient,
46
+ title,
47
+ titleIcon,
48
+ navRightSlot,
49
+ shellFallback,
50
+ children,
51
+ }: ImaginariumAppProps ) {
52
+ const qc = useMemo( () => queryClient ?? defaultQueryClient, [ queryClient ] )
53
+ const requireAuth = Config.get<boolean>( 'ImaginariumApp', 'requireAuth', true )
54
+ const resolvedTitleIcon = titleIcon
55
+ ?? Config.get<IconRef | undefined>( 'ImaginariumApp', 'titleIcon', undefined )
56
+
57
+ const gated = (
58
+ <GatedShell
59
+ title={title}
60
+ titleIcon={resolvedTitleIcon}
61
+ navRightSlot={navRightSlot}
62
+ shellFallback={shellFallback}
63
+ >
64
+ {children}
65
+ </GatedShell>
66
+ )
67
+
68
+ return (
69
+ <QueryClientProvider client={qc}>
70
+ <TooltipProvider>
71
+ <ImaginariumApiProvider apiBaseUrl={apiBaseUrl}>
72
+ {requireAuth ? <RequireAuth>{gated}</RequireAuth> : gated}
73
+ <Toaster />
74
+ </ImaginariumApiProvider>
75
+ </TooltipProvider>
76
+ </QueryClientProvider>
77
+ )
78
+ }
@@ -0,0 +1,46 @@
1
+ import { isValidElement, createElement, type ReactNode } from 'react'
2
+ import { Route, Switch } from 'wouter'
3
+
4
+ import { getPages, getProviders } from '../plugins/registry'
5
+ import type { PagePlugin } from '../plugins/types'
6
+
7
+ function renderPageElement( plugin: PagePlugin ): ReactNode {
8
+ const { element } = plugin
9
+ if ( isValidElement( element ) ) { return element }
10
+ if ( typeof element === 'function' ) {
11
+ return createElement( element as React.ComponentType )
12
+ }
13
+ return element as ReactNode
14
+ }
15
+
16
+ export interface ShellProps {
17
+ fallback?: ReactNode
18
+ children?: ReactNode
19
+ }
20
+
21
+ export function Shell( { fallback, children }: ShellProps ) {
22
+ const pages = getPages()
23
+ const providers = getProviders()
24
+
25
+ let tree: ReactNode = (
26
+ <Switch>
27
+ {pages.map( ( page ) => (
28
+ <Route key={page.path} path={page.path}>
29
+ {renderPageElement( page )}
30
+ </Route>
31
+ ) )}
32
+ <Route>{fallback ?? <div className="p-8 text-muted-foreground">Not found</div>}</Route>
33
+ </Switch>
34
+ )
35
+
36
+ for ( const { component: Provider } of providers ) {
37
+ tree = <Provider>{tree}</Provider>
38
+ }
39
+
40
+ return (
41
+ <main className="container mx-auto px-4 py-6">
42
+ {children}
43
+ {tree}
44
+ </main>
45
+ )
46
+ }
@@ -0,0 +1,3 @@
1
+ export { Shell, type ShellProps } from './Shell'
2
+ export { AppNav, type AppNavProps } from './AppNav'
3
+ export { ImaginariumApp, type ImaginariumAppProps } from './ImaginariumApp'
@@ -0,0 +1,9 @@
1
+ import { Settings } from 'lucide-react'
2
+
3
+ export default {
4
+ id : 'imaginarium.ImagineSystemNavItem',
5
+ path : '/system-config',
6
+ label : 'Imagine Systems',
7
+ sort : 100,
8
+ icon : Settings,
9
+ }
@@ -0,0 +1,7 @@
1
+ import ImagineSystemView from './ImagineSystemView'
2
+
3
+ export default {
4
+ id : 'imaginarium.ImagineSystemPage',
5
+ path : '/system-config',
6
+ element : ImagineSystemView,
7
+ }