lula2 0.0.5 → 0.0.6

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 (108) hide show
  1. package/README.md +291 -8
  2. package/dist/_app/env.js +1 -0
  3. package/dist/_app/immutable/assets/0.DtiRW3lO.css +1 -0
  4. package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
  5. package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
  6. package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
  7. package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
  8. package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
  9. package/dist/_app/immutable/chunks/Bg_R1qWi.js +3 -0
  10. package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
  11. package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
  12. package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
  13. package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
  14. package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
  15. package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
  16. package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
  17. package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
  18. package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
  19. package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
  20. package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
  21. package/dist/_app/immutable/nodes/4.Blq-4WQS.js +9 -0
  22. package/dist/_app/version.json +1 -0
  23. package/dist/cli/commands/crawl.js +128 -0
  24. package/dist/cli/commands/ui.js +2769 -0
  25. package/dist/cli/commands/version.js +30 -0
  26. package/dist/cli/server/index.js +2713 -0
  27. package/dist/cli/server/server.js +2702 -0
  28. package/dist/cli/server/serverState.js +1199 -0
  29. package/dist/cli/server/spreadsheetRoutes.js +788 -0
  30. package/dist/cli/server/types.js +0 -0
  31. package/dist/cli/server/websocketServer.js +2625 -0
  32. package/dist/cli/utils/debug.js +24 -0
  33. package/dist/favicon.svg +1 -0
  34. package/dist/index.html +38 -0
  35. package/dist/index.js +2924 -37
  36. package/dist/lula.png +0 -0
  37. package/dist/lula2 +2 -0
  38. package/package.json +120 -72
  39. package/src/app.css +192 -0
  40. package/src/app.d.ts +13 -0
  41. package/src/app.html +13 -0
  42. package/src/lib/actions/fadeWhenScrollable.ts +39 -0
  43. package/src/lib/actions/modal.ts +230 -0
  44. package/src/lib/actions/tooltip.ts +82 -0
  45. package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
  46. package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
  47. package/src/lib/components/control-sets/index.ts +5 -0
  48. package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
  49. package/src/lib/components/controls/ControlsList.svelte +608 -0
  50. package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
  51. package/src/lib/components/controls/MappingCard.svelte +105 -0
  52. package/src/lib/components/controls/MappingForm.svelte +188 -0
  53. package/src/lib/components/controls/index.ts +9 -0
  54. package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
  55. package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
  56. package/src/lib/components/controls/renderers/index.ts +5 -0
  57. package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
  58. package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
  59. package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
  60. package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
  61. package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
  62. package/src/lib/components/controls/tabs/index.ts +8 -0
  63. package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
  64. package/src/lib/components/controls/utils/textProcessor.ts +164 -0
  65. package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
  66. package/src/lib/components/forms/DynamicField.svelte +494 -0
  67. package/src/lib/components/forms/FormField.svelte +107 -0
  68. package/src/lib/components/forms/index.ts +6 -0
  69. package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
  70. package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
  71. package/src/lib/components/setup/index.ts +5 -0
  72. package/src/lib/components/ui/Dropdown.svelte +107 -0
  73. package/src/lib/components/ui/EmptyState.svelte +80 -0
  74. package/src/lib/components/ui/FeatureToggle.svelte +50 -0
  75. package/src/lib/components/ui/SearchBar.svelte +73 -0
  76. package/src/lib/components/ui/StatusBadge.svelte +79 -0
  77. package/src/lib/components/ui/TabNavigation.svelte +48 -0
  78. package/src/lib/components/ui/Tooltip.svelte +120 -0
  79. package/src/lib/components/ui/index.ts +10 -0
  80. package/src/lib/components/version-control/DiffViewer.svelte +292 -0
  81. package/src/lib/components/version-control/TimelineItem.svelte +107 -0
  82. package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
  83. package/src/lib/components/version-control/index.ts +6 -0
  84. package/src/lib/form-types.ts +57 -0
  85. package/src/lib/formatUtils.ts +17 -0
  86. package/src/lib/index.ts +5 -0
  87. package/src/lib/types.ts +180 -0
  88. package/src/lib/websocket.ts +359 -0
  89. package/src/routes/+layout.svelte +236 -0
  90. package/src/routes/+page.svelte +38 -0
  91. package/src/routes/control/[id]/+page.svelte +112 -0
  92. package/src/routes/setup/+page.svelte +241 -0
  93. package/src/stores/compliance.ts +95 -0
  94. package/src/styles/highlightjs.css +20 -0
  95. package/src/styles/modal.css +58 -0
  96. package/src/styles/tables.css +111 -0
  97. package/src/styles/tooltip.css +65 -0
  98. package/dist/controls/index.d.ts +0 -18
  99. package/dist/controls/index.d.ts.map +0 -1
  100. package/dist/controls/index.js +0 -18
  101. package/dist/crawl.d.ts +0 -62
  102. package/dist/crawl.d.ts.map +0 -1
  103. package/dist/crawl.js +0 -172
  104. package/dist/index.d.ts +0 -8
  105. package/dist/index.d.ts.map +0 -1
  106. package/src/controls/index.ts +0 -19
  107. package/src/crawl.ts +0 -227
  108. package/src/index.ts +0 -46
