@camstack/addon-admin-ui 0.1.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 (122) hide show
  1. package/index.html +22 -0
  2. package/package.json +69 -0
  3. package/public/brand/logo-dark.svg +16 -0
  4. package/public/brand/logo-horizontal-dark.svg +21 -0
  5. package/public/brand/logo-horizontal-light.svg +21 -0
  6. package/public/brand/logo-light.svg +16 -0
  7. package/public/brand/logo-wide-dark.svg +24 -0
  8. package/public/brand/logo-wide-light.svg +24 -0
  9. package/public/favicon.svg +8 -0
  10. package/public/vendor/react-jsx-runtime.mjs +24 -0
  11. package/public/vendor/react.mjs +16 -0
  12. package/src/App.tsx +71 -0
  13. package/src/components/addons/AddonCard.tsx +339 -0
  14. package/src/components/addons/AddonUploadZone.tsx +307 -0
  15. package/src/components/addons/CapabilityBadge.tsx +55 -0
  16. package/src/components/addons/CapabilityMap.tsx +133 -0
  17. package/src/components/addons/UpdatesList.tsx +119 -0
  18. package/src/components/agents/AgentCard.tsx +281 -0
  19. package/src/components/agents/AgentLogs.tsx +231 -0
  20. package/src/components/agents/ProcessList.tsx +127 -0
  21. package/src/components/agents/ProcessTree.tsx +369 -0
  22. package/src/components/agents/TaskList.tsx +68 -0
  23. package/src/components/cameras/CameraCard.tsx +60 -0
  24. package/src/components/cameras/LiveEventsPanel.tsx +91 -0
  25. package/src/components/cameras/ProviderSection.tsx +50 -0
  26. package/src/components/cameras/StreamArea.tsx +107 -0
  27. package/src/components/cameras/tabs/AddonsTab.tsx +113 -0
  28. package/src/components/cameras/tabs/CameraEventsTab.tsx +129 -0
  29. package/src/components/cameras/tabs/PipelineTab.tsx +118 -0
  30. package/src/components/cameras/tabs/StreamsTab.tsx +114 -0
  31. package/src/components/dashboard/BlockPicker.tsx +54 -0
  32. package/src/components/dashboard/BlockWrapper.tsx +97 -0
  33. package/src/components/dashboard/DashboardGrid.tsx +160 -0
  34. package/src/components/dashboard/block-registry.ts +15 -0
  35. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +39 -0
  36. package/src/components/dashboard/blocks/StorageBlock.tsx +66 -0
  37. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +67 -0
  38. package/src/components/dashboard/blocks/index.ts +32 -0
  39. package/src/components/device/DeviceHeader.tsx +116 -0
  40. package/src/components/device/FloatingPanel.tsx +132 -0
  41. package/src/components/device/FloatingPanelManager.tsx +167 -0
  42. package/src/components/device/PanelContent.tsx +196 -0
  43. package/src/components/device/QuickConfigWizard.tsx +507 -0
  44. package/src/components/device/tabs/DetectionConfigTab.tsx +96 -0
  45. package/src/components/device/tabs/EventsTab.tsx +19 -0
  46. package/src/components/device/tabs/LogsTab.tsx +22 -0
  47. package/src/components/device/tabs/OverviewTab.tsx +104 -0
  48. package/src/components/device/tabs/ProviderSettingsTab.tsx +34 -0
  49. package/src/components/device/tabs/RecordingTab.tsx +47 -0
  50. package/src/components/device/tabs/ReplTab.tsx +153 -0
  51. package/src/components/device/tabs/TrackTrailTab.tsx +49 -0
  52. package/src/components/device/tabs/ZonesTab.tsx +98 -0
  53. package/src/components/device/zone-editor/ZoneCanvas.tsx +354 -0
  54. package/src/components/device/zone-editor/ZoneForm.tsx +128 -0
  55. package/src/components/device/zone-editor/ZoneList.tsx +150 -0
  56. package/src/components/form-builder/FormBuilder.tsx +135 -0
  57. package/src/components/form-builder/FormField.tsx +732 -0
  58. package/src/components/form-builder/ModelSelector.tsx +239 -0
  59. package/src/components/integrations/AddDeviceDialog.tsx +205 -0
  60. package/src/components/integrations/CompactDeviceCard.tsx +35 -0
  61. package/src/components/integrations/DeviceCard.tsx +29 -0
  62. package/src/components/integrations/DeviceDiscoveryStep.tsx +105 -0
  63. package/src/components/integrations/DeviceGrid.tsx +79 -0
  64. package/src/components/integrations/DeviceGroupHeader.tsx +17 -0
  65. package/src/components/integrations/DiscoveredDeviceCard.tsx +26 -0
  66. package/src/components/integrations/IntegrationCard.tsx +40 -0
  67. package/src/components/integrations/IntegrationWizard.tsx +171 -0
  68. package/src/components/integrations/ProviderConfigForm.tsx +89 -0
  69. package/src/components/integrations/ProviderPicker.tsx +91 -0
  70. package/src/components/integrations/SnapshotPopover.tsx +68 -0
  71. package/src/components/metrics/AgentLoad.tsx +113 -0
  72. package/src/components/metrics/IntegrationUsage.tsx +90 -0
  73. package/src/components/metrics/PipelineStatus.tsx +105 -0
  74. package/src/components/metrics/ProcessResources.tsx +139 -0
  75. package/src/components/pipeline/PhaseSettings.tsx +131 -0
  76. package/src/components/shared/CapabilityBadges.tsx +30 -0
  77. package/src/components/shared/ProviderIcon.tsx +42 -0
  78. package/src/components/shared/StatusBadge.tsx +23 -0
  79. package/src/components/shared/WebRtcPlayer.tsx +211 -0
  80. package/src/components/timeline/EventMarker.tsx +32 -0
  81. package/src/components/timeline/TimelineBar.tsx +131 -0
  82. package/src/components/ui/ConfirmDialog.tsx +115 -0
  83. package/src/components/ui/ToastContainer.tsx +92 -0
  84. package/src/contexts/auth-context.tsx +91 -0
  85. package/src/hooks/useBackendClient.ts +6 -0
  86. package/src/hooks/useTheme.ts +1 -0
  87. package/src/i18n/en.json +164 -0
  88. package/src/i18n/index.ts +29 -0
  89. package/src/i18n/it.json +164 -0
  90. package/src/index.css +63 -0
  91. package/src/layouts/AddonPageLoader.tsx +120 -0
  92. package/src/layouts/AppLayout.tsx +238 -0
  93. package/src/layouts/ProtectedRoute.tsx +25 -0
  94. package/src/lib/addon-page-context.ts +29 -0
  95. package/src/lib/backend.ts +16 -0
  96. package/src/main.tsx +21 -0
  97. package/src/pages/AccessDenied.tsx +22 -0
  98. package/src/pages/Cameras.tsx +127 -0
  99. package/src/pages/Dashboard.tsx +6 -0
  100. package/src/pages/DeviceDetail.tsx +175 -0
  101. package/src/pages/IntegrationDetail.tsx +224 -0
  102. package/src/pages/Integrations.tsx +330 -0
  103. package/src/pages/Login.tsx +106 -0
  104. package/src/pages/Metrics.tsx +18 -0
  105. package/src/pages/PipelineConfig.tsx +282 -0
  106. package/src/pages/Showroom.tsx +351 -0
  107. package/src/pages/Timeline.tsx +269 -0
  108. package/src/pages/system/Addons.tsx +525 -0
  109. package/src/pages/system/Agents.tsx +362 -0
  110. package/src/pages/system/Logs.tsx +131 -0
  111. package/src/pages/system/Models.tsx +102 -0
  112. package/src/pages/system/Processes.tsx +129 -0
  113. package/src/pages/system/Repl.tsx +148 -0
  114. package/src/pages/system/Settings.tsx +168 -0
  115. package/src/pages/system/Users.tsx +174 -0
  116. package/src/server/addon.ts +54 -0
  117. package/src/types/config-ui.ts +210 -0
  118. package/src/types/dashboard.ts +39 -0
  119. package/tsconfig.json +29 -0
  120. package/tsconfig.server.json +16 -0
  121. package/tsup.config.ts +20 -0
  122. package/vite.config.ts +68 -0
