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,112 @@
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 { ControlDetailsPanel, ControlsList } from '$components/controls';
8
+ import { wsClient } from '$lib/websocket';
9
+ import { selectedControl } from '$stores/compliance';
10
+ import { Document } from 'carbon-icons-svelte';
11
+ import { onMount } from 'svelte';
12
+
13
+ // Track the last fetched control ID to avoid refetching
14
+ let lastFetchedControlId = '';
15
+ let isLoadingControl = $state(false);
16
+
17
+ // React to URL parameter changes and fetch control details
18
+ $effect(() => {
19
+ const controlId = $page.params.id;
20
+ if (!controlId) return;
21
+
22
+ const decodedControlId = decodeURIComponent(controlId);
23
+
24
+ // Check if we need to fetch - only when control ID changes
25
+ if (decodedControlId && decodedControlId !== lastFetchedControlId) {
26
+ // Check connection status without subscribing to full appState
27
+ const isConnected = wsClient.isConnected();
28
+
29
+ if (isConnected) {
30
+ lastFetchedControlId = decodedControlId;
31
+ isLoadingControl = true;
32
+
33
+ // Fetch full control details from backend
34
+ wsClient.getControlDetails(decodedControlId);
35
+ }
36
+ }
37
+ });
38
+
39
+ // Listen for control details from WebSocket
40
+ onMount(() => {
41
+ const handleControlDetails = (event: CustomEvent) => {
42
+ const control = event.detail;
43
+ isLoadingControl = false;
44
+ if (control) {
45
+ selectedControl.set(control);
46
+ } else {
47
+ // Control not found, redirect to home
48
+ goto('/');
49
+ }
50
+ };
51
+
52
+ window.addEventListener('control-details', handleControlDetails as EventListener);
53
+
54
+ return () => {
55
+ window.removeEventListener('control-details', handleControlDetails as EventListener);
56
+ };
57
+ });
58
+ </script>
59
+
60
+ <!-- Left Pane: Controls List Card -->
61
+ <div class="w-1/2 flex flex-col">
62
+ <div
63
+ class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm h-full flex flex-col"
64
+ >
65
+ <ControlsList />
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Right Pane: Control Details -->
70
+ <div class="w-1/2 flex flex-col relative">
71
+ {#if isLoadingControl}
72
+ <!-- Loading spinner in top-right corner -->
73
+ <div class="absolute top-4 right-4 z-10">
74
+ <div
75
+ class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30"
76
+ title="Loading control..."
77
+ >
78
+ <svg
79
+ class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin"
80
+ fill="none"
81
+ viewBox="0 0 24 24"
82
+ >
83
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
84
+ ></circle>
85
+ <path
86
+ class="opacity-75"
87
+ fill="currentColor"
88
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
89
+ ></path>
90
+ </svg>
91
+ </div>
92
+ </div>
93
+ {/if}
94
+
95
+ {#if $selectedControl}
96
+ <ControlDetailsPanel control={$selectedControl} />
97
+ {:else}
98
+ <div class=" h-full flex flex-col">
99
+ <div class="flex-1 flex items-center justify-center p-8">
100
+ <div class="text-center text-gray-500 dark:text-gray-400">
101
+ <Document class="mx-auto h-16 w-16 mb-4" />
102
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
103
+ No Control Selected
104
+ </h3>
105
+ <p class="text-gray-600 dark:text-gray-400">
106
+ Select a control from the list to view and edit its details
107
+ </p>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ {/if}
112
+ </div>
@@ -0,0 +1,241 @@
1
+ <!-- Setup Wizard Page for Control Set Creation -->
2
+ <script lang="ts">
3
+ import { goto } from '$app/navigation';
4
+ import { ExistingControlSets, SpreadsheetImport } from '$components/setup';
5
+ import { appState, wsClient } from '$lib/websocket';
6
+ import { onMount } from 'svelte';
7
+ import { get } from 'svelte/store';
8
+
9
+ let activeTab: 'import' | 'existing' = 'import'; // Default to import
10
+ let currentControlSetPath = '';
11
+ let hasAnyControlSets = false;
12
+ let isSwitching = false;
13
+ let controlSets: {
14
+ path: string;
15
+ name: string;
16
+ description: string;
17
+ controlCount: number;
18
+ file: string;
19
+ }[] = [];
20
+
21
+ onMount(() => {
22
+ // Connect WebSocket if not already connected
23
+ wsClient.connect();
24
+
25
+ // Use appState to check current control set
26
+ const unsubscribe = appState.subscribe((state) => {
27
+ if (
28
+ state.name &&
29
+ state.name !== 'Unknown Control Set' &&
30
+ state.id !== 'unknown' &&
31
+ state.id !== 'default'
32
+ ) {
33
+ // Store the current path if a control set is loaded
34
+ currentControlSetPath = state.currentPath || '';
35
+ }
36
+ });
37
+
38
+ // Listen for control sets list
39
+ const handleControlSetsList = async (event: CustomEvent) => {
40
+ const data = event.detail;
41
+ if (data && data.controlSets) {
42
+ controlSets = data.controlSets || [];
43
+ hasAnyControlSets = controlSets.length > 0;
44
+
45
+ // Check if we should auto-load
46
+ if (controlSets.length === 1) {
47
+ const singleControlSet = controlSets[0];
48
+ const isAlreadyLoaded =
49
+ currentControlSetPath && currentControlSetPath.includes(singleControlSet.path);
50
+
51
+ // Only auto-load if it's not already the current control set
52
+ if (!isAlreadyLoaded && !currentControlSetPath) {
53
+ console.log(
54
+ 'Only one control set found and none loaded, auto-loading:',
55
+ singleControlSet.path
56
+ );
57
+ await switchControlSet(singleControlSet.path);
58
+ return;
59
+ }
60
+ }
61
+
62
+ // Only switch to existing tab if there are control sets
63
+ if (hasAnyControlSets) {
64
+ activeTab = 'existing';
65
+ }
66
+ }
67
+ };
68
+
69
+ window.addEventListener('control-sets-list', handleControlSetsList as unknown as EventListener);
70
+
71
+ // Request control sets scan via WebSocket (don't await in onMount)
72
+ wsClient.scanControlSets().catch((err) => {
73
+ console.error('Error scanning control sets:', err);
74
+ });
75
+
76
+ return () => {
77
+ unsubscribe();
78
+ window.removeEventListener(
79
+ 'control-sets-list',
80
+ handleControlSetsList as unknown as EventListener
81
+ );
82
+ };
83
+ });
84
+
85
+ async function switchControlSet(path: string) {
86
+ console.log('Starting control set switch to:', path);
87
+ isSwitching = true;
88
+
89
+ try {
90
+ // Send the switch command to WebSocket
91
+ await wsClient.switchControlSet(path);
92
+ console.log('WebSocket command sent, waiting for state update...');
93
+
94
+ // Wait for the actual state update to complete
95
+ await new Promise<void>((resolve) => {
96
+ let checkCount = 0;
97
+ const maxChecks = 50; // 5 seconds max (100ms intervals)
98
+
99
+ const checkInterval = setInterval(() => {
100
+ checkCount++;
101
+ const state = get(appState);
102
+
103
+ // Check if the switch is complete
104
+ if (
105
+ !state.isSwitchingControlSet &&
106
+ state.currentPath &&
107
+ state.currentPath.includes(path)
108
+ ) {
109
+ clearInterval(checkInterval);
110
+ console.log('Control set switch completed successfully');
111
+ resolve();
112
+ } else if (checkCount >= maxChecks) {
113
+ clearInterval(checkInterval);
114
+ console.error('Control set switch timed out');
115
+ alert('Control set switch timed out. Please try again.');
116
+ resolve();
117
+ }
118
+ }, 100);
119
+ });
120
+
121
+ // Navigate to home page after successful switch
122
+ isSwitching = false;
123
+ goto('/');
124
+ } catch (error) {
125
+ console.error('Error switching control set:', error);
126
+ alert('Failed to switch control set: ' + (error as Error).message);
127
+ isSwitching = false;
128
+ }
129
+ }
130
+
131
+ async function handleControlSetCreated(event: CustomEvent) {
132
+ const { path } = event.detail;
133
+ await switchControlSet(path);
134
+ }
135
+
136
+ async function handleControlSetSelected(event: CustomEvent) {
137
+ const { path } = event.detail;
138
+ await switchControlSet(path);
139
+ }
140
+
141
+ function handleTabChange(event: CustomEvent) {
142
+ const { tab } = event.detail;
143
+ if (tab) {
144
+ activeTab = tab;
145
+ }
146
+ }
147
+ </script>
148
+
149
+ <div class=" p-4">
150
+ <div class="max-w-6xl mx-auto">
151
+ <!-- Header -->
152
+ <div class="text-center py-8">
153
+ <h1
154
+ class="text-4xl font-extrabold text-gray-900 dark:text-white mb-4 flex items-center justify-center gap-3"
155
+ >
156
+ <img src="/lula.png" class="h-12 w-12" alt="Lula" />
157
+ <span>Lula</span>
158
+ </h1>
159
+ <p class="text-lg text-gray-600 dark:text-gray-400">
160
+ {#if currentControlSetPath}
161
+ You have an existing control set. You can continue using it or create a new one.
162
+ {:else if hasAnyControlSets}
163
+ Select an existing control set or import a new one from a spreadsheet.
164
+ {:else}
165
+ Let's get started by importing a control set from a spreadsheet.
166
+ {/if}
167
+ </p>
168
+ </div>
169
+
170
+ <!-- Tab Navigation (only show if there are existing control sets) -->
171
+ {#if hasAnyControlSets}
172
+ <div class="flex justify-center mb-8">
173
+ <div
174
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 inline-flex"
175
+ >
176
+ <button
177
+ on:click={() => (activeTab = 'existing')}
178
+ class="px-6 py-3 rounded-l-lg font-medium transition-colors {activeTab === 'existing'
179
+ ? 'bg-blue-600 text-white'
180
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}"
181
+ >
182
+ Select Existing Control Set
183
+ </button>
184
+ <button
185
+ on:click={() => (activeTab = 'import')}
186
+ class="px-6 py-3 rounded-r-lg font-medium transition-colors {activeTab === 'import'
187
+ ? 'bg-blue-600 text-white'
188
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}"
189
+ >
190
+ Import New from Spreadsheet
191
+ </button>
192
+ </div>
193
+ </div>
194
+ {/if}
195
+
196
+ <!-- Content Area -->
197
+ <div
198
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-lg shadow-xl p-6 relative border border-gray-200 dark:border-gray-700"
199
+ >
200
+ {#if activeTab === 'import'}
201
+ <SpreadsheetImport on:created={handleControlSetCreated} />
202
+ {:else}
203
+ <ExistingControlSets
204
+ {controlSets}
205
+ on:selected={handleControlSetSelected}
206
+ on:tab-change={handleTabChange}
207
+ />
208
+ {/if}
209
+
210
+ {#if isSwitching}
211
+ <div
212
+ class="absolute inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center rounded-lg"
213
+ >
214
+ <div class="bg-white dark:bg-gray-700 rounded-lg p-6 text-center">
215
+ <svg
216
+ class="animate-spin h-8 w-8 text-blue-600 mx-auto mb-4"
217
+ xmlns="http://www.w3.org/2000/svg"
218
+ fill="none"
219
+ viewBox="0 0 24 24"
220
+ >
221
+ <circle
222
+ class="opacity-25"
223
+ cx="12"
224
+ cy="12"
225
+ r="10"
226
+ stroke="currentColor"
227
+ stroke-width="4"
228
+ ></circle>
229
+ <path
230
+ class="opacity-75"
231
+ fill="currentColor"
232
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
233
+ ></path>
234
+ </svg>
235
+ <p class="text-gray-900 dark:text-white">Switching control set...</p>
236
+ </div>
237
+ </div>
238
+ {/if}
239
+ </div>
240
+ </div>
241
+ </div>
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ import type { Control, ControlWithMappings, Mapping } from '$lib/types';
5
+ import { derived, writable } from 'svelte/store';
6
+
7
+ // Base stores
8
+ export const controls = writable<Control[]>([]);
9
+ export const mappings = writable<Mapping[]>([]);
10
+ export const loading = writable(true);
11
+ export const saveStatus = writable<'saved' | 'saving' | 'error'>('saved');
12
+ export const searchTerm = writable('');
13
+ export const selectedFamily = writable<string | null>(null);
14
+ export const selectedControl = writable<Control | null>(null);
15
+
16
+ // Derived stores
17
+ export const families = derived(controls, ($controls) => {
18
+ const familySet = new Set(
19
+ $controls.map((c) => {
20
+ // Enhanced controls have family in _metadata.family, fallback to extracting from control-acronym
21
+ return (
22
+ (c as any)?._metadata?.family ||
23
+ (c as any)?.family ||
24
+ (c as any)?.['control-acronym']?.split('-')[0] ||
25
+ ''
26
+ );
27
+ })
28
+ );
29
+ return Array.from(familySet)
30
+ .filter((f) => f)
31
+ .sort();
32
+ });
33
+
34
+ export const filteredControls = derived(
35
+ [controls, selectedFamily, searchTerm],
36
+ ([$controls, $selectedFamily, $searchTerm]) => {
37
+ let results = $controls;
38
+
39
+ if ($selectedFamily) {
40
+ results = results.filter((c) => {
41
+ // Enhanced controls have family in _metadata.family, fallback to extracting from control-acronym
42
+ const family =
43
+ (c as any)?._metadata?.family ||
44
+ (c as any)?.family ||
45
+ (c as any)?.['control-acronym']?.split('-')[0] ||
46
+ '';
47
+ return family === $selectedFamily;
48
+ });
49
+ }
50
+
51
+ if ($searchTerm) {
52
+ const term = $searchTerm.toLowerCase();
53
+ results = results.filter((c) => JSON.stringify(c).toLowerCase().includes(term));
54
+ }
55
+
56
+ return results;
57
+ }
58
+ );
59
+
60
+ export const controlsWithMappings = derived(
61
+ [controls, mappings],
62
+ ([$controls, $mappings]): ControlWithMappings[] => {
63
+ return $controls.map((control) => ({
64
+ ...control,
65
+ mappings: $mappings.filter((m) => m.control_id === control.id)
66
+ }));
67
+ }
68
+ );
69
+
70
+ // Store actions - mostly for local state management
71
+ // All server operations now go through WebSocket
72
+ export const complianceStore = {
73
+ setSearchTerm(term: string) {
74
+ searchTerm.set(term);
75
+ },
76
+
77
+ setSelectedFamily(family: string | null) {
78
+ selectedFamily.set(family);
79
+ },
80
+
81
+ setSelectedControl(control: Control | null) {
82
+ // Strip mappings property if it exists to prevent it from being saved to control files
83
+ if (control && 'mappings' in control) {
84
+ const { mappings: _mappings, ...controlWithoutMappings } = control as any;
85
+ selectedControl.set(controlWithoutMappings);
86
+ } else {
87
+ selectedControl.set(control);
88
+ }
89
+ },
90
+
91
+ clearFilters() {
92
+ searchTerm.set('');
93
+ selectedFamily.set(null);
94
+ }
95
+ };
@@ -0,0 +1,20 @@
1
+ /*
2
+ Copyright 2025 Defense Unicorns
3
+ SPDX-License-Identifier: LicenseRef-Defense-Unicorns-Commercial
4
+ */
5
+ @reference "tailwindcss";
6
+
7
+ @layer components {
8
+ .hljs-attr {
9
+ color: #33bbc8 !important;
10
+ }
11
+
12
+ .hljs-code,
13
+ .hljs-addition,
14
+ .hljs-title.class_.inherited__,
15
+ .hljs-number,
16
+ .hljs-literal,
17
+ .hljs-string {
18
+ color: #d338d3 !important;
19
+ }
20
+ }
@@ -0,0 +1,58 @@
1
+ /*
2
+ Copyright 2025 Defense Unicorns
3
+ SPDX-License-Identifier: LicenseRef-Defense-Unicorns-Commercial
4
+ */
5
+
6
+ @reference "tailwindcss";
7
+
8
+ @layer components {
9
+ /* Backdrop styling */
10
+ .modal-backdrop {
11
+ @apply fixed inset-0 z-100 hidden bg-black/70;
12
+ }
13
+
14
+ /* Modal content */
15
+ .modal-content {
16
+ @apply absolute z-[1000] hidden max-h-[calc(100vh-2rem)] w-[calc(100vw-2rem)] max-w-2xl overflow-auto rounded-lg border border-gray-700 bg-gray-900 p-6 shadow-xl md:w-full;
17
+ }
18
+
19
+ /* When modal is open, content should be visible */
20
+ .modal-content[aria-hidden='false'] {
21
+ @apply fixed !block;
22
+ top: 50% !important;
23
+ left: 50% !important;
24
+ transform: translate(-50%, -50%) !important;
25
+ }
26
+
27
+ .modal-title {
28
+ @apply pe-4 text-lg font-semibold text-white;
29
+ }
30
+
31
+ /* Close button */
32
+ .modal-close {
33
+ @apply absolute top-3 right-4 flex h-8 w-8 cursor-pointer items-center justify-center border-0 bg-transparent text-2xl leading-none text-gray-400 transition-colors hover:text-gray-300;
34
+ }
35
+
36
+ .modal-close:hover {
37
+ @apply bg-gray-600/20 text-gray-100;
38
+ }
39
+
40
+ /* Animation classes */
41
+ .modal-fade-in {
42
+ animation: fadeIn 0.15s ease-in-out forwards;
43
+ }
44
+
45
+ @keyframes fadeIn {
46
+ from {
47
+ opacity: 0;
48
+ }
49
+ to {
50
+ opacity: 1;
51
+ }
52
+ }
53
+
54
+ /* Button styling for modals */
55
+ .btn {
56
+ @apply flex items-center rounded border border-gray-700 bg-gray-800/50 px-3 py-1 text-sm text-gray-400 transition-colors hover:border-blue-500 hover:text-blue-400;
57
+ }
58
+ }
@@ -0,0 +1,111 @@
1
+ /*
2
+ Copyright 2025 Defense Unicorns
3
+ SPDX-License-Identifier: LicenseRef-Defense-Unicorns-Commercial
4
+ */
5
+
6
+ @reference "tailwindcss";
7
+
8
+ @layer components {
9
+ .table-page {
10
+ @apply flex max-h-full flex-col gap-6 sm:gap-8;
11
+ }
12
+
13
+ .controls-container {
14
+ @apply flex flex-col items-center justify-between gap-4 p-1 md:flex-row;
15
+ }
16
+
17
+ .table-search {
18
+ @apply relative w-full md:w-1/2;
19
+ }
20
+
21
+ .table-search-icon {
22
+ @apply pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3;
23
+ svg {
24
+ @apply h-4 w-4 text-gray-400;
25
+ }
26
+ }
27
+
28
+ .table-search-input {
29
+ @apply w-full rounded-md border border-gray-700 bg-gray-800 py-2 pr-10 pl-10 [&::-webkit-search-cancel-button]:hidden;
30
+ }
31
+
32
+ .table-search-clear-btn {
33
+ @apply absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-white;
34
+ }
35
+
36
+ .controls-info-container {
37
+ @apply flex justify-center md:justify-start;
38
+ }
39
+ .controls-info-button {
40
+ @apply flex cursor-pointer items-center gap-1 rounded-md bg-blue-900/40 px-3 py-2 text-sm text-nowrap text-blue-200 hover:bg-blue-800/60;
41
+ }
42
+ .controls-info-button-icon {
43
+ @apply h-4 w-4;
44
+ }
45
+
46
+ .controls-info-modal-container {
47
+ @apply max-w-2xl rounded-lg border border-gray-700 bg-gray-900 p-0 shadow-xl;
48
+ }
49
+
50
+ .controls-info-modal-title {
51
+ @apply flex items-center justify-between border-b border-gray-800 px-6 py-4;
52
+ h3 {
53
+ @apply text-lg font-semibold text-white;
54
+ }
55
+ }
56
+
57
+ .controls-info-modal-body {
58
+ @apply p-6;
59
+ }
60
+
61
+ .error-container {
62
+ @apply flex max-h-full flex-col gap-4 sm:gap-6;
63
+ }
64
+
65
+ .error-content {
66
+ @apply mb-6 rounded-lg bg-red-900/30 p-4 text-red-200;
67
+ }
68
+
69
+ .error-retry-btn {
70
+ @apply mt-2 rounded-md bg-red-800 px-3 py-1 text-sm hover:bg-red-700;
71
+ }
72
+
73
+ .table-container {
74
+ @apply relative flex min-h-[var(--table-height)] overflow-y-hidden portrait:sm:min-h-auto;
75
+ }
76
+
77
+ .table-head {
78
+ @apply bg-gray-900;
79
+ }
80
+
81
+ .header-cell {
82
+ @apply px-4 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase;
83
+ }
84
+
85
+ .table-row {
86
+ @apply cursor-auto border-b border-gray-800;
87
+ }
88
+
89
+ .first-data-column {
90
+ @apply pl-4!;
91
+ }
92
+
93
+ .table-row--expandable {
94
+ @apply cursor-pointer hover:bg-gray-800/30!;
95
+ }
96
+ .data-cell {
97
+ @apply px-2 py-4 text-sm whitespace-nowrap;
98
+ }
99
+
100
+ .expanded-row-container {
101
+ @apply flex w-full flex-col justify-between gap-6 p-4 sm:flex-row;
102
+ }
103
+
104
+ .multi-item-cell {
105
+ @apply flex items-center gap-2.5;
106
+ }
107
+
108
+ .initial-avatar {
109
+ @apply hidden h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-700 text-indigo-300 sm:flex;
110
+ }
111
+ }
@@ -0,0 +1,65 @@
1
+ /*
2
+ Copyright 2025 Defense Unicorns
3
+ SPDX-License-Identifier: LicenseRef-Defense-Unicorns-Commercial
4
+ */
5
+ @reference "tailwindcss";
6
+
7
+ @layer components {
8
+ .tooltip {
9
+ @apply pointer-events-none fixed z-50 rounded bg-gray-950 p-2 text-xs text-gray-100 opacity-0 shadow-sm shadow-gray-600/40 transition-opacity duration-200;
10
+ z-index: 1000;
11
+ top: var(--group-top, 0);
12
+ max-width: min(320px, calc(100vw - 40px));
13
+ white-space: normal;
14
+ }
15
+
16
+ [data-tooltip-trigger]:hover .tooltip,
17
+ [data-tooltip-trigger]:focus-within .tooltip {
18
+ @apply opacity-100;
19
+ }
20
+
21
+ .tooltip::before {
22
+ content: '';
23
+ @apply absolute top-[16px] -translate-y-0 transform border-8 border-solid;
24
+ }
25
+
26
+ .tooltip-left {
27
+ @apply mr-2;
28
+ right: calc(100% - var(--group-left, 0));
29
+ }
30
+
31
+ .tooltip-left::before {
32
+ @apply -right-4 border-t-transparent border-r-transparent border-b-transparent border-l-gray-950;
33
+ }
34
+
35
+ .tooltip-right {
36
+ @apply ml-2;
37
+ left: var(--group-right, 0);
38
+ }
39
+
40
+ .tooltip-right::before {
41
+ @apply -left-4 border-t-transparent border-r-gray-950 border-b-transparent border-l-transparent;
42
+ }
43
+
44
+ .tooltip-top {
45
+ @apply mb-2;
46
+ bottom: calc(100% - var(--group-top, 0));
47
+ left: 50%;
48
+ transform: translateX(-50%);
49
+ }
50
+
51
+ .tooltip-top::before {
52
+ @apply -bottom-4 left-1/2 -translate-x-1/2 border-t-gray-950 border-r-transparent border-b-transparent border-l-transparent;
53
+ }
54
+
55
+ .tooltip-bottom {
56
+ @apply mt-2;
57
+ top: var(--group-bottom, 0);
58
+ left: 50%;
59
+ transform: translateX(-50%);
60
+ }
61
+
62
+ .tooltip-bottom::before {
63
+ @apply -top-4 left-1/2 -translate-x-1/2 border-t-transparent border-r-transparent border-b-gray-950 border-l-transparent;
64
+ }
65
+ }