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,968 @@
1
+ <script lang="ts">
2
+ import { CloudUpload, Draggable } from 'carbon-icons-svelte';
3
+ import { createEventDispatcher } from 'svelte';
4
+
5
+ const dispatch = createEventDispatcher();
6
+
7
+ // File data
8
+ let fileData: File | null = null;
9
+ let fileName = '';
10
+ let selectedSheet = '';
11
+ let sheets: string[] = [];
12
+ let fields: string[] = [];
13
+ let sampleData: any[] = [];
14
+ let controlCount = 0;
15
+ let rowPreviews: { row: number; preview: string }[] = [];
16
+ // Make availableFields reactive to both fields and fieldConfigs changes
17
+ $: availableFields = fields.filter((f) => fields.includes(f));
18
+
19
+ // Field configuration for tabs
20
+ type TabAssignment = 'overview' | 'implementation' | 'custom' | null;
21
+ let fieldConfigs = new Map<
22
+ string,
23
+ {
24
+ originalName: string;
25
+ tab: TabAssignment;
26
+ displayOrder: number;
27
+ fieldType: 'text' | 'textarea' | 'select' | 'date' | 'number' | 'boolean';
28
+ required: boolean;
29
+ }
30
+ >();
31
+
32
+ // Options
33
+ let headerRow = 1;
34
+ let controlIdField = ''; // Start empty to force selection
35
+ let controlSetName = '';
36
+ let controlSetDescription = '';
37
+
38
+ // UI State
39
+ let isLoading = false;
40
+ let errorMessage = '';
41
+ let successMessage = '';
42
+ let showFieldMapping = false;
43
+ let dragActive = false;
44
+
45
+ // Drag and drop state
46
+ let draggedField: string | null = null;
47
+ let dragOverTab: TabAssignment | null = null;
48
+ let dragOverField: string | null = null;
49
+
50
+ // Reset all form state
51
+ function resetFormState() {
52
+ // Reset field data
53
+ fields = [];
54
+ sampleData = [];
55
+ controlCount = 0;
56
+ fieldConfigs.clear();
57
+ fieldConfigs = new Map(); // Force reactivity
58
+
59
+ // Reset selections
60
+ controlIdField = '';
61
+
62
+ // Reset UI state
63
+ errorMessage = '';
64
+ successMessage = '';
65
+ draggedField = null;
66
+ dragOverTab = null;
67
+ dragOverField = null;
68
+ }
69
+
70
+ function handleDragOver(e: DragEvent) {
71
+ e.preventDefault();
72
+ dragActive = true;
73
+ }
74
+
75
+ function handleDragLeave() {
76
+ dragActive = false;
77
+ }
78
+
79
+ function handleDrop(e: DragEvent) {
80
+ e.preventDefault();
81
+ dragActive = false;
82
+
83
+ const files = e.dataTransfer?.files;
84
+ if (files && files.length > 0) {
85
+ handleFile(files[0]);
86
+ }
87
+ }
88
+
89
+ function handleFileSelect(e: Event) {
90
+ const target = e.target as HTMLInputElement;
91
+ if (target.files && target.files.length > 0) {
92
+ handleFile(target.files[0]);
93
+ }
94
+ }
95
+
96
+ async function handleFile(file: File) {
97
+ // Reset all form state when loading a new file
98
+ resetFormState();
99
+
100
+ fileName = file.name;
101
+ fileData = file;
102
+ errorMessage = '';
103
+ isLoading = true;
104
+
105
+ // Auto-populate control set name from filename (remove extension)
106
+ controlSetName = fileName.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
107
+ controlSetDescription = `Imported from ${fileName}`;
108
+
109
+ try {
110
+ // Send file to backend for parsing
111
+ const formData = new FormData();
112
+ formData.append('file', file);
113
+
114
+ const response = await fetch('/api/parse-excel', {
115
+ method: 'POST',
116
+ body: formData
117
+ });
118
+
119
+ if (!response.ok) {
120
+ const error = await response.json();
121
+ throw new Error(error.error || 'Failed to parse file');
122
+ }
123
+
124
+ const result = await response.json();
125
+
126
+ sheets = result.sheets || [];
127
+ selectedSheet = result.selectedSheet || sheets[0];
128
+ rowPreviews = result.rowPreviews || [];
129
+
130
+ // Set default header row
131
+ if (rowPreviews.length > 0 && headerRow === 1) {
132
+ headerRow = rowPreviews[0].row;
133
+ }
134
+
135
+ // Load fields from the selected sheet
136
+ await loadSheetData();
137
+ showFieldMapping = true;
138
+ } catch (error) {
139
+ errorMessage = 'Error reading file: ' + (error as Error).message;
140
+ } finally {
141
+ isLoading = false;
142
+ }
143
+ }
144
+
145
+ async function loadSheetData() {
146
+ if (!fileData || !selectedSheet) return;
147
+
148
+ isLoading = true;
149
+
150
+ // Clear previous field configurations when changing sheets
151
+ fieldConfigs.clear();
152
+ fieldConfigs = new Map(); // Force reactivity
153
+ controlIdField = ''; // Reset control ID field selection
154
+
155
+ try {
156
+ const formData = new FormData();
157
+ formData.append('file', fileData);
158
+ formData.append('sheetName', selectedSheet);
159
+ formData.append('headerRow', headerRow.toString());
160
+
161
+ const response = await fetch('/api/parse-excel-sheet', {
162
+ method: 'POST',
163
+ body: formData
164
+ });
165
+
166
+ if (!response.ok) {
167
+ const error = await response.json();
168
+ throw new Error(error.error || 'Failed to parse sheet');
169
+ }
170
+
171
+ const result = await response.json();
172
+
173
+ fields = result.fields || [];
174
+ sampleData = result.sampleData || [];
175
+ controlCount = result.controlCount || 0;
176
+
177
+ // Initialize field configurations with smart defaults
178
+ fields.forEach((field, index) => {
179
+ const lowerField = field.toLowerCase();
180
+ let tab: TabAssignment = 'custom';
181
+ let fieldType: 'text' | 'textarea' | 'select' | 'date' | 'number' | 'boolean' = 'text';
182
+
183
+ // Smart tab assignment based on field name
184
+ if (
185
+ lowerField.includes('implementation') ||
186
+ lowerField.includes('status') ||
187
+ lowerField.includes('narrative') ||
188
+ lowerField.includes('guidance')
189
+ ) {
190
+ tab = 'implementation';
191
+ } else if (
192
+ lowerField.includes('id') ||
193
+ lowerField.includes('title') ||
194
+ lowerField.includes('family') ||
195
+ lowerField.includes('cci') ||
196
+ lowerField.includes('control') ||
197
+ lowerField.includes('acronym')
198
+ ) {
199
+ tab = 'overview';
200
+ }
201
+
202
+ // Smart field type detection
203
+ if (
204
+ lowerField.includes('description') ||
205
+ lowerField.includes('narrative') ||
206
+ lowerField.includes('guidance') ||
207
+ lowerField.includes('statement')
208
+ ) {
209
+ fieldType = 'textarea';
210
+ } else if (
211
+ lowerField.includes('status') ||
212
+ lowerField.includes('type') ||
213
+ lowerField.includes('designation')
214
+ ) {
215
+ fieldType = 'select';
216
+ } else if (lowerField.includes('date')) {
217
+ fieldType = 'date';
218
+ } else if (lowerField.includes('count') || lowerField.includes('number')) {
219
+ fieldType = 'number';
220
+ }
221
+
222
+ fieldConfigs.set(field, {
223
+ originalName: field,
224
+ tab,
225
+ displayOrder: index,
226
+ fieldType,
227
+ required: lowerField.includes('id') || lowerField.includes('title')
228
+ });
229
+ });
230
+
231
+ // Trigger reactivity for fieldConfigs
232
+ fieldConfigs = fieldConfigs;
233
+
234
+ // Reset controlIdField if it doesn't exist in the new sheet
235
+ if (controlIdField && !fields.includes(controlIdField)) {
236
+ controlIdField = '';
237
+ }
238
+
239
+ // Default controlIdField to "AP Acronym" if it exists, has short values, and is unique
240
+ if (!controlIdField && fields.includes('AP Acronym')) {
241
+ const hasShortValues =
242
+ !sampleData.length ||
243
+ sampleData.every((row) => !row['AP Acronym'] || String(row['AP Acronym']).length < 25);
244
+ const nonEmptyValues = sampleData
245
+ .map((row) => row['AP Acronym'])
246
+ .filter((v) => v != null && v !== '' && String(v).trim() !== '');
247
+ const uniqueValues = new Set(nonEmptyValues);
248
+ const hasUniqueValues =
249
+ !nonEmptyValues.length || uniqueValues.size === nonEmptyValues.length;
250
+ const hasNonEmptyValues = nonEmptyValues.length > 0;
251
+ if (hasShortValues && hasUniqueValues && hasNonEmptyValues) {
252
+ controlIdField = 'AP Acronym';
253
+ }
254
+ }
255
+ } catch (error) {
256
+ errorMessage = 'Error loading sheet data: ' + (error as Error).message;
257
+ } finally {
258
+ isLoading = false;
259
+ }
260
+ }
261
+
262
+ function cleanFieldName(fieldName: string): string {
263
+ if (!fieldName) return fieldName;
264
+
265
+ // Always clean and apply kebab-case
266
+ let cleaned = fieldName.trim().replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim();
267
+
268
+ return toKebabCase(cleaned);
269
+ }
270
+
271
+ function toKebabCase(str: string): string {
272
+ return str
273
+ .replace(/\W+/g, ' ')
274
+ .split(/ |\s/)
275
+ .map((word) => word.toLowerCase())
276
+ .join('-');
277
+ }
278
+
279
+ // Drag and drop handlers for field assignment
280
+ function handleFieldDragStart(e: DragEvent, field: string) {
281
+ draggedField = field;
282
+ if (e.dataTransfer) {
283
+ e.dataTransfer.effectAllowed = 'move';
284
+ e.dataTransfer.setData('text/plain', field);
285
+ }
286
+ }
287
+
288
+ function handleFieldDragEnd() {
289
+ draggedField = null;
290
+ dragOverTab = null;
291
+ dragOverField = null;
292
+ }
293
+
294
+ function handleTabDragOver(e: DragEvent, tab: TabAssignment) {
295
+ e.preventDefault();
296
+ dragOverTab = tab;
297
+ if (e.dataTransfer) {
298
+ e.dataTransfer.dropEffect = 'move';
299
+ }
300
+ }
301
+
302
+ function handleTabDragLeave() {
303
+ dragOverTab = null;
304
+ }
305
+
306
+ function handleTabDrop(e: DragEvent, tab: TabAssignment, targetIndex?: number) {
307
+ e.preventDefault();
308
+ if (draggedField && fieldConfigs.has(draggedField)) {
309
+ const config = fieldConfigs.get(draggedField)!;
310
+ config.tab = tab;
311
+
312
+ // If dropping at a specific position, update display orders
313
+ if (targetIndex !== undefined && tab !== null) {
314
+ // Get all fields in this tab
315
+ const tabFields = Array.from(fieldConfigs.entries())
316
+ .filter(([_, cfg]) => cfg.tab === tab)
317
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder);
318
+
319
+ // Remove the dragged field from the list if it was already in this tab
320
+ const filteredFields = tabFields.filter(([field]) => field !== draggedField);
321
+
322
+ // Insert at the target position
323
+ filteredFields.splice(targetIndex, 0, [draggedField, config]);
324
+
325
+ // Update display orders for all fields in this tab
326
+ filteredFields.forEach(([field, cfg], index) => {
327
+ cfg.displayOrder = index;
328
+ fieldConfigs.set(field, cfg);
329
+ });
330
+ } else if (tab !== null) {
331
+ // If no specific position, add to end
332
+ const maxOrder = Math.max(
333
+ 0,
334
+ ...Array.from(fieldConfigs.values())
335
+ .filter((cfg) => cfg.tab === tab)
336
+ .map((cfg) => cfg.displayOrder)
337
+ );
338
+ config.displayOrder = maxOrder + 1;
339
+ }
340
+
341
+ fieldConfigs.set(draggedField, config);
342
+ fieldConfigs = fieldConfigs; // Trigger reactivity
343
+ }
344
+ draggedField = null;
345
+ dragOverTab = null;
346
+ }
347
+
348
+ function handleFieldDragOver(e: DragEvent, field: string) {
349
+ e.preventDefault();
350
+ e.stopPropagation();
351
+ dragOverField = field;
352
+ }
353
+
354
+ function handleFieldDragLeave() {
355
+ dragOverField = null;
356
+ }
357
+
358
+ function handleFieldDrop(e: DragEvent, targetField: string, tab: TabAssignment) {
359
+ e.preventDefault();
360
+ e.stopPropagation();
361
+
362
+ if (draggedField && draggedField !== targetField) {
363
+ // Find the index of the target field
364
+ const tabFields = Array.from(fieldConfigs.entries())
365
+ .filter(([_, cfg]) => cfg.tab === tab)
366
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder);
367
+
368
+ const targetIndex = tabFields.findIndex(([field]) => field === targetField);
369
+ if (targetIndex !== -1) {
370
+ handleTabDrop(e, tab, targetIndex);
371
+ }
372
+ }
373
+ dragOverField = null;
374
+ }
375
+
376
+ async function importSpreadsheet() {
377
+ if (!fileData || !fileName) return;
378
+
379
+ // Validate required fields
380
+ if (!controlIdField) {
381
+ errorMessage = 'Please select a Control ID field before importing';
382
+ successMessage = ''; // Clear any previous success message
383
+ return;
384
+ }
385
+
386
+ if (!controlSetName || controlSetName.trim() === '') {
387
+ errorMessage = 'Please enter a Control Set Name before importing';
388
+ successMessage = ''; // Clear any previous success message
389
+ return;
390
+ }
391
+
392
+ isLoading = true;
393
+ errorMessage = '';
394
+ successMessage = '';
395
+
396
+ try {
397
+ const formData = new FormData();
398
+
399
+ // Add the file
400
+ formData.append('file', fileData, fileName);
401
+
402
+ // Add configuration
403
+ formData.append('controlIdField', controlIdField);
404
+ formData.append('startRow', headerRow.toString());
405
+ formData.append('namingConvention', 'kebab-case');
406
+ formData.append('skipEmpty', 'true');
407
+ formData.append('skipEmptyRows', 'true');
408
+ formData.append('controlSetName', controlSetName || fileName.replace(/\.[^.]+$/, ''));
409
+ formData.append(
410
+ 'controlSetDescription',
411
+ controlSetDescription || `Imported from ${fileName}`
412
+ );
413
+
414
+ // Add field schema configuration - include all fields that are assigned to a tab
415
+ const fieldSchema = Array.from(fieldConfigs.entries())
416
+ .filter(([field, config]) => config.tab !== null)
417
+ .map(([field, config]) => ({
418
+ fieldName: cleanFieldName(field),
419
+ ...config
420
+ }));
421
+ formData.append('fieldSchema', JSON.stringify(fieldSchema));
422
+
423
+ const response = await fetch('/api/import-spreadsheet', {
424
+ method: 'POST',
425
+ body: formData
426
+ });
427
+
428
+ if (!response.ok) {
429
+ const error = await response.json();
430
+ throw new Error(error.error || 'Import failed');
431
+ }
432
+
433
+ const result = await response.json();
434
+ successMessage = `Successfully imported ${result.controlCount} controls into ${result.families.length} families`;
435
+
436
+ // Dispatch event to parent
437
+ dispatch('created', { path: result.outputDir });
438
+ } catch (error) {
439
+ errorMessage = 'Error importing spreadsheet: ' + (error as Error).message;
440
+ } finally {
441
+ isLoading = false;
442
+ }
443
+ }
444
+ </script>
445
+
446
+ <div class="space-y-6">
447
+ <!-- File Upload Area -->
448
+ <div
449
+ on:dragover={handleDragOver}
450
+ on:dragleave={handleDragLeave}
451
+ on:drop={handleDrop}
452
+ role="button"
453
+ tabindex="0"
454
+ class="relative"
455
+ >
456
+ <label
457
+ class="flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer transition-all duration-300 {dragActive
458
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
459
+ : 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600'}"
460
+ >
461
+ <div class="flex flex-col items-center justify-center pt-5 pb-6">
462
+ <CloudUpload class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" />
463
+ <p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
464
+ <span class="font-semibold">Click to upload</span> or drag and drop
465
+ </p>
466
+ <p class="text-xs text-gray-500 dark:text-gray-400">XLSX, XLS or CSV files</p>
467
+ </div>
468
+ <input on:change={handleFileSelect} type="file" class="hidden" accept=".xlsx,.xls,.csv" />
469
+ </label>
470
+ </div>
471
+
472
+ {#if fileName}
473
+ <div
474
+ class="p-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
475
+ >
476
+ <div class="flex items-center">
477
+ <svg
478
+ class="flex-shrink-0 inline w-4 h-4 mr-3"
479
+ aria-hidden="true"
480
+ xmlns="http://www.w3.org/2000/svg"
481
+ fill="currentColor"
482
+ viewBox="0 0 20 20"
483
+ >
484
+ <path
485
+ d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"
486
+ />
487
+ </svg>
488
+ <div>
489
+ <span class="font-medium">File loaded:</span>
490
+ {fileName}
491
+ <div class="mt-1">
492
+ <span class="font-medium">Sheets:</span>
493
+ {sheets.length} |
494
+ <span class="font-medium">Fields:</span>
495
+ {fields.length} |
496
+ <span class="font-medium">Controls found:</span>
497
+ {controlCount}
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ {/if}
503
+
504
+ {#if errorMessage}
505
+ <div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400">
506
+ <div class="flex items-center">
507
+ <svg
508
+ class="flex-shrink-0 inline w-4 h-4 mr-3"
509
+ aria-hidden="true"
510
+ xmlns="http://www.w3.org/2000/svg"
511
+ fill="currentColor"
512
+ viewBox="0 0 20 20"
513
+ >
514
+ <path
515
+ d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"
516
+ />
517
+ </svg>
518
+ <span>{errorMessage}</span>
519
+ </div>
520
+ </div>
521
+ {/if}
522
+
523
+ {#if successMessage}
524
+ <div
525
+ class="p-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400"
526
+ >
527
+ <div class="flex items-center">
528
+ <svg
529
+ class="flex-shrink-0 inline w-4 h-4 mr-3"
530
+ aria-hidden="true"
531
+ xmlns="http://www.w3.org/2000/svg"
532
+ fill="currentColor"
533
+ viewBox="0 0 20 20"
534
+ >
535
+ <path
536
+ d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"
537
+ />
538
+ </svg>
539
+ <span>{successMessage}</span>
540
+ </div>
541
+ </div>
542
+ {/if}
543
+
544
+ {#if showFieldMapping && fields.length > 0}
545
+ <!-- Import Options -->
546
+ <div
547
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700"
548
+ >
549
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Import Options</h3>
550
+
551
+ <!-- Control Set Details -->
552
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
553
+ <div>
554
+ <label
555
+ for="controlSetName"
556
+ class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
557
+ >
558
+ Control Set Name <span class="text-red-500">*</span>
559
+ </label>
560
+ <input
561
+ type="text"
562
+ id="controlSetName"
563
+ bind:value={controlSetName}
564
+ placeholder="e.g., NIST 800-53 Rev 4"
565
+ class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
566
+ required
567
+ />
568
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
569
+ This will be used as the display name and folder name
570
+ </p>
571
+ </div>
572
+
573
+ <div>
574
+ <label
575
+ for="controlSetDescription"
576
+ class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
577
+ >
578
+ Description
579
+ </label>
580
+ <input
581
+ type="text"
582
+ id="controlSetDescription"
583
+ bind:value={controlSetDescription}
584
+ placeholder="Optional description"
585
+ class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
586
+ />
587
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
588
+ Brief description of this control set
589
+ </p>
590
+ </div>
591
+ </div>
592
+
593
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
594
+ <div>
595
+ <label for="sheet" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
596
+ Sheet
597
+ </label>
598
+ <select
599
+ id="sheet"
600
+ bind:value={selectedSheet}
601
+ on:change={() => {
602
+ loadSheetData();
603
+ }}
604
+ class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
605
+ >
606
+ {#each sheets as sheet}
607
+ <option value={sheet}>{sheet}</option>
608
+ {/each}
609
+ </select>
610
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
611
+ Select which worksheet contains your control data
612
+ </p>
613
+ </div>
614
+
615
+ <div>
616
+ <label
617
+ for="headerRow"
618
+ class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
619
+ >
620
+ Select Header Row
621
+ </label>
622
+ <select
623
+ id="headerRow"
624
+ bind:value={headerRow}
625
+ on:change={loadSheetData}
626
+ class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
627
+ >
628
+ {#each rowPreviews as preview}
629
+ <option value={preview.row}>
630
+ Row {preview.row}: {preview.preview}
631
+ </option>
632
+ {/each}
633
+ </select>
634
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
635
+ Select the row containing column headers
636
+ </p>
637
+ </div>
638
+
639
+ <div>
640
+ <label
641
+ for="controlIdField"
642
+ class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
643
+ >
644
+ Control ID Field <span class="text-red-500">*</span>
645
+ </label>
646
+ <select
647
+ id="controlIdField"
648
+ bind:value={controlIdField}
649
+ class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white {!controlIdField
650
+ ? 'border-red-500'
651
+ : ''}"
652
+ required
653
+ >
654
+ <option value="" disabled>Select Control ID field</option>
655
+ {#each fields as field}
656
+ {@const exampleValue =
657
+ sampleData.length > 0 && sampleData[0][field]
658
+ ? String(sampleData[0][field]).slice(0, 30)
659
+ : ''}
660
+ {@const hasShortValues =
661
+ !sampleData.length ||
662
+ sampleData.every((row) => !row[field] || String(row[field]).length < 25)}
663
+ {@const nonEmptyValues = sampleData
664
+ .map((row) => row[field])
665
+ .filter((v) => v != null && v !== '' && String(v).trim() !== '')}
666
+ {@const uniqueValues = new Set(nonEmptyValues)}
667
+ {@const hasUniqueValues =
668
+ !nonEmptyValues.length || uniqueValues.size === nonEmptyValues.length}
669
+ {@const hasNonEmptyValues = nonEmptyValues.length > 0}
670
+ {#if hasShortValues && hasUniqueValues && hasNonEmptyValues}
671
+ <option value={field}>
672
+ {field}{exampleValue ? ` (e.g., ${exampleValue})` : ''}
673
+ </option>
674
+ {/if}
675
+ {/each}
676
+ </select>
677
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
678
+ Column containing unique control identifiers (e.g., AC-1, SC-7)
679
+ </p>
680
+ </div>
681
+ </div>
682
+ </div>
683
+
684
+ <!-- Field Configuration -->
685
+ <div
686
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700"
687
+ >
688
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Organize Fields</h3>
689
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
690
+ Drag fields to organize them. <strong>Overview fields</strong> will appear as table columns in
691
+ the controls list.
692
+ </p>
693
+
694
+ <!-- Column Layout -->
695
+ <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
696
+ <!-- Excluded Fields Column -->
697
+ <div
698
+ class="border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
699
+ >
700
+ <div
701
+ class="p-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 rounded-t-lg"
702
+ >
703
+ <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Excluded Fields</h4>
704
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Not imported</p>
705
+ </div>
706
+ <div
707
+ class="p-3 min-h-[400px] max-h-[600px] overflow-y-auto space-y-2 transition-colors
708
+ {dragOverTab === null ? 'bg-gray-100 dark:bg-gray-800' : ''}"
709
+ on:dragover={(e) => handleTabDragOver(e, null)}
710
+ on:dragleave={handleTabDragLeave}
711
+ on:drop={(e) => handleTabDrop(e, null)}
712
+ role="region"
713
+ aria-label="Excluded fields drop zone"
714
+ >
715
+ {#each fields.filter((f) => !fieldConfigs.get(f) || fieldConfigs.get(f)?.tab === null) as field (field)}
716
+ <div
717
+ draggable="true"
718
+ on:dragstart={(e) => handleFieldDragStart(e, field)}
719
+ on:dragend={handleFieldDragEnd}
720
+ role="button"
721
+ aria-label="Drag {field} field"
722
+ tabindex="0"
723
+ class="flex items-center px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded text-sm cursor-move hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors opacity-75"
724
+ >
725
+ <svg class="w-3 h-3 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
726
+ <path
727
+ d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"
728
+ />
729
+ </svg>
730
+ <span class="truncate line-through">{field}</span>
731
+ {#if field === controlIdField}
732
+ <span class="ml-auto text-xs text-blue-600 dark:text-blue-400">ID</span>
733
+ {/if}
734
+ </div>
735
+ {/each}
736
+ {#if fields.filter((f) => !fieldConfigs.get(f) || fieldConfigs.get(f)?.tab === null).length === 0}
737
+ <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
738
+ No excluded fields
739
+ </p>
740
+ {/if}
741
+ </div>
742
+ </div>
743
+
744
+ <!-- Overview Tab Column -->
745
+ <div
746
+ class="border border-blue-300 dark:border-blue-700 rounded-lg bg-white dark:bg-gray-800"
747
+ >
748
+ <div
749
+ class="p-3 border-b border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20 rounded-t-lg"
750
+ >
751
+ <h4 class="text-sm font-semibold text-blue-700 dark:text-blue-300">Overview Tab</h4>
752
+ <p class="text-xs text-blue-600 dark:text-blue-400 mt-1">
753
+ Shows in details & table columns
754
+ </p>
755
+ </div>
756
+ <div
757
+ class="p-3 min-h-[400px] max-h-[600px] overflow-y-auto space-y-2 transition-colors
758
+ {dragOverTab === 'overview' ? 'bg-blue-50 dark:bg-blue-900/10' : ''}"
759
+ on:dragover={(e) => handleTabDragOver(e, 'overview')}
760
+ on:dragleave={handleTabDragLeave}
761
+ on:drop={(e) => handleTabDrop(e, 'overview')}
762
+ role="region"
763
+ aria-label="Overview tab drop zone"
764
+ >
765
+ {#each Array.from(fieldConfigs.entries())
766
+ .filter(([field, config]) => config.tab === 'overview')
767
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
768
+ <div
769
+ draggable="true"
770
+ on:dragstart={(e) => handleFieldDragStart(e, field)}
771
+ on:dragend={handleFieldDragEnd}
772
+ on:dragover={(e) => handleFieldDragOver(e, field)}
773
+ on:dragleave={handleFieldDragLeave}
774
+ on:drop={(e) => handleFieldDrop(e, field, 'overview')}
775
+ role="button"
776
+ aria-label="{field} field in Overview tab"
777
+ tabindex="0"
778
+ class="flex items-center px-3 py-2 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded text-sm cursor-move hover:bg-blue-200 dark:hover:bg-blue-800/30 transition-colors
779
+ {dragOverField === field && draggedField !== field
780
+ ? 'border-t-2 border-blue-500'
781
+ : ''}"
782
+ >
783
+ <Draggable class="w-3 h-3 mr-2 flex-shrink-0" />
784
+ <span class="truncate">{field}</span>
785
+ </div>
786
+ {/each}
787
+ {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'overview').length === 0}
788
+ <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
789
+ Drop fields here
790
+ </p>
791
+ {/if}
792
+ </div>
793
+ </div>
794
+
795
+ <!-- Implementation Tab Column -->
796
+ <div
797
+ class="border border-green-300 dark:border-green-700 rounded-lg bg-white dark:bg-gray-800"
798
+ >
799
+ <div
800
+ class="p-3 border-b border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 rounded-t-lg"
801
+ >
802
+ <h4 class="text-sm font-semibold text-green-700 dark:text-green-300">
803
+ Implementation Tab
804
+ </h4>
805
+ <p class="text-xs text-green-600 dark:text-green-400 mt-1">Status & compliance</p>
806
+ </div>
807
+ <div
808
+ class="p-3 min-h-[400px] max-h-[600px] overflow-y-auto space-y-2 transition-colors
809
+ {dragOverTab === 'implementation' ? 'bg-green-50 dark:bg-green-900/10' : ''}"
810
+ on:dragover={(e) => handleTabDragOver(e, 'implementation')}
811
+ on:dragleave={handleTabDragLeave}
812
+ on:drop={(e) => handleTabDrop(e, 'implementation')}
813
+ role="region"
814
+ aria-label="Implementation tab drop zone"
815
+ >
816
+ {#each Array.from(fieldConfigs.entries())
817
+ .filter(([field, config]) => config.tab === 'implementation')
818
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
819
+ <div
820
+ draggable="true"
821
+ on:dragstart={(e) => handleFieldDragStart(e, field)}
822
+ on:dragend={handleFieldDragEnd}
823
+ on:dragover={(e) => handleFieldDragOver(e, field)}
824
+ on:dragleave={handleFieldDragLeave}
825
+ on:drop={(e) => handleFieldDrop(e, field, 'implementation')}
826
+ role="button"
827
+ aria-label="{field} field in Implementation tab"
828
+ tabindex="0"
829
+ class="flex items-center px-3 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded text-sm cursor-move hover:bg-green-200 dark:hover:bg-green-800/30 transition-colors
830
+ {dragOverField === field && draggedField !== field
831
+ ? 'border-t-2 border-green-500'
832
+ : ''}"
833
+ >
834
+ <Draggable class="w-3 h-3 mr-2 flex-shrink-0" />
835
+ <span class="truncate">{field}</span>
836
+ </div>
837
+ {/each}
838
+ {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'implementation').length === 0}
839
+ <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
840
+ Drop fields here
841
+ </p>
842
+ {/if}
843
+ </div>
844
+ </div>
845
+
846
+ <!-- Custom Tab Column -->
847
+ <div
848
+ class="border border-purple-300 dark:border-purple-700 rounded-lg bg-white dark:bg-gray-800"
849
+ >
850
+ <div
851
+ class="p-3 border-b border-purple-200 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 rounded-t-lg"
852
+ >
853
+ <h4 class="text-sm font-semibold text-purple-700 dark:text-purple-300">Custom Tab</h4>
854
+ <p class="text-xs text-purple-600 dark:text-purple-400 mt-1">Additional fields</p>
855
+ </div>
856
+ <div
857
+ class="p-3 min-h-[400px] max-h-[600px] overflow-y-auto space-y-2 transition-colors
858
+ {dragOverTab === 'custom' ? 'bg-purple-50 dark:bg-purple-900/10' : ''}"
859
+ on:dragover={(e) => handleTabDragOver(e, 'custom')}
860
+ on:dragleave={handleTabDragLeave}
861
+ on:drop={(e) => handleTabDrop(e, 'custom')}
862
+ role="region"
863
+ aria-label="Custom fields drop zone"
864
+ >
865
+ {#each Array.from(fieldConfigs.entries())
866
+ .filter(([field, config]) => config.tab === 'custom')
867
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
868
+ <div
869
+ draggable="true"
870
+ on:dragstart={(e) => handleFieldDragStart(e, field)}
871
+ on:dragend={handleFieldDragEnd}
872
+ on:dragover={(e) => handleFieldDragOver(e, field)}
873
+ on:dragleave={handleFieldDragLeave}
874
+ on:drop={(e) => handleFieldDrop(e, field, 'custom')}
875
+ role="button"
876
+ aria-label="{field} field in Custom tab"
877
+ tabindex="0"
878
+ class="flex items-center px-3 py-2 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded text-sm cursor-move hover:bg-purple-200 dark:hover:bg-purple-800/30 transition-colors
879
+ {dragOverField === field && draggedField !== field
880
+ ? 'border-t-2 border-purple-500'
881
+ : ''}"
882
+ >
883
+ <Draggable class="w-3 h-3 mr-2 flex-shrink-0" />
884
+ <span class="truncate">{field}</span>
885
+ </div>
886
+ {/each}
887
+ {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'custom').length === 0}
888
+ <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
889
+ Drop fields here
890
+ </p>
891
+ {/if}
892
+ </div>
893
+ </div>
894
+ </div>
895
+ </div>
896
+
897
+ <!-- Sample Data Preview -->
898
+ {#if sampleData.length > 0}
899
+ <div
900
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700"
901
+ >
902
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
903
+ Sample Data Preview
904
+ </h3>
905
+ <div class="overflow-x-auto">
906
+ <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
907
+ <thead
908
+ class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-gray-600 dark:text-gray-400"
909
+ >
910
+ <tr>
911
+ {#each fields.slice(0, 5) as field}
912
+ <th class="px-4 py-2">{field}</th>
913
+ {/each}
914
+ </tr>
915
+ </thead>
916
+ <tbody>
917
+ {#each sampleData as row}
918
+ <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
919
+ {#each fields.slice(0, 5) as field}
920
+ <td class="px-4 py-2">{row[field] || ''}</td>
921
+ {/each}
922
+ </tr>
923
+ {/each}
924
+ </tbody>
925
+ </table>
926
+ </div>
927
+ </div>
928
+ {/if}
929
+
930
+ <!-- Import Button -->
931
+ <div class="flex justify-center">
932
+ <button
933
+ on:click={importSpreadsheet}
934
+ disabled={isLoading || !fileData || !controlIdField}
935
+ class="px-5 py-2.5 text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed"
936
+ title={!controlIdField ? 'Please select a Control ID field' : ''}
937
+ >
938
+ {#if isLoading}
939
+ <span class="flex items-center">
940
+ <svg
941
+ class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
942
+ xmlns="http://www.w3.org/2000/svg"
943
+ fill="none"
944
+ viewBox="0 0 24 24"
945
+ >
946
+ <circle
947
+ class="opacity-25"
948
+ cx="12"
949
+ cy="12"
950
+ r="10"
951
+ stroke="currentColor"
952
+ stroke-width="4"
953
+ ></circle>
954
+ <path
955
+ class="opacity-75"
956
+ fill="currentColor"
957
+ 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"
958
+ ></path>
959
+ </svg>
960
+ Importing...
961
+ </span>
962
+ {:else}
963
+ Import to Control Set
964
+ {/if}
965
+ </button>
966
+ </div>
967
+ {/if}
968
+ </div>