@@ -0,0 +1,164 @@
1
+ {
2
+ "nav": {
3
+ "dashboard": "Dashboard",
4
+ "integrations": "Integrations",
5
+ "metrics": "Metrics"
6
+ },
7
+ "system": {
8
+ "title": "System",
9
+ "addons": "Addons",
10
+ "agents": "Agents",
11
+ "processes": "Processes",
12
+ "logs": "Logs",
13
+ "users": "Users",
14
+ "settings": "Settings",
15
+ "repl": "REPL",
16
+ "models": "Models"
17
+ },
18
+ "common": {
19
+ "save": "Save",
20
+ "cancel": "Cancel",
21
+ "delete": "Delete",
22
+ "enable": "Enable",
23
+ "disable": "Disable",
24
+ "edit": "Edit",
25
+ "add": "Add",
26
+ "remove": "Remove",
27
+ "confirm": "Confirm",
28
+ "close": "Close",
29
+ "back": "Back",
30
+ "next": "Next",
31
+ "search": "Search",
32
+ "filter": "Filter",
33
+ "reset": "Reset",
34
+ "apply": "Apply",
35
+ "submit": "Submit",
36
+ "loading": "Loading…",
37
+ "error": "Error",
38
+ "success": "Success",
39
+ "warning": "Warning",
40
+ "info": "Info",
41
+ "noData": "No data available",
42
+ "noResults": "No results found",
43
+ "retry": "Retry",
44
+ "refresh": "Refresh",
45
+ "copy": "Copy",
46
+ "copied": "Copied!",
47
+ "yes": "Yes",
48
+ "no": "No"
49
+ },
50
+ "status": {
51
+ "online": "Online",
52
+ "offline": "Offline",
53
+ "connecting": "Connecting",
54
+ "error": "Error",
55
+ "disabled": "Disabled",
56
+ "enabled": "Enabled",
57
+ "active": "Active",
58
+ "inactive": "Inactive",
59
+ "running": "Running",
60
+ "stopped": "Stopped",
61
+ "pending": "Pending",
62
+ "unknown": "Unknown"
63
+ },
64
+ "auth": {
65
+ "login": "Login",
66
+ "logout": "Logout",
67
+ "username": "Username",
68
+ "password": "Password",
69
+ "signIn": "Sign In",
70
+ "invalidCredentials": "Invalid username or password"
71
+ },
72
+ "theme": {
73
+ "dark": "Dark",
74
+ "light": "Light",
75
+ "system": "System"
76
+ },
77
+ "language": {
78
+ "select": "Language",
79
+ "en": "English",
80
+ "it": "Italian"
81
+ },
82
+ "pages": {
83
+ "dashboard": {
84
+ "title": "Dashboard",
85
+ "subtitle": "Overview of all devices and their status"
86
+ },
87
+ "integrations": {
88
+ "title": "Integrations",
89
+ "subtitle": "Manage camera sources and provider connections"
90
+ },
91
+ "metrics": {
92
+ "title": "Metrics",
93
+ "subtitle": "System performance and usage statistics"
94
+ },
95
+ "agents": {
96
+ "title": "Agents",
97
+ "subtitle": "Manage distributed processing agents"
98
+ },
99
+ "addons": {
100
+ "title": "Addons",
101
+ "subtitle": "Extend functionality with addon modules"
102
+ },
103
+ "settings": {
104
+ "title": "Settings",
105
+ "subtitle": "Configure system-wide preferences"
106
+ },
107
+ "users": {
108
+ "title": "Users",
109
+ "subtitle": "Manage user accounts and permissions"
110
+ },
111
+ "logs": {
112
+ "title": "Logs",
113
+ "subtitle": "View system and device logs"
114
+ }
115
+ },
116
+ "integrations": {
117
+ "preview": "Preview",
118
+ "snapshotUnavailable": "Snapshot unavailable",
119
+ "noSnapshot": "No snapshot",
120
+ "lastSnapshot": "Last snapshot:",
121
+ "offline": "Offline",
122
+ "active": "Active",
123
+ "available": "Available",
124
+ "noDevices": "No devices.",
125
+ "addFirstDevice": "Add your first device to get started.",
126
+ "import": "+ Import",
127
+ "noProviders": "No providers available. Install a provider addon.",
128
+ "manage": "Manage",
129
+ "integrationName": "Integration name",
130
+ "integrationNamePlaceholder": "e.g. Garage Server",
131
+ "connectionSuccess": "Connection successful!",
132
+ "errorPrefix": "Error:",
133
+ "back": "Back",
134
+ "testing": "Testing...",
135
+ "testConnection": "Test connection",
136
+ "forward": "Next",
137
+ "searchingDevices": "Searching for devices...",
138
+ "foundDevices": "Found {{count}} devices. Select the ones to import.",
139
+ "noDevicesFound": "No devices found.",
140
+ "skip": "Skip",
141
+ "importSelected": "Import selected",
142
+ "newIntegration": "New Integration",
143
+ "configureIntegration": "Configure Integration",
144
+ "importDevices": "Import Devices",
145
+ "addDevice": "Add device",
146
+ "name": "Name",
147
+ "namePlaceholder": "e.g. Entrance Camera",
148
+ "snapshotUrlOptional": "Snapshot URL (optional)",
149
+ "streamUrls": "Stream URLs",
150
+ "addStream": "Add stream",
151
+ "cancel": "Cancel",
152
+ "saving": "Saving...",
153
+ "save": "Save",
154
+ "saveAndNew": "Save and new",
155
+ "test": "Test",
156
+ "stream": "Stream {{number}}:",
157
+ "devices": "devices",
158
+ "rediscover": "Rediscover",
159
+ "config": "Config",
160
+ "integrationNotFound": "Integration not found.",
161
+ "configComingSoon": "Config coming soon.",
162
+ "providerConfig": "Provider Config"
163
+ }
164
+ }
@@ -0,0 +1,29 @@
1
+ import i18n from 'i18next'
2
+ import { initReactI18next } from 'react-i18next'
3
+ import en from './en.json'
4
+ import it from './it.json'
5
+
6
+ const STORAGE_KEY = 'camstack_language'
7
+
8
+ const savedLanguage = localStorage.getItem(STORAGE_KEY) ?? 'en'
9
+
10
+ i18n
11
+ .use(initReactI18next)
12
+ .init({
13
+ resources: {
14
+ en: { translation: en },
15
+ it: { translation: it },
16
+ },
17
+ lng: savedLanguage,
18
+ fallbackLng: 'en',
19
+ interpolation: {
20
+ escapeValue: false, // React already escapes values
21
+ },
22
+ })
23
+
24
+ // Persist language changes
25
+ i18n.on('languageChanged', (lng) => {
26
+ localStorage.setItem(STORAGE_KEY, lng)
27
+ })
28
+
29
+ export default i18n
@@ -0,0 +1,164 @@
1
+ {
2
+ "nav": {
3
+ "dashboard": "Dashboard",
4
+ "integrations": "Integrazioni",
5
+ "metrics": "Metriche"
6
+ },
7
+ "system": {
8
+ "title": "Sistema",
9
+ "addons": "Addon",
10
+ "agents": "Agenti",
11
+ "processes": "Processi",
12
+ "logs": "Log",
13
+ "users": "Utenti",
14
+ "settings": "Impostazioni",
15
+ "repl": "REPL",
16
+ "models": "Modelli"
17
+ },
18
+ "common": {
19
+ "save": "Salva",
20
+ "cancel": "Annulla",
21
+ "delete": "Elimina",
22
+ "enable": "Abilita",
23
+ "disable": "Disabilita",
24
+ "edit": "Modifica",
25
+ "add": "Aggiungi",
26
+ "remove": "Rimuovi",
27
+ "confirm": "Conferma",
28
+ "close": "Chiudi",
29
+ "back": "Indietro",
30
+ "next": "Avanti",
31
+ "search": "Cerca",
32
+ "filter": "Filtra",
33
+ "reset": "Reimposta",
34
+ "apply": "Applica",
35
+ "submit": "Invia",
36
+ "loading": "Caricamento…",
37
+ "error": "Errore",
38
+ "success": "Successo",
39
+ "warning": "Avviso",
40
+ "info": "Info",
41
+ "noData": "Nessun dato disponibile",
42
+ "noResults": "Nessun risultato trovato",
43
+ "retry": "Riprova",
44
+ "refresh": "Aggiorna",
45
+ "copy": "Copia",
46
+ "copied": "Copiato!",
47
+ "yes": "Sì",
48
+ "no": "No"
49
+ },
50
+ "status": {
51
+ "online": "Online",
52
+ "offline": "Offline",
53
+ "connecting": "Connessione in corso",
54
+ "error": "Errore",
55
+ "disabled": "Disabilitato",
56
+ "enabled": "Abilitato",
57
+ "active": "Attivo",
58
+ "inactive": "Inattivo",
59
+ "running": "In esecuzione",
60
+ "stopped": "Fermato",
61
+ "pending": "In attesa",
62
+ "unknown": "Sconosciuto"
63
+ },
64
+ "auth": {
65
+ "login": "Accesso",
66
+ "logout": "Esci",
67
+ "username": "Nome utente",
68
+ "password": "Password",
69
+ "signIn": "Accedi",
70
+ "invalidCredentials": "Nome utente o password non validi"
71
+ },
72
+ "theme": {
73
+ "dark": "Scuro",
74
+ "light": "Chiaro",
75
+ "system": "Sistema"
76
+ },
77
+ "language": {
78
+ "select": "Lingua",
79
+ "en": "Inglese",
80
+ "it": "Italiano"
81
+ },
82
+ "pages": {
83
+ "dashboard": {
84
+ "title": "Dashboard",
85
+ "subtitle": "Panoramica di tutti i dispositivi e il loro stato"
86
+ },
87
+ "integrations": {
88
+ "title": "Integrazioni",
89
+ "subtitle": "Gestisci le sorgenti video e le connessioni ai provider"
90
+ },
91
+ "metrics": {
92
+ "title": "Metriche",
93
+ "subtitle": "Statistiche di prestazioni e utilizzo del sistema"
94
+ },
95
+ "agents": {
96
+ "title": "Agenti",
97
+ "subtitle": "Gestisci gli agenti di elaborazione distribuita"
98
+ },
99
+ "addons": {
100
+ "title": "Addon",
101
+ "subtitle": "Estendi le funzionalità con moduli addon"
102
+ },
103
+ "settings": {
104
+ "title": "Impostazioni",
105
+ "subtitle": "Configura le preferenze di sistema"
106
+ },
107
+ "users": {
108
+ "title": "Utenti",
109
+ "subtitle": "Gestisci gli account utente e i permessi"
110
+ },
111
+ "logs": {
112
+ "title": "Log",
113
+ "subtitle": "Visualizza i log di sistema e dei dispositivi"
114
+ }
115
+ },
116
+ "integrations": {
117
+ "preview": "Anteprima",
118
+ "snapshotUnavailable": "Snapshot non disponibile",
119
+ "noSnapshot": "Nessuno snapshot",
120
+ "lastSnapshot": "Ultimo snapshot:",
121
+ "offline": "Offline",
122
+ "active": "Attivi",
123
+ "available": "Disponibili",
124
+ "noDevices": "Nessun dispositivo.",
125
+ "addFirstDevice": "Aggiungi il primo dispositivo per iniziare.",
126
+ "import": "+ Importa",
127
+ "noProviders": "Nessun provider disponibile. Installa un addon provider.",
128
+ "manage": "Gestisci",
129
+ "integrationName": "Nome integrazione",
130
+ "integrationNamePlaceholder": "es. Server Garage",
131
+ "connectionSuccess": "Connessione riuscita!",
132
+ "errorPrefix": "Errore:",
133
+ "back": "Indietro",
134
+ "testing": "Test...",
135
+ "testConnection": "Test connessione",
136
+ "forward": "Avanti",
137
+ "searchingDevices": "Ricerca dispositivi in corso...",
138
+ "foundDevices": "Trovati {{count}} dispositivi. Seleziona quelli da importare.",
139
+ "noDevicesFound": "Nessun dispositivo trovato.",
140
+ "skip": "Salta",
141
+ "importSelected": "Importa selezionati",
142
+ "newIntegration": "Nuova Integrazione",
143
+ "configureIntegration": "Configura Integrazione",
144
+ "importDevices": "Importa Dispositivi",
145
+ "addDevice": "Aggiungi dispositivo",
146
+ "name": "Nome",
147
+ "namePlaceholder": "es. Camera Ingresso",
148
+ "snapshotUrlOptional": "Snapshot URL (opzionale)",
149
+ "streamUrls": "Stream URLs",
150
+ "addStream": "Aggiungi stream",
151
+ "cancel": "Annulla",
152
+ "saving": "Salvataggio...",
153
+ "save": "Salva",
154
+ "saveAndNew": "Salva e nuova",
155
+ "test": "Test",
156
+ "stream": "Stream {{number}}:",
157
+ "devices": "dispositivi",
158
+ "rediscover": "Riscopri",
159
+ "config": "Config",
160
+ "integrationNotFound": "Integrazione non trovata.",
161
+ "configComingSoon": "Config coming soon.",
162
+ "providerConfig": "Provider Config"
163
+ }
164
+ }
package/src/index.css ADDED
@@ -0,0 +1,63 @@
1
+ @import "tailwindcss";
2
+ @import "@camstack/ui-library/tailwind/camstack-theme.css";
3
+
4
+ /* Scan ui-library source for Tailwind classes used in shared components */
5
+ @source "../../ui-library/src";
6
+
7
+ /* Force dark mode by default until ThemeProvider kicks in */
8
+ :root {
9
+ color-scheme: dark;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
15
+ -webkit-font-smoothing: antialiased;
16
+ -moz-osx-font-smoothing: grayscale;
17
+ }
18
+
19
+ /* No global transitions — use Tailwind transition-* utilities on specific elements */
20
+
21
+ /* Override react-grid-layout default styles to match our theme */
22
+ .react-grid-item.react-grid-placeholder {
23
+ background-color: var(--color-primary) !important;
24
+ opacity: 0.15 !important;
25
+ border-radius: 8px !important;
26
+ }
27
+
28
+ .react-grid-item > .react-resizable-handle::after {
29
+ border-right-color: var(--color-foreground-subtle) !important;
30
+ border-bottom-color: var(--color-foreground-subtle) !important;
31
+ }
32
+
33
+ /* Scrollbar styling */
34
+ ::-webkit-scrollbar {
35
+ width: 6px;
36
+ height: 6px;
37
+ }
38
+
39
+ ::-webkit-scrollbar-track {
40
+ background: transparent;
41
+ }
42
+
43
+ ::-webkit-scrollbar-thumb {
44
+ background-color: var(--color-border);
45
+ border-radius: 3px;
46
+ }
47
+
48
+ ::-webkit-scrollbar-thumb:hover {
49
+ background-color: var(--color-foreground-subtle);
50
+ }
51
+
52
+ /* Focus ring for keyboard accessibility */
53
+ :focus-visible {
54
+ outline: 2px solid var(--color-primary);
55
+ outline-offset: 2px;
56
+ border-radius: 4px;
57
+ }
58
+
59
+ /* Input placeholder color */
60
+ input::placeholder,
61
+ textarea::placeholder {
62
+ color: var(--color-foreground-disabled);
63
+ }
@@ -0,0 +1,120 @@
1
+ import { useState, useEffect, Suspense, lazy, type ComponentType } from 'react'
2
+ import { useParams } from 'react-router-dom'
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { getBackendClient } from '../lib/backend'
5
+
6
+ export interface AddonPageInfo {
7
+ readonly id: string
8
+ readonly addonId: string
9
+ readonly label: string
10
+ readonly icon: string
11
+ readonly path: string
12
+ readonly bundle: string
13
+ readonly bundleUrl: string
14
+ }
15
+
16
+ /** Flatten the nested tRPC response into a flat AddonPageInfo */
17
+ export function flattenPages(raw: readonly unknown[]): readonly AddonPageInfo[] {
18
+ return raw.map((item: any) => ({
19
+ id: item.page.id,
20
+ addonId: item.addonId,
21
+ label: item.page.label,
22
+ icon: item.page.icon,
23
+ path: item.page.path,
24
+ bundle: item.page.bundle,
25
+ bundleUrl: item.bundleUrl,
26
+ }))
27
+ }
28
+
29
+ /** Cache for loaded page components */
30
+ const pageComponentCache = new Map<string, ComponentType<any>>()
31
+
32
+ export function AddonPageLoader() {
33
+ const { pagePath } = useParams<{ pagePath: string }>()
34
+ const client = getBackendClient()
35
+
36
+ const { data: pages } = useQuery({
37
+ queryKey: ['addon-pages'],
38
+ queryFn: async (): Promise<readonly AddonPageInfo[]> => {
39
+ const raw = await client.trpc.addonPages.listPages.query()
40
+ return flattenPages(raw as readonly unknown[])
41
+ },
42
+ staleTime: 60_000,
43
+ })
44
+
45
+ const page = pages?.find((p) => p.path === `/addon/${pagePath}`)
46
+ const [PageComponent, setPageComponent] = useState<ComponentType<any> | null>(null)
47
+ const [error, setError] = useState<string | null>(null)
48
+
49
+ useEffect(() => {
50
+ if (!page) return
51
+
52
+ const cacheKey = `${page.addonId}/${page.bundle}`
53
+
54
+ // Check cache
55
+ const cached = pageComponentCache.get(cacheKey)
56
+ if (cached) {
57
+ setPageComponent(() => cached)
58
+ return
59
+ }
60
+
61
+ // Dynamic import of the addon page module
62
+ setError(null)
63
+ setPageComponent(null)
64
+
65
+ const url = page.bundleUrl
66
+ import(/* @vite-ignore */ url)
67
+ .then((mod) => {
68
+ const Component = mod.default
69
+ if (typeof Component !== 'function') {
70
+ throw new Error(`Addon page "${page.id}" does not export a default React component`)
71
+ }
72
+ pageComponentCache.set(cacheKey, Component)
73
+ setPageComponent(() => Component)
74
+ })
75
+ .catch((err) => {
76
+ console.error(`[AddonPageLoader] Failed to load ${url}:`, err)
77
+ setError(`Failed to load addon page: ${err.message}`)
78
+ })
79
+ }, [page])
80
+
81
+ if (!pages) {
82
+ return (
83
+ <div className="flex items-center justify-center h-full">
84
+ <div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
85
+ </div>
86
+ )
87
+ }
88
+
89
+ if (!page) {
90
+ return (
91
+ <div className="flex items-center justify-center h-full text-foreground-subtle">
92
+ Addon page not found
93
+ </div>
94
+ )
95
+ }
96
+
97
+ if (error) {
98
+ return (
99
+ <div className="flex items-center justify-center h-full text-red-400 text-sm">
100
+ {error}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ if (!PageComponent) {
106
+ return (
107
+ <div className="flex items-center justify-center h-full">
108
+ <div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
109
+ </div>
110
+ )
111
+ }
112
+
113
+ return (
114
+ <PageComponent
115
+ trpc={client.trpc}
116
+ theme={{ isDark: document.documentElement.classList.contains('dark') }}
117
+ navigate={(path: string) => { window.location.href = path }}
118
+ />
119
+ )
120
+ }