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,298 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { DynamicControlForm } from '$components/forms';
6
+ import type { ControlSchema, ValidationResult } from '$lib/form-types';
7
+ import type { Control } from '$lib/types';
8
+ import { mappings } from '$stores/compliance';
9
+ import { Close } from 'carbon-icons-svelte';
10
+
11
+ interface Props {
12
+ control: Control;
13
+ schema: ControlSchema;
14
+ onClose: () => void;
15
+ onSave: (control: Control) => void;
16
+ }
17
+
18
+ let { control, schema, onClose, onSave }: Props = $props();
19
+
20
+ let editedControl = $state({ ...control });
21
+ let activeTab = $state('details');
22
+ let validationResult = $state<ValidationResult | null>(null);
23
+
24
+ function handleSave() {
25
+ // Validate before saving if we have validation results
26
+ if (!validationResult?.valid) {
27
+ // Show validation errors
28
+ return;
29
+ }
30
+ onSave(editedControl);
31
+ }
32
+
33
+ function handleValidation(result: ValidationResult) {
34
+ validationResult = result;
35
+ }
36
+
37
+ function parseCCIsFromNarrative(narrative: string): string[] {
38
+ const cciPattern = /CCI-(\d{6})/g;
39
+ const matches = narrative.match(cciPattern);
40
+ return matches ? [...new Set(matches)] : [];
41
+ }
42
+
43
+ let ccisInNarrative = $derived(
44
+ parseCCIsFromNarrative(editedControl['control-implementation-narrative'] || '')
45
+ );
46
+ let associatedMappings = $derived($mappings.filter((m) => m.control_id === control.id));
47
+
48
+ // Group fields by section for tabbed interface
49
+ const detailsFields = $derived(
50
+ schema.fields.filter((f) => f.group === 'identification' || f.group === 'description')
51
+ );
52
+
53
+ const implementationFields = $derived(schema.fields.filter((f) => f.group === 'implementation'));
54
+
55
+ const complianceFields = $derived(schema.fields.filter((f) => f.group === 'compliance'));
56
+
57
+ // Count validation errors per tab
58
+ const detailsErrors = $derived(
59
+ validationResult?.errors.filter((e) => detailsFields.some((f) => f.id === e.field)).length || 0
60
+ );
61
+
62
+ const implementationErrors = $derived(
63
+ validationResult?.errors.filter((e) => implementationFields.some((f) => f.id === e.field))
64
+ .length || 0
65
+ );
66
+
67
+ const complianceErrors = $derived(
68
+ validationResult?.errors.filter((e) => complianceFields.some((f) => f.id === e.field)).length ||
69
+ 0
70
+ );
71
+ </script>
72
+
73
+ <!-- Modal Backdrop -->
74
+ <div
75
+ class="fixed inset-0 bg-gray-700 dark:bg-gray-900 bg-opacity-50 dark:bg-opacity-75 overflow-y-auto h-full w-full z-50"
76
+ role="dialog"
77
+ aria-modal="true"
78
+ tabindex="-1"
79
+ onclick={onClose}
80
+ onkeydown={(e) => e.key === 'Escape' && onClose()}
81
+ >
82
+ <!-- Modal -->
83
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
84
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
85
+ <div
86
+ class="relative min-h-screen w-full p-6 bg-white dark:bg-gray-900"
87
+ role="document"
88
+ onclick={(e) => e.stopPropagation()}
89
+ onkeydown={(e) => e.stopPropagation()}
90
+ >
91
+ <!-- Header -->
92
+ <div
93
+ class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-gray-700"
94
+ >
95
+ <div>
96
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
97
+ Edit Control: {control['control-acronym']}
98
+ </h3>
99
+ {#if !validationResult?.valid && validationResult?.errors.length}
100
+ <p class="text-sm text-red-600 dark:text-red-400 mt-1">
101
+ {validationResult.errors.length} validation error{validationResult.errors.length === 1
102
+ ? ''
103
+ : 's'} found
104
+ </p>
105
+ {/if}
106
+ </div>
107
+ <button
108
+ onclick={onClose}
109
+ class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
110
+ aria-label="Close modal"
111
+ >
112
+ <Close class="w-6 h-6" />
113
+ </button>
114
+ </div>
115
+
116
+ <!-- Tabs -->
117
+ <div class="mt-4">
118
+ <nav class="flex space-x-8">
119
+ <button
120
+ onclick={() => (activeTab = 'details')}
121
+ class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'details'
122
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
123
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
124
+ >
125
+ Details
126
+ {#if detailsErrors > 0}
127
+ <span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
128
+ {detailsErrors}
129
+ </span>
130
+ {/if}
131
+ </button>
132
+ <button
133
+ onclick={() => (activeTab = 'implementation')}
134
+ class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'implementation'
135
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
136
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
137
+ >
138
+ Implementation
139
+ {#if implementationErrors > 0}
140
+ <span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
141
+ {implementationErrors}
142
+ </span>
143
+ {/if}
144
+ </button>
145
+ <button
146
+ onclick={() => (activeTab = 'compliance')}
147
+ class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'compliance'
148
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
149
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
150
+ >
151
+ Compliance
152
+ {#if complianceErrors > 0}
153
+ <span class="ml-1 bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full">
154
+ {complianceErrors}
155
+ </span>
156
+ {/if}
157
+ </button>
158
+ <button
159
+ onclick={() => (activeTab = 'mappings')}
160
+ class="py-2 px-1 border-b-2 font-medium text-sm {activeTab === 'mappings'
161
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
162
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
163
+ >
164
+ Mappings ({associatedMappings.length})
165
+ </button>
166
+ </nav>
167
+ </div>
168
+
169
+ <!-- Tab Content -->
170
+ <div class="mt-6 max-h-screen-3/4 overflow-y-auto">
171
+ {#if activeTab === 'details'}
172
+ <DynamicControlForm
173
+ bind:control={editedControl}
174
+ schema={{
175
+ ...schema,
176
+ fields: detailsFields
177
+ }}
178
+ onValidation={handleValidation}
179
+ />
180
+ {:else if activeTab === 'implementation'}
181
+ <DynamicControlForm
182
+ bind:control={editedControl}
183
+ schema={{
184
+ ...schema,
185
+ fields: implementationFields
186
+ }}
187
+ onValidation={handleValidation}
188
+ />
189
+
190
+ {#if ccisInNarrative.length > 0}
191
+ <div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
192
+ <div class="block text-sm font-medium text-blue-700 dark:text-blue-300 mb-2">
193
+ CCIs Found in Narrative
194
+ </div>
195
+ <div class="flex flex-wrap gap-2">
196
+ {#each ccisInNarrative as cci}
197
+ <span
198
+ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200"
199
+ >
200
+ {cci}
201
+ </span>
202
+ {/each}
203
+ </div>
204
+ </div>
205
+ {/if}
206
+ {:else if activeTab === 'compliance'}
207
+ <DynamicControlForm
208
+ bind:control={editedControl}
209
+ schema={{
210
+ ...schema,
211
+ fields: complianceFields
212
+ }}
213
+ onValidation={handleValidation}
214
+ />
215
+ {:else if activeTab === 'mappings'}
216
+ <div class="space-y-4">
217
+ {#if associatedMappings.length > 0}
218
+ {#each associatedMappings as mapping}
219
+ <div
220
+ class="border border-gray-200 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-800"
221
+ >
222
+ <div class="flex justify-between items-start mb-2">
223
+ <span class="text-sm font-medium text-gray-900 dark:text-white"
224
+ >@lula {mapping.uuid}</span
225
+ >
226
+ <button
227
+ onclick={() => navigator.clipboard.writeText(`@lula ${mapping.uuid}`)}
228
+ class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
229
+ >
230
+ Copy UUID
231
+ </button>
232
+ </div>
233
+ <p class="text-sm text-gray-700 dark:text-gray-300 mb-2">{mapping.justification}</p>
234
+ <div
235
+ class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
236
+ >
237
+ <span>Status: {mapping.status}</span>
238
+ <span>By: {mapping.created_by}</span>
239
+ </div>
240
+ </div>
241
+ {/each}
242
+ {:else}
243
+ <div class="text-center py-8">
244
+ <svg
245
+ class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
246
+ fill="none"
247
+ viewBox="0 0 24 24"
248
+ stroke="currentColor"
249
+ >
250
+ <path
251
+ stroke-linecap="round"
252
+ stroke-linejoin="round"
253
+ stroke-width="2"
254
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
255
+ />
256
+ </svg>
257
+ <h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No mappings</h3>
258
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
259
+ This control has no associated mappings yet.
260
+ </p>
261
+ </div>
262
+ {/if}
263
+ </div>
264
+ {/if}
265
+ </div>
266
+
267
+ <!-- Footer -->
268
+ <div
269
+ class="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700 mt-6"
270
+ >
271
+ <!-- Schema Info -->
272
+ <div class="text-xs text-gray-500 dark:text-gray-400">
273
+ Schema: {schema.name} v{schema.version}
274
+ <span class="ml-2 text-green-600 dark:text-green-400">Dynamic Forms</span>
275
+ </div>
276
+
277
+ <!-- Action Buttons -->
278
+ <div class="flex space-x-3">
279
+ <button
280
+ onclick={onClose}
281
+ class="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-md hover:bg-gray-50 dark:hover:bg-gray-700"
282
+ >
283
+ Cancel
284
+ </button>
285
+ <button
286
+ onclick={handleSave}
287
+ disabled={!validationResult?.valid}
288
+ class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
289
+ >
290
+ Save Changes
291
+ {#if !validationResult?.valid && validationResult?.errors.length}
292
+ <span class="ml-1">({validationResult.errors.length} errors)</span>
293
+ {/if}
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
@@ -0,0 +1,105 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { Mapping } from '$lib/types';
6
+ import { StatusBadge } from '../ui';
7
+
8
+ interface Props {
9
+ mapping: Mapping;
10
+ onEdit?: (mapping: Mapping) => void;
11
+ onDelete?: (uuid: string) => void;
12
+ showActions?: boolean;
13
+ }
14
+
15
+ let { mapping, onEdit, onDelete, showActions = false }: Props = $props();
16
+
17
+ function handleCopyUuid() {
18
+ navigator.clipboard.writeText(`@lula ${mapping.uuid}`);
19
+ }
20
+
21
+ function handleEdit() {
22
+ onEdit?.(mapping);
23
+ }
24
+
25
+ function handleDelete() {
26
+ if (confirm('Are you sure you want to delete this mapping?')) {
27
+ onDelete?.(mapping.uuid);
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <div
33
+ class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200"
34
+ >
35
+ <!-- Header -->
36
+ <div
37
+ class="bg-gray-100 dark:bg-gray-900 rounded-t-xl px-6 py-4 border-b border-gray-200 dark:border-gray-700"
38
+ >
39
+ <div class="flex justify-between items-center">
40
+ <span
41
+ class="text-xs font-mono font-medium text-gray-600 dark:text-gray-300"
42
+ >
43
+ {mapping.uuid}
44
+ </span>
45
+ <div class="flex items-center space-x-2">
46
+ <button
47
+ onclick={handleCopyUuid}
48
+ class="inline-flex items-center px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 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 transition-all duration-200 shadow-sm hover:shadow"
49
+ title="Copy UUID to clipboard"
50
+ >
51
+ Copy UUID
52
+ </button>
53
+ {#if showActions}
54
+ <button
55
+ onclick={handleEdit}
56
+ class="inline-flex items-center px-3 py-2 text-xs font-medium text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-100 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-all duration-200 shadow-sm hover:shadow"
57
+ title="Edit mapping"
58
+ >
59
+ Edit
60
+ </button>
61
+ <button
62
+ onclick={handleDelete}
63
+ class="inline-flex items-center px-3 py-2 text-xs font-medium text-red-600 dark:text-red-300 hover:text-red-800 dark:hover:text-red-100 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/50 transition-all duration-200 shadow-sm hover:shadow"
64
+ title="Delete mapping"
65
+ >
66
+ Delete
67
+ </button>
68
+ {/if}
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Content -->
74
+ <div class="px-6 py-4">
75
+ <p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
76
+ {mapping.justification}
77
+ </p>
78
+
79
+ {#if mapping.source_entries && mapping.source_entries.length > 0}
80
+ <div class="mb-4">
81
+ <h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">Source References</h4>
82
+ <div class="space-y-2">
83
+ {#each mapping.source_entries as entry}
84
+ <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
85
+ <div class="flex items-start justify-between">
86
+ <span class="text-xs font-mono text-blue-600 dark:text-blue-400 break-all">
87
+ {entry.location}
88
+ </span>
89
+ {#if entry.shasum}
90
+ <span class="text-xs text-gray-500 dark:text-gray-400 ml-2" title="SHA checksum">
91
+ {entry.shasum.substring(0, 8)}...
92
+ </span>
93
+ {/if}
94
+ </div>
95
+ </div>
96
+ {/each}
97
+ </div>
98
+ </div>
99
+ {/if}
100
+
101
+ <div class="flex items-center justify-start">
102
+ <StatusBadge status={mapping.status} type="mapping" />
103
+ </div>
104
+ </div>
105
+ </div>
@@ -0,0 +1,188 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { SourceEntry } from '$lib/types';
6
+ import { Add, TrashCan } from 'carbon-icons-svelte';
7
+ import FormField from '../forms/FormField.svelte';
8
+
9
+ interface MappingFormData {
10
+ justification: string;
11
+ status: 'planned' | 'implemented' | 'verified';
12
+ source_entries: SourceEntry[];
13
+ }
14
+
15
+ interface Props {
16
+ initialData?: Partial<MappingFormData>;
17
+ onSubmit: (data: MappingFormData) => void;
18
+ onCancel: () => void;
19
+ loading?: boolean;
20
+ submitLabel?: string;
21
+ }
22
+
23
+ let {
24
+ initialData = {},
25
+ onSubmit,
26
+ onCancel,
27
+ loading = false,
28
+ submitLabel = 'Create Mapping'
29
+ }: Props = $props();
30
+
31
+ let formData = $state<MappingFormData>({
32
+ justification: initialData.justification || '',
33
+ status: initialData.status || 'planned',
34
+ source_entries: initialData.source_entries || []
35
+ });
36
+
37
+ let newLocation = $state('');
38
+ let newShasum = $state('');
39
+
40
+ const statusOptions = ['planned', 'implemented', 'verified'];
41
+
42
+ const isValid = $derived(formData.justification.trim().length > 0);
43
+
44
+ function handleSubmit() {
45
+ if (!isValid || loading) return;
46
+ onSubmit(formData);
47
+ }
48
+
49
+ function handleCancel() {
50
+ // Reset form
51
+ formData = {
52
+ justification: initialData.justification || '',
53
+ status: initialData.status || 'planned',
54
+ source_entries: initialData.source_entries || []
55
+ };
56
+ newLocation = '';
57
+ newShasum = '';
58
+ onCancel();
59
+ }
60
+
61
+ function addSourceEntry() {
62
+ if (newLocation.trim()) {
63
+ const entry: SourceEntry = {
64
+ location: newLocation.trim()
65
+ };
66
+ if (newShasum.trim()) {
67
+ entry.shasum = newShasum.trim();
68
+ }
69
+ formData.source_entries = [...formData.source_entries, entry];
70
+ newLocation = '';
71
+ newShasum = '';
72
+ }
73
+ }
74
+
75
+ function removeSourceEntry(index: number) {
76
+ formData.source_entries = formData.source_entries.filter((_, i) => i !== index);
77
+ }
78
+ </script>
79
+
80
+ <div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
81
+ <div class="space-y-8">
82
+ <!-- Main form fields -->
83
+ <div class="grid grid-cols-1 gap-8">
84
+ <FormField
85
+ id="mapping-justification"
86
+ label="Justification"
87
+ type="textarea"
88
+ bind:value={formData.justification}
89
+ rows={4}
90
+ placeholder="Explain how this compliance artifact satisfies the control requirements..."
91
+ required
92
+ />
93
+
94
+ <FormField
95
+ id="mapping-status"
96
+ label="Implementation Status"
97
+ type="select"
98
+ bind:value={formData.status}
99
+ options={statusOptions}
100
+ />
101
+ </div>
102
+
103
+ <!-- Source References -->
104
+ <div class="space-y-4">
105
+ <div class="flex items-center justify-between">
106
+ <div class="text-sm font-medium text-gray-900 dark:text-white">Source References</div>
107
+ <span class="text-xs text-gray-500 dark:text-gray-400">Optional</span>
108
+ </div>
109
+
110
+ <!-- Existing entries -->
111
+ {#if formData.source_entries.length > 0}
112
+ <div class="space-y-3">
113
+ {#each formData.source_entries as entry, index}
114
+ <div class="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
115
+ <div class="flex-1 min-w-0">
116
+ <div class="text-sm font-mono text-gray-900 dark:text-white break-all">
117
+ {entry.location}
118
+ </div>
119
+ {#if entry.shasum}
120
+ <div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
121
+ SHA: {entry.shasum.substring(0, 8)}...
122
+ </div>
123
+ {/if}
124
+ </div>
125
+ <button
126
+ type="button"
127
+ onclick={() => removeSourceEntry(index)}
128
+ class="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors"
129
+ title="Remove reference"
130
+ >
131
+ <TrashCan size={16} />
132
+ </button>
133
+ </div>
134
+ {/each}
135
+ </div>
136
+ {/if}
137
+
138
+ <!-- Add new entry -->
139
+ <div class="grid grid-cols-1 gap-4">
140
+ <input
141
+ type="text"
142
+ bind:value={newLocation}
143
+ placeholder="File path or URI (e.g., src/auth/handler.ts:42)"
144
+ class="w-full px-4 py-3 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
145
+ />
146
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
147
+ <input
148
+ type="text"
149
+ bind:value={newShasum}
150
+ placeholder="SHA checksum (optional)"
151
+ class="px-4 py-3 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
152
+ />
153
+ <button
154
+ type="button"
155
+ onclick={addSourceEntry}
156
+ disabled={!newLocation.trim()}
157
+ class="px-4 py-3 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
158
+ >
159
+ <Add size={16} />
160
+ Add Reference
161
+ </button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Action buttons -->
167
+ <div class="flex justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
168
+ <button
169
+ type="button"
170
+ onclick={handleCancel}
171
+ disabled={loading}
172
+ class="px-6 py-2.5 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-gray-500 disabled:opacity-50 transition-all duration-200"
173
+ >
174
+ Cancel
175
+ </button>
176
+ <button
177
+ type="button"
178
+ onclick={handleSubmit}
179
+ disabled={!isValid || loading}
180
+ class="px-6 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 {isValid && !loading
181
+ ? 'text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'
182
+ : 'text-gray-400 bg-gray-200 dark:bg-gray-700 cursor-not-allowed'}"
183
+ >
184
+ {loading ? 'Saving...' : submitLabel}
185
+ </button>
186
+ </div>
187
+ </div>
188
+ </div>
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as ControlDetailsPanel } from './ControlDetailsPanel.svelte';
5
+ export { default as ControlsList } from './ControlsList.svelte';
6
+ export { default as DynamicControlEditor } from './DynamicControlEditor.svelte';
7
+ export { default as MappingCard } from './MappingCard.svelte';
8
+ export { default as MappingForm } from './MappingForm.svelte';
9
+