@camstack/addon-admin-ui 0.1.2 → 0.1.4

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-BoVZEQ1j.js +598 -0
  2. package/dist/assets/index-DwSc8ann.css +1 -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 +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  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 -108
  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 -172
  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 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  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 -254
  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 -222
  98. package/src/pages/Integrations.tsx +0 -333
  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 -396
  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 -28
  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,104 +0,0 @@
1
- import { StatusBadge } from '../../shared/StatusBadge'
2
- import { CapabilityBadges } from '../../shared/CapabilityBadges'
3
- import { ProviderIcon } from '../../shared/ProviderIcon'
4
- import { WebRtcPlayer } from '../../shared/WebRtcPlayer'
5
- import { getBackendClient } from '../../../lib/backend'
6
-
7
- interface OverviewTabProps {
8
- device: Record<string, unknown>
9
- }
10
-
11
- export function OverviewTab({ device }: OverviewTabProps) {
12
- const id = String(device.id ?? '—')
13
- const name = String(device.name ?? '—')
14
- const type = String(device.type ?? '—')
15
- const provider = String(device.providerId ?? device.provider ?? '—')
16
- const status = String(device.status ?? 'offline')
17
- const capabilities = (device.capabilities ?? []) as string[]
18
-
19
- const client = getBackendClient()
20
- const serverUrl = (client as unknown as { serverUrl?: string }).serverUrl ?? window.location.origin
21
-
22
- return (
23
- <div className="space-y-6">
24
- {/* Live Stream Preview */}
25
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
26
- <div className="border-b border-border px-4 py-2.5">
27
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Live Preview</h2>
28
- </div>
29
- <div className="p-3">
30
- <WebRtcPlayer
31
- serverUrl={serverUrl}
32
- streamId={id}
33
- className="w-full aspect-video rounded"
34
- muted
35
- />
36
- </div>
37
- </div>
38
- {/* Device Info */}
39
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
40
- <div className="border-b border-border px-4 py-2.5">
41
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Device Info</h2>
42
- </div>
43
- <div className="divide-y divide-border">
44
- <InfoRow label="ID" value={<span className="font-mono text-xs text-foreground">{id}</span>} />
45
- <InfoRow label="Name" value={name} />
46
- <InfoRow label="Type" value={type} />
47
- <InfoRow
48
- label="Provider"
49
- value={
50
- <div className="flex items-center gap-2">
51
- <ProviderIcon type={provider} size="sm" showLabel />
52
- </div>
53
- }
54
- />
55
- <InfoRow
56
- label="Status"
57
- value={<StatusBadge status={status} />}
58
- />
59
- <InfoRow
60
- label="Capabilities"
61
- value={
62
- capabilities.length > 0 ? (
63
- <CapabilityBadges capabilities={capabilities} />
64
- ) : (
65
- <span className="text-xs text-foreground-subtle">None</span>
66
- )
67
- }
68
- />
69
- </div>
70
- </div>
71
-
72
- {/* Pipeline Status */}
73
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
74
- <div className="border-b border-border px-4 py-2.5">
75
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Pipeline Status</h2>
76
- </div>
77
- <div className="px-4 py-8 flex flex-col items-center text-center text-foreground-subtle">
78
- <p className="text-sm">Pipeline status</p>
79
- <p className="text-xs mt-1">Pipeline metrics will appear here once the device is active</p>
80
- </div>
81
- </div>
82
-
83
- {/* Last 5 Events */}
84
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
85
- <div className="border-b border-border px-4 py-2.5">
86
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Recent Events</h2>
87
- </div>
88
- <div className="px-4 py-8 flex flex-col items-center text-center text-foreground-subtle">
89
- <p className="text-sm">No recent events</p>
90
- <p className="text-xs mt-1">Events will appear here as the device generates them</p>
91
- </div>
92
- </div>
93
- </div>
94
- )
95
- }
96
-
97
- function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
98
- return (
99
- <div className="flex items-center gap-4 px-4 py-2.5">
100
- <span className="w-28 flex-shrink-0 text-xs text-foreground-subtle">{label}</span>
101
- <div className="text-xs text-foreground">{value}</div>
102
- </div>
103
- )
104
- }
@@ -1,34 +0,0 @@
1
- import { Settings } from 'lucide-react'
2
-
3
- export function ProviderSettingsTab() {
4
- return (
5
- <div className="p-4 space-y-4">
6
- {/* Explanation */}
7
- <div className="rounded-lg border border-border bg-surface p-4">
8
- <div className="flex items-start gap-3">
9
- <div className="flex items-center justify-center rounded-full bg-primary/10 p-2 shrink-0">
10
- <Settings className="h-4 w-4 text-primary" />
11
- </div>
12
- <div>
13
- <p className="text-sm font-medium text-foreground">Provider Settings</p>
14
- <p className="mt-1 text-xs text-foreground-subtle leading-relaxed">
15
- Provider settings are loaded from the addon configuration schema. Each provider
16
- addon declares its own settings using a declarative schema that is rendered
17
- automatically as a form.
18
- </p>
19
- </div>
20
- </div>
21
- </div>
22
-
23
- {/* Placeholder */}
24
- <div className="flex flex-col items-center justify-center py-12 text-center rounded-lg border border-dashed border-border">
25
- <p className="text-xs text-foreground-subtle">
26
- Select an addon to configure provider-level settings.
27
- </p>
28
- <p className="text-[10px] text-foreground-disabled mt-1">
29
- Settings will be rendered here using the FormBuilder once a provider addon is selected.
30
- </p>
31
- </div>
32
- </div>
33
- )
34
- }
@@ -1,47 +0,0 @@
1
- import { CircleDot, Film } from 'lucide-react'
2
-
3
- export function RecordingTab() {
4
- const isRecording = false
5
-
6
- return (
7
- <div className="space-y-4">
8
- {/* Recording status */}
9
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
10
- <div className="border-b border-border px-4 py-2.5">
11
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Recording Status</h2>
12
- </div>
13
- <div className="px-4 py-4 flex items-center gap-3">
14
- <div
15
- className={`flex items-center justify-center rounded-full p-2 ${
16
- isRecording ? 'bg-danger/10' : 'bg-foreground-subtle/10'
17
- }`}
18
- >
19
- <CircleDot
20
- className={`h-5 w-5 ${isRecording ? 'text-danger' : 'text-foreground-subtle'}`}
21
- />
22
- </div>
23
- <div>
24
- <p className={`text-sm font-medium ${isRecording ? 'text-danger' : 'text-foreground-subtle'}`}>
25
- {isRecording ? 'Recording' : 'Not Recording'}
26
- </p>
27
- <p className="text-[11px] text-foreground-subtle mt-0.5">
28
- {isRecording ? 'Recording in progress' : 'No active recording session'}
29
- </p>
30
- </div>
31
- </div>
32
- </div>
33
-
34
- {/* Segments */}
35
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
36
- <div className="border-b border-border px-4 py-2.5">
37
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Recorded Segments</h2>
38
- </div>
39
- <div className="flex flex-col items-center justify-center py-12 text-foreground-subtle">
40
- <Film className="h-8 w-8 mb-3 opacity-30" />
41
- <p className="text-sm">No recorded segments</p>
42
- <p className="text-xs mt-1 opacity-70">Recorded clips will appear here</p>
43
- </div>
44
- </div>
45
- </div>
46
- )
47
- }
@@ -1,153 +0,0 @@
1
- import { useState, useRef } from 'react'
2
- import { useMutation } from '@tanstack/react-query'
3
- import { Play, ChevronUp, Trash2 } from 'lucide-react'
4
- import { useBackendClient } from '../../../hooks/useBackendClient'
5
-
6
- interface ReplTabProps {
7
- deviceId: string
8
- }
9
-
10
- interface HistoryEntry {
11
- id: number
12
- code: string
13
- result: string
14
- error: boolean
15
- ts: string
16
- }
17
-
18
- let historyCounter = 0
19
-
20
- export function ReplTab({ deviceId }: ReplTabProps) {
21
- const client = useBackendClient()
22
- const [code, setCode] = useState('')
23
- const [history, setHistory] = useState<HistoryEntry[]>([])
24
- const textareaRef = useRef<HTMLTextAreaElement>(null)
25
-
26
- const evalMutation = useMutation({
27
- mutationFn: (src: string) =>
28
- client.replEval(src, { type: 'device' as const, deviceId }),
29
- onSuccess: (data, src) => {
30
- const result = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
31
- setHistory((prev) => [
32
- ...prev,
33
- {
34
- id: ++historyCounter,
35
- code: src,
36
- result,
37
- error: false,
38
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
39
- },
40
- ])
41
- setCode('')
42
- },
43
- onError: (err: unknown, src) => {
44
- const result = err instanceof Error ? err.message : String(err)
45
- setHistory((prev) => [
46
- ...prev,
47
- {
48
- id: ++historyCounter,
49
- code: src,
50
- result,
51
- error: true,
52
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
53
- },
54
- ])
55
- },
56
- })
57
-
58
- function handleExecute() {
59
- const trimmed = code.trim()
60
- if (!trimmed) return
61
- evalMutation.mutate(trimmed)
62
- }
63
-
64
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
65
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
66
- e.preventDefault()
67
- handleExecute()
68
- }
69
- }
70
-
71
- function reuseCode(src: string) {
72
- setCode(src)
73
- textareaRef.current?.focus()
74
- }
75
-
76
- return (
77
- <div className="space-y-4">
78
- <div className="flex items-center justify-between">
79
- <p className="text-xs text-foreground-subtle">
80
- Scoped to device: <span className="font-mono text-foreground">{deviceId}</span>
81
- </p>
82
- {history.length > 0 && (
83
- <button
84
- onClick={() => setHistory([])}
85
- className="inline-flex items-center gap-1 text-[10px] text-foreground-subtle hover:text-foreground"
86
- >
87
- <Trash2 className="h-3 w-3" />
88
- Clear history
89
- </button>
90
- )}
91
- </div>
92
-
93
- {/* Input */}
94
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
95
- <div className="flex items-center justify-between border-b border-border px-3 py-1.5">
96
- <span className="text-[10px] text-foreground-subtle font-mono">JavaScript — Ctrl+Enter to run</span>
97
- <button
98
- onClick={handleExecute}
99
- disabled={evalMutation.isPending || !code.trim()}
100
- className="inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-[10px] font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
101
- >
102
- <Play className="h-3 w-3" />
103
- {evalMutation.isPending ? 'Running...' : 'Execute'}
104
- </button>
105
- </div>
106
- <textarea
107
- ref={textareaRef}
108
- value={code}
109
- onChange={(e) => setCode(e.target.value)}
110
- onKeyDown={handleKeyDown}
111
- rows={5}
112
- placeholder="// Enter JavaScript code scoped to this device..."
113
- className="w-full resize-y bg-background p-3 font-mono text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none"
114
- spellCheck={false}
115
- />
116
- </div>
117
-
118
- {/* History */}
119
- {history.length === 0 && (
120
- <div className="text-xs text-foreground-subtle">No evaluations yet</div>
121
- )}
122
-
123
- {history.length > 0 && (
124
- <div className="space-y-3">
125
- {[...history].reverse().map((entry) => (
126
- <div key={entry.id} className="rounded-lg border border-border bg-surface overflow-hidden">
127
- <div className="flex items-start justify-between gap-2 border-b border-border bg-background px-3 py-2">
128
- <pre className="font-mono text-xs text-foreground whitespace-pre-wrap break-all flex-1">{entry.code}</pre>
129
- <div className="flex items-center gap-1.5 flex-shrink-0">
130
- <span className="text-[10px] text-foreground-subtle">{entry.ts}</span>
131
- <button
132
- onClick={() => reuseCode(entry.code)}
133
- title="Reuse"
134
- className="text-foreground-subtle hover:text-foreground"
135
- >
136
- <ChevronUp className="h-3.5 w-3.5" />
137
- </button>
138
- </div>
139
- </div>
140
- <pre
141
- className={`px-3 py-2 font-mono text-xs whitespace-pre-wrap break-all ${
142
- entry.error ? 'text-danger' : 'text-success'
143
- }`}
144
- >
145
- {entry.result}
146
- </pre>
147
- </div>
148
- ))}
149
- </div>
150
- )}
151
- </div>
152
- )
153
- }
@@ -1,49 +0,0 @@
1
- import { useState } from 'react'
2
- import { Footprints } from 'lucide-react'
3
-
4
- export function TrackTrailTab() {
5
- const [trailEnabled, setTrailEnabled] = useState(false)
6
-
7
- return (
8
- <div className="space-y-4">
9
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
10
- <div className="border-b border-border px-4 py-2.5">
11
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Track Trail Config</h2>
12
- <p className="text-[11px] text-foreground-subtle mt-0.5">Visualize movement trails for tracked objects</p>
13
- </div>
14
- <div className="px-4 py-3">
15
- <div className="flex items-center justify-between">
16
- <div>
17
- <p className="text-xs font-medium text-foreground">Enable Trail Visualization</p>
18
- <p className="text-[11px] text-foreground-subtle mt-0.5">Show movement history for tracked objects</p>
19
- </div>
20
- <button
21
- onClick={() => setTrailEnabled((v) => !v)}
22
- className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
23
- trailEnabled ? 'bg-primary' : 'bg-foreground-subtle/30'
24
- }`}
25
- >
26
- <span
27
- className={`inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition-transform ${
28
- trailEnabled ? 'translate-x-4' : 'translate-x-0'
29
- }`}
30
- />
31
- </button>
32
- </div>
33
- </div>
34
- </div>
35
-
36
- {/* Active trails */}
37
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
38
- <div className="border-b border-border px-4 py-2.5">
39
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Active Trails</h2>
40
- </div>
41
- <div className="flex flex-col items-center justify-center py-12 text-foreground-subtle">
42
- <Footprints className="h-8 w-8 mb-3 opacity-30" />
43
- <p className="text-sm">No active trails</p>
44
- <p className="text-xs mt-1 opacity-70">Trails appear when objects are being tracked</p>
45
- </div>
46
- </div>
47
- </div>
48
- )
49
- }
@@ -1,98 +0,0 @@
1
- import { useState } from 'react'
2
- import { ZoneCanvas } from '../zone-editor/ZoneCanvas'
3
- import { ZoneList } from '../zone-editor/ZoneList'
4
- import { ZoneForm } from '../zone-editor/ZoneForm'
5
- import type { Zone } from '../zone-editor/ZoneCanvas'
6
-
7
- // ---------------------------------------------------------------------------
8
- // ZonesTab
9
- // ---------------------------------------------------------------------------
10
-
11
- export function ZonesTab() {
12
- const [zones, setZones] = useState<Zone[]>([])
13
- const [selectedZoneId, setSelectedZoneId] = useState<string | null>(null)
14
- const [drawingType, setDrawingType] = useState<'intrusion' | 'package-watch' | 'tripwire' | null>(null)
15
-
16
- const selectedZone = zones.find((z) => z.id === selectedZoneId) ?? null
17
-
18
- const handleZoneComplete = (zone: Zone) => {
19
- setZones((prev) => [...prev, zone])
20
- setSelectedZoneId(zone.id)
21
- setDrawingType(null)
22
- }
23
-
24
- const handleZonePointsChange = (id: string, points: { x: number; y: number }[]) => {
25
- setZones((prev) => prev.map((z) => (z.id === id ? { ...z, points } : z)))
26
- }
27
-
28
- const handleZoneChange = (updated: Zone) => {
29
- setZones((prev) => prev.map((z) => (z.id === updated.id ? updated : z)))
30
- }
31
-
32
- const handleDeleteZone = (id: string) => {
33
- setZones((prev) => prev.filter((z) => z.id !== id))
34
- if (selectedZoneId === id) setSelectedZoneId(null)
35
- }
36
-
37
- const handleStartDraw = (type: 'intrusion' | 'package-watch' | 'tripwire') => {
38
- setSelectedZoneId(null)
39
- setDrawingType(type)
40
- }
41
-
42
- return (
43
- <div className="space-y-4">
44
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
45
- <div className="border-b border-border px-4 py-2.5 flex items-center justify-between">
46
- <div>
47
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Zone Editor</h2>
48
- <p className="text-[11px] text-foreground-subtle mt-0.5">
49
- Define detection zones and tripwire lines
50
- </p>
51
- </div>
52
- <span className="text-[10px] text-foreground-subtle">
53
- {zones.length} zone{zones.length !== 1 ? 's' : ''}
54
- </span>
55
- </div>
56
-
57
- {/* Main layout: canvas + sidebar */}
58
- <div className="flex flex-col lg:flex-row">
59
- {/* Canvas area */}
60
- <div className="flex-1 p-4">
61
- <ZoneCanvas
62
- zones={zones}
63
- selectedZoneId={selectedZoneId}
64
- onSelectZone={setSelectedZoneId}
65
- onZonePointsChange={handleZonePointsChange}
66
- onZoneComplete={handleZoneComplete}
67
- drawingType={drawingType}
68
- />
69
- </div>
70
-
71
- {/* Sidebar */}
72
- <div className="w-full lg:w-64 border-t lg:border-t-0 lg:border-l border-border p-4 flex flex-col gap-4">
73
- <ZoneList
74
- zones={zones}
75
- selectedZoneId={selectedZoneId}
76
- drawingType={drawingType}
77
- onSelectZone={setSelectedZoneId}
78
- onDeleteZone={handleDeleteZone}
79
- onStartDraw={handleStartDraw}
80
- onCancelDraw={() => setDrawingType(null)}
81
- />
82
-
83
- {selectedZone && !drawingType && (
84
- <>
85
- <div className="border-t border-border" />
86
- <ZoneForm
87
- zone={selectedZone}
88
- onChange={handleZoneChange}
89
- onDelete={() => handleDeleteZone(selectedZone.id)}
90
- />
91
- </>
92
- )}
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
- )
98
- }