@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.
- package/LICENSE.md +3 -0
- package/README.md +4 -0
- package/dist/Card-CFK_M3Te.js +192 -0
- package/dist/Card-CFK_M3Te.js.map +1 -0
- package/dist/Card-Z-6q-k3a.cjs +2 -0
- package/dist/Card-Z-6q-k3a.cjs.map +1 -0
- package/dist/ImaginariumApp-CCSmWwRB.js +278 -0
- package/dist/ImaginariumApp-CCSmWwRB.js.map +1 -0
- package/dist/ImaginariumApp-DoCgiASz.cjs +2 -0
- package/dist/ImaginariumApp-DoCgiASz.cjs.map +1 -0
- package/dist/ImaginariumUI.d.ts +40 -0
- package/dist/ImaginariumUI.d.ts.map +1 -0
- package/dist/PluginSetup-Cauc3iMv.cjs +2 -0
- package/dist/PluginSetup-Cauc3iMv.cjs.map +1 -0
- package/dist/PluginSetup-DnpUFDph.js +459 -0
- package/dist/PluginSetup-DnpUFDph.js.map +1 -0
- package/dist/PluginSetup.d.ts +11 -0
- package/dist/PluginSetup.d.ts.map +1 -0
- package/dist/PluginTypes-Bt6202F8.js +7 -0
- package/dist/PluginTypes-Bt6202F8.js.map +1 -0
- package/dist/PluginTypes-DOX3G1ip.cjs +2 -0
- package/dist/PluginTypes-DOX3G1ip.cjs.map +1 -0
- package/dist/api/ImaginariumApiContext.d.ts +13 -0
- package/dist/api/ImaginariumApiContext.d.ts.map +1 -0
- package/dist/auth/LoginPage.d.ts +13 -0
- package/dist/auth/LoginPage.d.ts.map +1 -0
- package/dist/auth/RequireAuth.d.ts +23 -0
- package/dist/auth/RequireAuth.d.ts.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/components/ui/Button.d.ts +12 -0
- package/dist/components/ui/Button.d.ts.map +1 -0
- package/dist/components/ui/Card.d.ts +9 -0
- package/dist/components/ui/Card.d.ts.map +1 -0
- package/dist/components/ui/Dialog.d.ts +20 -0
- package/dist/components/ui/Dialog.d.ts.map +1 -0
- package/dist/components/ui/Input.d.ts +4 -0
- package/dist/components/ui/Input.d.ts.map +1 -0
- package/dist/components/ui/Label.d.ts +6 -0
- package/dist/components/ui/Label.d.ts.map +1 -0
- package/dist/components/ui/Toast.d.ts +16 -0
- package/dist/components/ui/Toast.d.ts.map +1 -0
- package/dist/components/ui/Toaster.d.ts +2 -0
- package/dist/components/ui/Toaster.d.ts.map +1 -0
- package/dist/components/ui/Tooltip.d.ts +8 -0
- package/dist/components/ui/Tooltip.d.ts.map +1 -0
- package/dist/hooks/useToast.d.ts +45 -0
- package/dist/hooks/useToast.d.ts.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/plugins/PluginTypes.d.ts +4 -0
- package/dist/plugins/PluginTypes.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/registry.d.ts +8 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/types.d.ts +21 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins.cjs +2 -0
- package/dist/plugins.cjs.map +1 -0
- package/dist/plugins.js +14 -0
- package/dist/plugins.js.map +1 -0
- package/dist/registry--NcHYEqp.js +31 -0
- package/dist/registry--NcHYEqp.js.map +1 -0
- package/dist/registry-Nyaf2ket.cjs +2 -0
- package/dist/registry-Nyaf2ket.cjs.map +1 -0
- package/dist/shell/AppNav.d.ts +17 -0
- package/dist/shell/AppNav.d.ts.map +1 -0
- package/dist/shell/ImaginariumApp.d.ts +15 -0
- package/dist/shell/ImaginariumApp.d.ts.map +1 -0
- package/dist/shell/Shell.d.ts +7 -0
- package/dist/shell/Shell.d.ts.map +1 -0
- package/dist/shell/index.d.ts +4 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell.cjs +2 -0
- package/dist/shell.cjs.map +1 -0
- package/dist/shell.js +7 -0
- package/dist/shell.js.map +1 -0
- package/dist/system/ImagineSystemNavItem.d.ts +9 -0
- package/dist/system/ImagineSystemNavItem.d.ts.map +1 -0
- package/dist/system/ImagineSystemPage.d.ts +8 -0
- package/dist/system/ImagineSystemPage.d.ts.map +1 -0
- package/dist/system/ImagineSystemView.d.ts +12 -0
- package/dist/system/ImagineSystemView.d.ts.map +1 -0
- package/dist/system/PluginSetup.d.ts +23 -0
- package/dist/system/PluginSetup.d.ts.map +1 -0
- package/dist/system/index.d.ts +7 -0
- package/dist/system/index.d.ts.map +1 -0
- package/dist/system/types.d.ts +22 -0
- package/dist/system/types.d.ts.map +1 -0
- package/dist/system.cjs +2 -0
- package/dist/system.cjs.map +1 -0
- package/dist/system.js +11 -0
- package/dist/system.js.map +1 -0
- package/dist/theme/tailwind-preset.cjs +2 -0
- package/dist/theme/tailwind-preset.cjs.map +1 -0
- package/dist/theme/tailwind-preset.d.ts +4 -0
- package/dist/theme/tailwind-preset.d.ts.map +1 -0
- package/dist/theme/tailwind-preset.js +62 -0
- package/dist/theme/tailwind-preset.js.map +1 -0
- package/lib/index.cjs +2 -0
- package/lib/index.js +122 -0
- package/package.json +82 -0
- package/src/ImaginariumUI.tsx +98 -0
- package/src/PluginSetup.ts +30 -0
- package/src/api/ImaginariumApiContext.tsx +54 -0
- package/src/auth/LoginPage.tsx +84 -0
- package/src/auth/RequireAuth.tsx +72 -0
- package/src/auth/index.ts +2 -0
- package/src/components/ui/Button.tsx +49 -0
- package/src/components/ui/Card.tsx +58 -0
- package/src/components/ui/Dialog.tsx +105 -0
- package/src/components/ui/Input.tsx +22 -0
- package/src/components/ui/Label.tsx +19 -0
- package/src/components/ui/Toast.tsx +106 -0
- package/src/components/ui/Toaster.tsx +29 -0
- package/src/components/ui/Tooltip.tsx +26 -0
- package/src/hooks/useToast.ts +130 -0
- package/src/index.ts +63 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugins/PluginTypes.ts +3 -0
- package/src/plugins/index.ts +10 -0
- package/src/plugins/leverege-plugin.d.ts +20 -0
- package/src/plugins/registry.ts +31 -0
- package/src/plugins/types.ts +19 -0
- package/src/shell/AppNav.tsx +63 -0
- package/src/shell/ImaginariumApp.tsx +78 -0
- package/src/shell/Shell.tsx +46 -0
- package/src/shell/index.ts +3 -0
- package/src/system/ImagineSystemNavItem.ts +9 -0
- package/src/system/ImagineSystemPage.tsx +7 -0
- package/src/system/ImagineSystemView.tsx +423 -0
- package/src/system/PluginSetup.ts +57 -0
- package/src/system/index.ts +14 -0
- package/src/system/types.ts +24 -0
- package/src/theme/index.css +133 -0
- 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'
|
package/src/lib/utils.ts
ADDED
|
@@ -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
|
+
}
|