@camstack/addon-admin-ui 0.1.1 → 0.1.3

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 (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +5 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -339
  10. package/src/components/addons/AddonUploadZone.tsx +0 -307
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -119
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -171
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -113
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -90
  69. package/src/components/metrics/PipelineStatus.tsx +0 -105
  70. package/src/components/metrics/ProcessResources.tsx +0 -139
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -238
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -224
  98. package/src/pages/Integrations.tsx +0 -330
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -525
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -210
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,307 +0,0 @@
1
- import { useState, useRef, useCallback } from 'react'
2
- import { Upload, FileArchive, X, Loader2, CheckCircle, AlertCircle } from 'lucide-react'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Types
6
- // ---------------------------------------------------------------------------
7
-
8
- type UploadStatus = 'idle' | 'dragging' | 'uploading' | 'success' | 'error'
9
-
10
- interface UploadResult {
11
- success: boolean
12
- packageName?: string
13
- version?: string
14
- error?: string
15
- }
16
-
17
- interface AddonUploadZoneProps {
18
- onUploadSuccess: () => void
19
- }
20
-
21
- // ---------------------------------------------------------------------------
22
- // Accepted file extensions
23
- // ---------------------------------------------------------------------------
24
-
25
- const ACCEPTED_EXTENSIONS = ['.tgz', '.tar.gz', '.zip']
26
- const ACCEPT_MIME = '.tgz,.tar.gz,.zip'
27
-
28
- function isAcceptedFile(file: File): boolean {
29
- const name = file.name.toLowerCase()
30
- return ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext))
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // AddonUploadZone
35
- // ---------------------------------------------------------------------------
36
-
37
- export function AddonUploadZone({ onUploadSuccess }: AddonUploadZoneProps) {
38
- const [status, setStatus] = useState<UploadStatus>('idle')
39
- const [progress, setProgress] = useState(0)
40
- const [selectedFile, setSelectedFile] = useState<File | null>(null)
41
- const [result, setResult] = useState<UploadResult | null>(null)
42
- const fileInputRef = useRef<HTMLInputElement>(null)
43
-
44
- const resetState = useCallback(() => {
45
- setStatus('idle')
46
- setProgress(0)
47
- setSelectedFile(null)
48
- setResult(null)
49
- }, [])
50
-
51
- const uploadFile = useCallback(
52
- async (file: File) => {
53
- if (!isAcceptedFile(file)) {
54
- setStatus('error')
55
- setResult({ success: false, error: `Invalid file type. Accepted: ${ACCEPTED_EXTENSIONS.join(', ')}` })
56
- return
57
- }
58
-
59
- setSelectedFile(file)
60
- setStatus('uploading')
61
- setProgress(0)
62
- setResult(null)
63
-
64
- const formData = new FormData()
65
- formData.append('file', file)
66
-
67
- const token = localStorage.getItem('camstack_admin_token') ?? ''
68
-
69
- try {
70
- const xhr = new XMLHttpRequest()
71
-
72
- const uploadResult = await new Promise<UploadResult>((resolve, reject) => {
73
- xhr.upload.addEventListener('progress', (event) => {
74
- if (event.lengthComputable) {
75
- const pct = Math.round((event.loaded / event.total) * 100)
76
- setProgress(pct)
77
- }
78
- })
79
-
80
- xhr.addEventListener('load', () => {
81
- try {
82
- const body = JSON.parse(xhr.responseText)
83
- if (xhr.status >= 200 && xhr.status < 300 && body.success) {
84
- resolve({
85
- success: true,
86
- packageName: body.packageName,
87
- version: body.version,
88
- })
89
- } else {
90
- resolve({
91
- success: false,
92
- error: body.error ?? `Upload failed (HTTP ${xhr.status})`,
93
- })
94
- }
95
- } catch {
96
- resolve({ success: false, error: `Unexpected response (HTTP ${xhr.status})` })
97
- }
98
- })
99
-
100
- xhr.addEventListener('error', () => {
101
- reject(new Error('Network error'))
102
- })
103
-
104
- xhr.addEventListener('abort', () => {
105
- reject(new Error('Upload aborted'))
106
- })
107
-
108
- xhr.open('POST', '/api/addons/upload')
109
- xhr.setRequestHeader('Authorization', `Bearer ${token}`)
110
- xhr.send(formData)
111
- })
112
-
113
- setResult(uploadResult)
114
- setStatus(uploadResult.success ? 'success' : 'error')
115
-
116
- if (uploadResult.success) {
117
- onUploadSuccess()
118
- }
119
- } catch (err) {
120
- const message = err instanceof Error ? err.message : 'Upload failed'
121
- setResult({ success: false, error: message })
122
- setStatus('error')
123
- }
124
- },
125
- [onUploadSuccess],
126
- )
127
-
128
- // --- Drag event handlers ---
129
-
130
- const handleDragEnter = useCallback((e: React.DragEvent) => {
131
- e.preventDefault()
132
- e.stopPropagation()
133
- setStatus((prev) => (prev === 'uploading' ? prev : 'dragging'))
134
- }, [])
135
-
136
- const handleDragLeave = useCallback((e: React.DragEvent) => {
137
- e.preventDefault()
138
- e.stopPropagation()
139
- // Only leave when exiting the drop zone itself
140
- if (e.currentTarget.contains(e.relatedTarget as Node)) return
141
- setStatus((prev) => (prev === 'uploading' ? prev : 'idle'))
142
- }, [])
143
-
144
- const handleDragOver = useCallback((e: React.DragEvent) => {
145
- e.preventDefault()
146
- e.stopPropagation()
147
- }, [])
148
-
149
- const handleDrop = useCallback(
150
- (e: React.DragEvent) => {
151
- e.preventDefault()
152
- e.stopPropagation()
153
- setStatus('idle')
154
-
155
- const file = e.dataTransfer.files[0]
156
- if (file) {
157
- uploadFile(file)
158
- }
159
- },
160
- [uploadFile],
161
- )
162
-
163
- const handleFileSelect = useCallback(
164
- (e: React.ChangeEvent<HTMLInputElement>) => {
165
- const file = e.target.files?.[0]
166
- if (file) {
167
- uploadFile(file)
168
- }
169
- // Reset input so the same file can be re-selected
170
- e.target.value = ''
171
- },
172
- [uploadFile],
173
- )
174
-
175
- const handleBrowseClick = useCallback(() => {
176
- fileInputRef.current?.click()
177
- }, [])
178
-
179
- // --- Render helpers ---
180
-
181
- const isIdle = status === 'idle' || status === 'dragging'
182
- const isDragging = status === 'dragging'
183
-
184
- return (
185
- <div className="space-y-2">
186
- {/* Hidden file input */}
187
- <input
188
- ref={fileInputRef}
189
- type="file"
190
- accept={ACCEPT_MIME}
191
- onChange={handleFileSelect}
192
- className="hidden"
193
- />
194
-
195
- {/* Drop zone */}
196
- <div
197
- onDragEnter={handleDragEnter}
198
- onDragLeave={handleDragLeave}
199
- onDragOver={handleDragOver}
200
- onDrop={handleDrop}
201
- onClick={isIdle ? handleBrowseClick : undefined}
202
- className={[
203
- 'relative rounded-lg border-2 border-dashed transition-all cursor-pointer',
204
- 'flex flex-col items-center justify-center gap-2 px-6 py-6',
205
- isDragging
206
- ? 'border-primary bg-primary/5 scale-[1.01]'
207
- : status === 'uploading'
208
- ? 'border-border bg-surface cursor-default'
209
- : status === 'success'
210
- ? 'border-green-500/40 bg-green-500/5 cursor-default'
211
- : status === 'error'
212
- ? 'border-red-500/40 bg-red-500/5 cursor-default'
213
- : 'border-border bg-surface hover:border-primary/50 hover:bg-surface-hover/50',
214
- ].join(' ')}
215
- >
216
- {/* Idle / dragging state */}
217
- {isIdle && (
218
- <>
219
- <Upload className={`h-6 w-6 ${isDragging ? 'text-primary' : 'text-foreground-subtle'}`} />
220
- <div className="text-center">
221
- <p className="text-xs font-medium text-foreground">
222
- {isDragging ? 'Drop addon file here' : 'Drag & drop addon file'}
223
- </p>
224
- <p className="text-[10px] text-foreground-subtle mt-0.5">
225
- or{' '}
226
- <span className="text-primary underline underline-offset-2">browse files</span>
227
- {' '}&middot; .tgz, .tar.gz, .zip
228
- </p>
229
- </div>
230
- </>
231
- )}
232
-
233
- {/* Uploading state */}
234
- {status === 'uploading' && selectedFile && (
235
- <>
236
- <div className="flex items-center gap-2">
237
- <FileArchive className="h-5 w-5 text-primary shrink-0" />
238
- <div className="min-w-0">
239
- <p className="text-xs font-medium text-foreground truncate">{selectedFile.name}</p>
240
- <p className="text-[10px] text-foreground-subtle">
241
- {(selectedFile.size / 1024).toFixed(1)} KB
242
- </p>
243
- </div>
244
- </div>
245
-
246
- {/* Progress bar */}
247
- <div className="w-full max-w-xs">
248
- <div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
249
- <div
250
- className="h-full rounded-full bg-primary transition-all duration-300 ease-out"
251
- style={{ width: `${progress}%` }}
252
- />
253
- </div>
254
- <div className="flex items-center justify-between mt-1">
255
- <span className="text-[10px] text-foreground-subtle flex items-center gap-1">
256
- <Loader2 className="h-3 w-3 animate-spin" />
257
- Uploading...
258
- </span>
259
- <span className="text-[10px] text-foreground-subtle font-mono">{progress}%</span>
260
- </div>
261
- </div>
262
- </>
263
- )}
264
-
265
- {/* Success state */}
266
- {status === 'success' && result && (
267
- <>
268
- <CheckCircle className="h-6 w-6 text-green-500" />
269
- <div className="text-center">
270
- <p className="text-xs font-medium text-foreground">Addon installed</p>
271
- <p className="text-[10px] text-foreground-subtle mt-0.5">
272
- {result.packageName} v{result.version}
273
- </p>
274
- </div>
275
- <button
276
- type="button"
277
- onClick={(e) => { e.stopPropagation(); resetState() }}
278
- className="absolute top-2 right-2 p-1 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
279
- title="Dismiss"
280
- >
281
- <X className="h-3.5 w-3.5" />
282
- </button>
283
- </>
284
- )}
285
-
286
- {/* Error state */}
287
- {status === 'error' && result && (
288
- <>
289
- <AlertCircle className="h-6 w-6 text-red-400" />
290
- <div className="text-center">
291
- <p className="text-xs font-medium text-red-400">Upload failed</p>
292
- <p className="text-[10px] text-foreground-subtle mt-0.5">{result.error}</p>
293
- </div>
294
- <button
295
- type="button"
296
- onClick={(e) => { e.stopPropagation(); resetState() }}
297
- className="absolute top-2 right-2 p-1 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
298
- title="Dismiss"
299
- >
300
- <X className="h-3.5 w-3.5" />
301
- </button>
302
- </>
303
- )}
304
- </div>
305
- </div>
306
- )
307
- }
@@ -1,55 +0,0 @@
1
- const CAPABILITY_COLORS: Record<string, string> = {
2
- // detection / vision
3
- detection: 'bg-green-500/10 text-green-400',
4
- detector: 'bg-green-500/10 text-green-400',
5
- // streaming
6
- streaming: 'bg-blue-500/10 text-blue-400',
7
- decode: 'bg-blue-500/10 text-blue-400',
8
- decoder: 'bg-blue-500/10 text-blue-400',
9
- // recording
10
- recording: 'bg-orange-500/10 text-orange-400',
11
- recorder: 'bg-orange-500/10 text-orange-400',
12
- // transcoding
13
- transcoding: 'bg-purple-500/10 text-purple-400',
14
- transcoder: 'bg-purple-500/10 text-purple-400',
15
- transcode: 'bg-purple-500/10 text-purple-400',
16
- // restream
17
- restream: 'bg-cyan-500/10 text-cyan-400',
18
- restreamer: 'bg-cyan-500/10 text-cyan-400',
19
- // storage
20
- storage: 'bg-yellow-500/10 text-yellow-400',
21
- // notification
22
- notification: 'bg-pink-500/10 text-pink-400',
23
- notifier: 'bg-pink-500/10 text-pink-400',
24
- // faces
25
- faces: 'bg-indigo-500/10 text-indigo-400',
26
- // default
27
- }
28
-
29
- interface CapabilityBadgeProps {
30
- /** Capability name (string) or declaration object ({ name, mode }) */
31
- capability: string | { name: string; mode?: string }
32
- /** Optional mode label shown after a separator (overrides object mode) */
33
- mode?: string
34
- size?: 'sm' | 'xs'
35
- }
36
-
37
- export function CapabilityBadge({ capability, mode, size = 'xs' }: CapabilityBadgeProps) {
38
- const capName = typeof capability === 'string' ? capability : capability.name
39
- const capMode = mode ?? (typeof capability === 'object' ? capability.mode : undefined)
40
- const lower = capName.toLowerCase()
41
- const color = CAPABILITY_COLORS[lower] ?? 'bg-primary/10 text-primary'
42
- const textSize = size === 'sm' ? 'text-xs' : 'text-[10px]'
43
-
44
- return (
45
- <span className={`inline-flex items-center rounded-md px-1.5 py-0.5 font-medium gap-1 ${textSize} ${color}`}>
46
- {capName}
47
- {capMode && (
48
- <>
49
- <span className="opacity-40">·</span>
50
- <span className="opacity-70">{capMode}</span>
51
- </>
52
- )}
53
- </span>
54
- )
55
- }
@@ -1,133 +0,0 @@
1
- import { CheckCircle } from 'lucide-react'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Types
5
- // ---------------------------------------------------------------------------
6
-
7
- export type CapabilityMode = 'singleton' | 'collection'
8
-
9
- export interface CapabilityProvider {
10
- addonId: string
11
- addonName: string
12
- active: boolean
13
- }
14
-
15
- export interface CapabilityEntry {
16
- name: string
17
- mode: CapabilityMode
18
- providers: CapabilityProvider[]
19
- }
20
-
21
- // ---------------------------------------------------------------------------
22
- // Color helpers
23
- // ---------------------------------------------------------------------------
24
-
25
- const CAPABILITY_BORDER_COLORS: Record<string, string> = {
26
- detection: 'border-green-500',
27
- detector: 'border-green-500',
28
- streaming: 'border-blue-500',
29
- decode: 'border-blue-500',
30
- decoder: 'border-blue-500',
31
- recording: 'border-orange-500',
32
- recorder: 'border-orange-500',
33
- transcoding: 'border-purple-500',
34
- transcoder: 'border-purple-500',
35
- restream: 'border-cyan-500',
36
- restreamer: 'border-cyan-500',
37
- storage: 'border-yellow-500',
38
- notification: 'border-pink-500',
39
- notifier: 'border-pink-500',
40
- faces: 'border-indigo-500',
41
- }
42
-
43
- const CAPABILITY_BG_COLORS: Record<string, string> = {
44
- detection: 'bg-green-500/5',
45
- detector: 'bg-green-500/5',
46
- streaming: 'bg-blue-500/5',
47
- decode: 'bg-blue-500/5',
48
- decoder: 'bg-blue-500/5',
49
- recording: 'bg-orange-500/5',
50
- recorder: 'bg-orange-500/5',
51
- transcoding: 'bg-purple-500/5',
52
- transcoder: 'bg-purple-500/5',
53
- restream: 'bg-cyan-500/5',
54
- restreamer: 'bg-cyan-500/5',
55
- storage: 'bg-yellow-500/5',
56
- notification: 'bg-pink-500/5',
57
- notifier: 'bg-pink-500/5',
58
- faces: 'bg-indigo-500/5',
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // CapabilityCard
63
- // ---------------------------------------------------------------------------
64
-
65
- function CapabilityCard({ entry }: { entry: CapabilityEntry }) {
66
- const lower = entry.name.toLowerCase()
67
- const borderColor = CAPABILITY_BORDER_COLORS[lower] ?? 'border-primary'
68
- const bgColor = CAPABILITY_BG_COLORS[lower] ?? 'bg-primary/5'
69
- const activeProvider = entry.providers.find((p) => p.active)
70
-
71
- return (
72
- <div className={`rounded-lg border-t-2 border border-border ${borderColor} ${bgColor} p-3 space-y-2`}>
73
- <div className="flex items-center justify-between gap-2">
74
- <span className="text-xs font-semibold text-foreground capitalize">{entry.name}</span>
75
- <span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${entry.mode === 'singleton' ? 'bg-primary/10 text-primary' : 'bg-info/10 text-info'}`}>
76
- {entry.mode}
77
- </span>
78
- </div>
79
-
80
- {entry.mode === 'singleton' ? (
81
- <div>
82
- {activeProvider ? (
83
- <div className="flex items-center gap-1.5 text-[10px] text-foreground">
84
- <CheckCircle className="h-3 w-3 text-green-400 shrink-0" />
85
- <span className="font-medium">{activeProvider.addonName}</span>
86
- <span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium ml-auto">ACTIVE</span>
87
- </div>
88
- ) : (
89
- <div className="text-[10px] text-foreground-subtle italic">No active provider</div>
90
- )}
91
- </div>
92
- ) : (
93
- <div className="space-y-1">
94
- {entry.providers.length === 0 && (
95
- <div className="text-[10px] text-foreground-subtle italic">No providers</div>
96
- )}
97
- {entry.providers.map((p) => (
98
- <div key={p.addonId} className="flex items-center justify-between gap-2 text-[10px]">
99
- <span className="text-foreground-subtle">{p.addonName}</span>
100
- {p.active && (
101
- <span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium">ACTIVE</span>
102
- )}
103
- </div>
104
- ))}
105
- </div>
106
- )}
107
- </div>
108
- )
109
- }
110
-
111
- // ---------------------------------------------------------------------------
112
- // CapabilityMap
113
- // ---------------------------------------------------------------------------
114
-
115
- interface CapabilityMapProps {
116
- capabilities: CapabilityEntry[]
117
- }
118
-
119
- export function CapabilityMap({ capabilities }: CapabilityMapProps) {
120
- if (capabilities.length === 0) {
121
- return (
122
- <div className="text-xs text-foreground-subtle text-center py-8">No capabilities registered</div>
123
- )
124
- }
125
-
126
- return (
127
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
128
- {capabilities.map((cap) => (
129
- <CapabilityCard key={cap.name} entry={cap} />
130
- ))}
131
- </div>
132
- )
133
- }
@@ -1,119 +0,0 @@
1
- import { ArrowRight, RotateCcw, Zap } from 'lucide-react'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Types
5
- // ---------------------------------------------------------------------------
6
-
7
- export interface UpdateEntry {
8
- id: string
9
- name: string
10
- currentVersion: string
11
- newVersion: string
12
- changelog?: string
13
- requiresRestart: boolean
14
- }
15
-
16
- // ---------------------------------------------------------------------------
17
- // UpdatesList
18
- // ---------------------------------------------------------------------------
19
-
20
- interface UpdatesListProps {
21
- updates: UpdateEntry[]
22
- onUpdate: (id: string) => void
23
- onUpdateAll: () => void
24
- onCheckUpdates: () => void
25
- isChecking: boolean
26
- }
27
-
28
- export function UpdatesList({ updates, onUpdate, onUpdateAll, onCheckUpdates, isChecking }: UpdatesListProps) {
29
- return (
30
- <div className="space-y-4">
31
- {/* Toolbar */}
32
- <div className="flex items-center justify-between gap-3">
33
- <button
34
- type="button"
35
- onClick={onCheckUpdates}
36
- disabled={isChecking}
37
- className="flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs text-foreground hover:bg-surface transition-colors disabled:opacity-50"
38
- >
39
- <RotateCcw className={`h-3.5 w-3.5 ${isChecking ? 'animate-spin' : ''}`} />
40
- {isChecking ? 'Checking…' : 'Check for updates'}
41
- </button>
42
-
43
- {updates.length > 0 && (
44
- <button
45
- type="button"
46
- onClick={onUpdateAll}
47
- className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 transition-colors"
48
- >
49
- Update all ({updates.length})
50
- </button>
51
- )}
52
- </div>
53
-
54
- {updates.length === 0 && !isChecking && (
55
- <div className="rounded-lg border border-border bg-surface py-10 text-center text-xs text-foreground-subtle">
56
- All packages are up to date
57
- </div>
58
- )}
59
-
60
- {updates.length > 0 && (
61
- <div className="space-y-2">
62
- {updates.map((update) => (
63
- <div
64
- key={update.id}
65
- className="rounded-lg border border-border bg-surface px-4 py-3 flex items-start justify-between gap-4"
66
- >
67
- <div className="flex items-start gap-3 min-w-0">
68
- {/* Icon letter */}
69
- <div className="flex-shrink-0 h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center">
70
- <span className="text-xs font-bold text-primary uppercase">
71
- {update.name.charAt(0)}
72
- </span>
73
- </div>
74
-
75
- <div className="min-w-0">
76
- <div className="flex items-center gap-2 flex-wrap">
77
- <span className="text-sm font-semibold text-foreground">{update.name}</span>
78
- {update.requiresRestart ? (
79
- <span className="inline-flex items-center gap-1 rounded-full bg-warning/10 text-warning px-2 py-0.5 text-[10px] font-medium">
80
- <RotateCcw className="h-2.5 w-2.5" />
81
- Restart
82
- </span>
83
- ) : (
84
- <span className="inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-400 px-2 py-0.5 text-[10px] font-medium">
85
- <Zap className="h-2.5 w-2.5" />
86
- Hot-swap
87
- </span>
88
- )}
89
- </div>
90
-
91
- {/* Version diff */}
92
- <div className="flex items-center gap-1.5 mt-1 text-[10px]">
93
- <span className="text-foreground-subtle font-mono">v{update.currentVersion}</span>
94
- <ArrowRight className="h-3 w-3 text-foreground-subtle" />
95
- <span className="text-green-400 font-mono font-medium">v{update.newVersion}</span>
96
- </div>
97
-
98
- {update.changelog && (
99
- <p className="mt-1.5 text-[10px] text-foreground-subtle leading-relaxed line-clamp-2">
100
- {update.changelog}
101
- </p>
102
- )}
103
- </div>
104
- </div>
105
-
106
- <button
107
- type="button"
108
- onClick={() => onUpdate(update.id)}
109
- className="flex-shrink-0 rounded-md bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1.5 text-xs font-medium transition-colors"
110
- >
111
- Update
112
- </button>
113
- </div>
114
- ))}
115
- </div>
116
- )}
117
- </div>
118
- )
119
- }