@@ -0,0 +1,359 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ import { browser } from '$app/environment';
5
+ import { writable } from 'svelte/store';
6
+ import type { Control, Mapping } from './types';
7
+
8
+ export interface WSMessage {
9
+ type:
10
+ | 'state-update'
11
+ | 'connected'
12
+ | 'error'
13
+ | 'metadata-update'
14
+ | 'controls-update'
15
+ | 'mappings-update'
16
+ | 'control-details'
17
+ | 'control-sets-list'
18
+ | 'control-updated'
19
+ | 'mapping-created'
20
+ | 'mapping-updated'
21
+ | 'mapping-deleted';
22
+ payload?: any;
23
+ }
24
+
25
+ export interface AppState {
26
+ // Control set properties are spread at root level
27
+ id?: string;
28
+ name?: string;
29
+ title?: string;
30
+ version?: string;
31
+ description?: string;
32
+ fieldSchema?: any;
33
+ field_schema?: any;
34
+ control_id_field?: string;
35
+ project?: {
36
+ framework?: {
37
+ baseline?: string;
38
+ };
39
+ };
40
+ // State properties
41
+ currentPath: string;
42
+ controls: Control[];
43
+ mappings: Mapping[];
44
+ families: string[];
45
+ totalControls: number;
46
+ totalMappings: number;
47
+ isConnected: boolean;
48
+ isSwitchingControlSet?: boolean;
49
+ }
50
+
51
+ // Create a writable store for the complete app state
52
+ export const appState = writable<AppState>({
53
+ id: 'unknown',
54
+ name: 'Unknown Control Set',
55
+ currentPath: '',
56
+ controls: [],
57
+ mappings: [],
58
+ families: [],
59
+ totalControls: 0,
60
+ totalMappings: 0,
61
+ isConnected: false
62
+ });
63
+
64
+ class WebSocketClient {
65
+ private ws: WebSocket | null = null;
66
+ private reconnectTimer: number | null = null;
67
+ private reconnectAttempts = 0;
68
+ private maxReconnectAttempts = 5;
69
+ private reconnectDelay = 1000; // Start with 1 second
70
+
71
+ connect() {
72
+ if (!browser) return;
73
+
74
+ // Don't create a new connection if one already exists
75
+ if (
76
+ this.ws &&
77
+ (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)
78
+ ) {
79
+ console.log('WebSocket already connected or connecting');
80
+ return;
81
+ }
82
+
83
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
84
+ // In development, WebSocket is on port 3000, not the dev server port
85
+ const host =
86
+ browser && window.location.hostname === 'localhost' ? 'localhost:3000' : window.location.host;
87
+ const wsUrl = `${protocol}//${host}/ws`;
88
+
89
+ console.log('Connecting to WebSocket:', wsUrl);
90
+
91
+ try {
92
+ this.ws = new WebSocket(wsUrl);
93
+
94
+ this.ws.onopen = () => {
95
+ console.log('WebSocket connected');
96
+ this.reconnectAttempts = 0;
97
+ this.reconnectDelay = 1000;
98
+ };
99
+
100
+ this.ws.onmessage = (event) => {
101
+ try {
102
+ const message: WSMessage = JSON.parse(event.data);
103
+ this.handleMessage(message);
104
+ } catch (error) {
105
+ console.error('Failed to parse WebSocket message:', error);
106
+ }
107
+ };
108
+
109
+ this.ws.onerror = (error) => {
110
+ console.error('WebSocket error:', error);
111
+ };
112
+
113
+ this.ws.onclose = () => {
114
+ console.log('WebSocket disconnected');
115
+ appState.update((state) => ({ ...state, isConnected: false }));
116
+ this.ws = null;
117
+ this.scheduleReconnect();
118
+ };
119
+ } catch (error) {
120
+ console.error('Failed to create WebSocket:', error);
121
+ this.scheduleReconnect();
122
+ }
123
+ }
124
+
125
+ private handleMessage(message: WSMessage) {
126
+ console.log('WebSocket message received:', message);
127
+
128
+ switch (message.type) {
129
+ case 'connected':
130
+ console.log('WebSocket connection confirmed');
131
+ appState.update((state) => ({ ...state, isConnected: true }));
132
+ break;
133
+
134
+ case 'state-update':
135
+ console.log('State update received');
136
+ // Update the entire app state with the payload
137
+ if (message.payload) {
138
+ appState.set({
139
+ ...message.payload,
140
+ isConnected: true,
141
+ isSwitchingControlSet: false // Clear switching flag after state update
142
+ });
143
+ }
144
+ break;
145
+
146
+ case 'metadata-update':
147
+ console.log('Metadata update received');
148
+ // Update metadata and control set info
149
+ if (message.payload) {
150
+ appState.update((state) => ({
151
+ ...state,
152
+ ...message.payload,
153
+ isConnected: true,
154
+ isSwitchingControlSet: false // Clear switching flag
155
+ }));
156
+ }
157
+ break;
158
+
159
+ case 'controls-update':
160
+ console.log('Controls update received');
161
+ // Update just the controls array
162
+ if (message.payload) {
163
+ appState.update((state) => ({
164
+ ...state,
165
+ controls: message.payload
166
+ }));
167
+ }
168
+ break;
169
+
170
+ case 'mappings-update':
171
+ console.log('Mappings update received');
172
+ // Update just the mappings array
173
+ if (message.payload) {
174
+ appState.update((state) => ({
175
+ ...state,
176
+ mappings: message.payload
177
+ }));
178
+ }
179
+ break;
180
+
181
+ case 'control-details':
182
+ console.log('Control details received:', message.payload);
183
+ // Emit a custom event for control details
184
+ if (message.payload) {
185
+ window.dispatchEvent(
186
+ new CustomEvent('control-details', {
187
+ detail: message.payload
188
+ })
189
+ );
190
+ }
191
+ break;
192
+
193
+ case 'control-sets-list':
194
+ console.log('Control sets list received');
195
+ // Emit a custom event for control sets list
196
+ if (message.payload) {
197
+ window.dispatchEvent(
198
+ new CustomEvent('control-sets-list', {
199
+ detail: message.payload
200
+ })
201
+ );
202
+ }
203
+ break;
204
+
205
+ case 'control-updated':
206
+ console.log('Control updated successfully:', message.payload);
207
+ // Don't trigger any state updates - the component already has the updated data
208
+ // This just confirms the save was successful
209
+ break;
210
+
211
+ case 'mapping-created':
212
+ case 'mapping-updated':
213
+ case 'mapping-deleted':
214
+ console.log(`Mapping operation successful: ${message.type}`, message.payload);
215
+ // Emit an event so the control details panel can refresh its mappings
216
+ window.dispatchEvent(
217
+ new CustomEvent('mappings-changed', {
218
+ detail: message.payload
219
+ })
220
+ );
221
+ break;
222
+
223
+ case 'error':
224
+ console.error('WebSocket error:', message.payload);
225
+ break;
226
+
227
+ default:
228
+ console.warn('Unknown WebSocket message type:', message.type);
229
+ }
230
+ }
231
+
232
+ private scheduleReconnect() {
233
+ if (this.reconnectTimer) {
234
+ clearTimeout(this.reconnectTimer);
235
+ }
236
+
237
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
238
+ console.error('Max reconnection attempts reached');
239
+ return;
240
+ }
241
+
242
+ this.reconnectAttempts++;
243
+ console.log(
244
+ `Scheduling reconnect attempt ${this.reconnectAttempts} in ${this.reconnectDelay}ms`
245
+ );
246
+
247
+ this.reconnectTimer = window.setTimeout(() => {
248
+ this.connect();
249
+ }, this.reconnectDelay);
250
+
251
+ // Exponential backoff
252
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); // Max 30 seconds
253
+ }
254
+
255
+ disconnect() {
256
+ if (this.reconnectTimer) {
257
+ clearTimeout(this.reconnectTimer);
258
+ this.reconnectTimer = null;
259
+ }
260
+
261
+ if (this.ws) {
262
+ this.ws.close();
263
+ this.ws = null;
264
+ }
265
+
266
+ appState.update((state) => ({ ...state, isConnected: false }));
267
+ }
268
+
269
+ // Check if WebSocket is connected without subscribing to store
270
+ isConnected(): boolean {
271
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
272
+ }
273
+
274
+ // Send a command to the backend
275
+ async sendCommand(type: string, payload?: any) {
276
+ console.log(`Sending WebSocket command: ${type}`, payload);
277
+
278
+ // Wait for connection if not ready
279
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
280
+ console.log('WebSocket not ready, waiting for connection...');
281
+ try {
282
+ await this.waitForConnection();
283
+ } catch (error) {
284
+ console.error('Failed to connect:', error);
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
290
+ const message = JSON.stringify({ type, payload });
291
+ console.log('Sending message:', message);
292
+ this.ws.send(message);
293
+ } else {
294
+ const error = new Error('WebSocket not connected after waiting');
295
+ console.error(error);
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ // Wait for WebSocket to be connected
301
+ private waitForConnection(timeout = 5000): Promise<void> {
302
+ return new Promise((resolve, reject) => {
303
+ const startTime = Date.now();
304
+
305
+ const checkConnection = () => {
306
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
307
+ resolve();
308
+ } else if (Date.now() - startTime > timeout) {
309
+ reject(new Error('WebSocket connection timeout'));
310
+ } else {
311
+ setTimeout(checkConnection, 100);
312
+ }
313
+ };
314
+
315
+ checkConnection();
316
+ });
317
+ }
318
+
319
+ // High-level command methods
320
+ async updateControl(control: Control) {
321
+ return this.sendCommand('update-control', control);
322
+ }
323
+
324
+ async createMapping(mapping: Mapping) {
325
+ return this.sendCommand('create-mapping', mapping);
326
+ }
327
+
328
+ async updateMapping(mapping: Mapping) {
329
+ return this.sendCommand('update-mapping', mapping);
330
+ }
331
+
332
+ async deleteMapping(uuid: string) {
333
+ return this.sendCommand('delete-mapping', { uuid });
334
+ }
335
+
336
+ async switchControlSet(path: string) {
337
+ // Set switching flag to prevent redirect
338
+ appState.update((state) => ({ ...state, isSwitchingControlSet: true }));
339
+ return this.sendCommand('switch-control-set', { path });
340
+ }
341
+
342
+ async getControlDetails(id: string) {
343
+ return this.sendCommand('get-control', { id });
344
+ }
345
+
346
+ async scanControlSets() {
347
+ return this.sendCommand('scan-control-sets');
348
+ }
349
+
350
+ send(message: any) {
351
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
352
+ this.ws.send(JSON.stringify(message));
353
+ } else {
354
+ console.warn('WebSocket not connected, cannot send message');
355
+ }
356
+ }
357
+ }
358
+
359
+ export const wsClient = new WebSocketClient();
@@ -0,0 +1,236 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { goto } from '$app/navigation';
6
+ import { page } from '$app/stores';
7
+ import { ControlSetInfo } from '$components/control-sets';
8
+ import { Dropdown } from '$components/ui';
9
+ import { appState, wsClient } from '$lib/websocket';
10
+ import { Code, DocumentExport, Download, LogoGithub } from 'carbon-icons-svelte';
11
+ import { onDestroy, onMount } from 'svelte';
12
+ import '../app.css';
13
+
14
+ let { children } = $props();
15
+
16
+ let hasCheckedInitialRedirect = false;
17
+
18
+ // Export controls function
19
+ async function exportControls(format: string) {
20
+ try {
21
+ // Build the export URL
22
+ const exportUrl = `/api/export-controls?format=${format}`;
23
+
24
+ // Create a temporary link and trigger download
25
+ const link = document.createElement('a');
26
+ link.href = exportUrl;
27
+ link.download = ''; // Let the server set the filename
28
+ document.body.appendChild(link);
29
+ link.click();
30
+ document.body.removeChild(link);
31
+ } catch (error) {
32
+ console.error('Export failed:', error);
33
+ }
34
+ }
35
+
36
+ onMount(() => {
37
+ // Connect WebSocket - this will get the initial state
38
+ wsClient.connect();
39
+
40
+ // Only check for redirect once on initial mount
41
+ // We need to wait for the WebSocket to connect and send initial state
42
+ let checkTimeoutId: number | null = null;
43
+
44
+ const checkForRedirect = async () => {
45
+ // Skip if we've already checked, we're on the setup page, or switching control sets
46
+ if (
47
+ hasCheckedInitialRedirect ||
48
+ $page.url.pathname === '/setup' ||
49
+ $appState.isSwitchingControlSet
50
+ ) {
51
+ return;
52
+ }
53
+
54
+ const state = $appState;
55
+
56
+ // If connected and we have the initial state
57
+ if (state.isConnected) {
58
+ hasCheckedInitialRedirect = true;
59
+
60
+ // Check if we have a valid control set
61
+ if (
62
+ !state.name ||
63
+ state.name === 'Unknown Control Set' ||
64
+ state.id === 'unknown' ||
65
+ state.id === 'default'
66
+ ) {
67
+ // Only redirect if we don't have controls (not just switching)
68
+ if (!state.controls || state.controls.length === 0) {
69
+ // Scan for available control sets
70
+ await wsClient.scanControlSets();
71
+
72
+ // Wait for the control sets list to arrive
73
+ const controlSets = await new Promise((resolve) => {
74
+ const handler = (event: CustomEvent) => {
75
+ window.removeEventListener('control-sets-list', handler as EventListener);
76
+ resolve(event.detail);
77
+ };
78
+ window.addEventListener('control-sets-list', handler as EventListener);
79
+
80
+ // Timeout after 2 seconds
81
+ setTimeout(() => {
82
+ window.removeEventListener('control-sets-list', handler as EventListener);
83
+ resolve(null);
84
+ }, 2000);
85
+ });
86
+
87
+ if (controlSets && Array.isArray(controlSets) && controlSets.length === 1) {
88
+ // Only one control set available - auto-load it
89
+ console.log('Auto-loading single control set:', controlSets[0].path);
90
+ await wsClient.switchControlSet(controlSets[0].path);
91
+ } else {
92
+ // Multiple control sets or none - show setup
93
+ goto('/setup');
94
+ }
95
+ }
96
+ }
97
+ } else {
98
+ // Not connected yet, check again in a moment
99
+ checkTimeoutId = window.setTimeout(checkForRedirect, 500);
100
+ }
101
+ };
102
+
103
+ // Start checking after a short delay to let WebSocket connect
104
+ checkTimeoutId = window.setTimeout(checkForRedirect, 100);
105
+
106
+ return () => {
107
+ if (checkTimeoutId) {
108
+ clearTimeout(checkTimeoutId);
109
+ }
110
+ };
111
+ });
112
+
113
+ onDestroy(() => {
114
+ // Disconnect WebSocket when component is destroyed
115
+ wsClient.disconnect();
116
+ });
117
+ </script>
118
+
119
+ <div class="h-screen flex flex-col">
120
+ {#if $page.url.pathname !== '/setup'}
121
+ <!-- Fixed Header -->
122
+ <header
123
+ class="bg-white dark:bg-gray-900 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-shrink-0"
124
+ >
125
+ <div class="w-full px-6 lg:px-8">
126
+ <div class="flex justify-between items-center h-16">
127
+ <!-- Left: Logo and Brand -->
128
+ <div class="flex items-center">
129
+ <a href="/" class="flex items-center space-x-3 hover:opacity-80 transition-opacity">
130
+ <img src="/lula.png" class="h-8 w-8" alt="Lula Logo" />
131
+ <div class="flex flex-col">
132
+ <span class="text-xl font-bold text-gray-900 dark:text-white"> Lula </span>
133
+ <span class="text-xs text-gray-500 dark:text-gray-400 -mt-1">
134
+ Gitops for Compliance
135
+ </span>
136
+ </div>
137
+ </a>
138
+ </div>
139
+
140
+ <!-- Right: Control Set Info and Actions -->
141
+ <div class="flex items-center space-x-4">
142
+ <!-- Control Set Info Badge -->
143
+ <ControlSetInfo />
144
+
145
+ {#if $appState.isConnected && $appState.controls && $appState.controls.length > 0}
146
+ <!-- Export Dropdown -->
147
+ <Dropdown
148
+ buttonLabel="Export"
149
+ buttonIcon={Download}
150
+ buttonClass="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
151
+ dropdownClass="w-48"
152
+ >
153
+ {#snippet children()}
154
+ <div class="space-y-1 p-1">
155
+ <button
156
+ onclick={() => exportControls('csv')}
157
+ class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
158
+ >
159
+ <div class="flex items-center gap-2">
160
+ <DocumentExport class="w-4 h-4" />
161
+ <span>Export as CSV</span>
162
+ </div>
163
+ </button>
164
+ <button
165
+ onclick={() => exportControls('excel')}
166
+ class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
167
+ >
168
+ <div class="flex items-center gap-2">
169
+ <Download class="w-4 h-4" />
170
+ <span>Export as Excel</span>
171
+ </div>
172
+ </button>
173
+ <button
174
+ onclick={() => exportControls('json')}
175
+ class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
176
+ >
177
+ <div class="flex items-center gap-2">
178
+ <Code class="w-4 h-4" />
179
+ <span>Export as JSON</span>
180
+ </div>
181
+ </button>
182
+ </div>
183
+ {/snippet}
184
+ </Dropdown>
185
+ {/if}
186
+
187
+ <!-- Github -->
188
+ <a
189
+ class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
190
+ title="Github"
191
+ href="https://github.com/defenseunicorns/lula"
192
+ target="_blank"
193
+ >
194
+ <LogoGithub class="w-5 h-5" />
195
+ </a>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </header>
200
+
201
+ <!-- Split Pane Layout with Cards -->
202
+ <div class="flex-1 flex gap-6 p-6 overflow-hidden">
203
+ {#if $appState.isSwitchingControlSet}
204
+ <!-- Show loading during control set switch -->
205
+ <div class="flex-1 flex justify-center items-center">
206
+ <div class="text-center">
207
+ <div
208
+ class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"
209
+ ></div>
210
+ <p class="text-gray-500 dark:text-gray-400">Switching control set...</p>
211
+ </div>
212
+ </div>
213
+ {:else if !$appState.isConnected || !$appState.controls || $appState.controls.length === 0 || !$appState.fieldSchema}
214
+ <div class="flex-1 flex justify-center items-center">
215
+ <div class="text-center">
216
+ <div
217
+ class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"
218
+ ></div>
219
+ <p class="text-gray-500 dark:text-gray-400">
220
+ {!$appState.isConnected
221
+ ? 'Connecting...'
222
+ : !$appState.fieldSchema
223
+ ? 'Loading schema...'
224
+ : 'Loading controls...'}
225
+ </p>
226
+ </div>
227
+ </div>
228
+ {:else}
229
+ {@render children()}
230
+ {/if}
231
+ </div>
232
+ {:else}
233
+ <!-- Setup page gets full screen -->
234
+ {@render children()}
235
+ {/if}
236
+ </div>
@@ -0,0 +1,38 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { ControlDetailsPanel, ControlsList } from '$components/controls';
6
+ import { selectedControl } from '$stores/compliance';
7
+ import { Document } from 'carbon-icons-svelte';
8
+ </script>
9
+
10
+ <!-- Left Pane: Controls List Card -->
11
+ <div class="w-1/2 flex flex-col">
12
+ <div
13
+ class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm h-full flex flex-col"
14
+ >
15
+ <ControlsList />
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Right Pane: Control Details -->
20
+ <div class="w-1/2 flex flex-col">
21
+ {#if $selectedControl}
22
+ <ControlDetailsPanel control={$selectedControl} />
23
+ {:else}
24
+ <div class=" h-full flex flex-col">
25
+ <div class="flex-1 flex items-center justify-center p-8">
26
+ <div class="text-center text-gray-500 dark:text-gray-400">
27
+ <Document class="mx-auto h-16 w-16 mb-4" />
28
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
29
+ No Control Selected
30
+ </h3>
31
+ <p class="text-gray-600 dark:text-gray-400">
32
+ Select a control from the list to view and edit its details
33
+ </p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ {/if}
38
+ </div>