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,103 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { FieldSchema } from '$lib/types';
6
+
7
+ interface Props {
8
+ fieldName: string;
9
+ field: FieldSchema;
10
+ value: any;
11
+ onChange: () => void;
12
+ }
13
+
14
+ let { fieldName, field, value = $bindable(), onChange }: Props = $props();
15
+
16
+ const displayName = $derived(
17
+ field.original_name || fieldName.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
18
+ );
19
+
20
+ // Generate unique ID for form control
21
+ const fieldId = $derived(`field-${fieldName}-${Math.random().toString(36).substr(2, 9)}`);
22
+ </script>
23
+
24
+ <div class="space-y-2">
25
+ <label for={fieldId} class="text-sm font-medium text-gray-700 dark:text-gray-300">
26
+ {displayName}
27
+ {#if field.required}
28
+ <span class="text-red-500">*</span>
29
+ {/if}
30
+ </label>
31
+
32
+ {#if field.ui_type === 'select' && field.options}
33
+ <select
34
+ id={fieldId}
35
+ bind:value
36
+ disabled={!field.editable}
37
+ onchange={onChange}
38
+ class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
39
+ >
40
+ <option value="">-- Select --</option>
41
+ {#each field.options as option}
42
+ <option value={option}>{option}</option>
43
+ {/each}
44
+ </select>
45
+ {:else if field.ui_type === 'textarea' || field.ui_type === 'long_text'}
46
+ <textarea
47
+ id={fieldId}
48
+ bind:value
49
+ disabled={!field.editable}
50
+ oninput={onChange}
51
+ rows="8"
52
+ class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed resize-y min-h-[120px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
53
+ placeholder={field.examples?.[0] || ''}
54
+ ></textarea>
55
+ {:else if field.ui_type === 'boolean'}
56
+ <div class="flex items-center">
57
+ <input
58
+ id={fieldId}
59
+ type="checkbox"
60
+ bind:checked={value}
61
+ disabled={!field.editable}
62
+ onchange={onChange}
63
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
64
+ />
65
+ <label class="ml-2 text-sm text-gray-900 dark:text-gray-300" for={fieldId}>
66
+ {value ? 'Yes' : 'No'}
67
+ </label>
68
+ </div>
69
+ {:else if field.ui_type === 'date'}
70
+ <input
71
+ id={fieldId}
72
+ type="date"
73
+ bind:value
74
+ disabled={!field.editable}
75
+ onchange={onChange}
76
+ class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
77
+ />
78
+ {:else if field.ui_type === 'number'}
79
+ <input
80
+ id={fieldId}
81
+ type="number"
82
+ bind:value
83
+ disabled={!field.editable}
84
+ oninput={onChange}
85
+ class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
86
+ />
87
+ {:else}
88
+ <!-- Default to text input -->
89
+ <input
90
+ id={fieldId}
91
+ type="text"
92
+ bind:value
93
+ disabled={!field.editable}
94
+ oninput={onChange}
95
+ class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
96
+ placeholder={field.examples?.[0] || ''}
97
+ />
98
+ {/if}
99
+
100
+ {#if field.is_array}
101
+ <p class="text-xs text-gray-500 dark:text-gray-400">This field supports multiple values</p>
102
+ {/if}
103
+ </div>
@@ -0,0 +1,49 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { FieldSchema } from '$lib/types';
6
+ import ProcessedTextRenderer from '../utils/ProcessedTextRenderer.svelte';
7
+
8
+ interface Props {
9
+ fieldName: string;
10
+ field: FieldSchema | null;
11
+ value: any;
12
+ readonly?: boolean;
13
+ }
14
+
15
+ let { fieldName, field, value, readonly = true }: Props = $props();
16
+
17
+ const displayName = $derived(
18
+ field?.original_name || fieldName.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
19
+ );
20
+
21
+ // Determine if this is a multiline text field that should be processed
22
+ const shouldProcessText = $derived(
23
+ field &&
24
+ (field.ui_type === 'textarea' || field.ui_type === 'long_text') &&
25
+ typeof value === 'string' &&
26
+ value.includes('\n')
27
+ );
28
+ </script>
29
+
30
+ <div class="space-y-2">
31
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400">
32
+ {displayName}
33
+ </div>
34
+ <div class="text-gray-900 dark:text-white font-medium">
35
+ {#if value === null || value === undefined || value === ''}
36
+ <span class="text-gray-400 dark:text-gray-500 italic">Not specified</span>
37
+ {:else if typeof value === 'boolean'}
38
+ <span class="capitalize">{value ? 'Yes' : 'No'}</span>
39
+ {:else if Array.isArray(value)}
40
+ <span>{value.join(', ')}</span>
41
+ {:else if shouldProcessText}
42
+ <div class="border-l-2 border-gray-200 dark:border-gray-700 pl-4">
43
+ <ProcessedTextRenderer text={value} />
44
+ </div>
45
+ {:else}
46
+ <span class="whitespace-pre-line">{value}</span>
47
+ {/if}
48
+ </div>
49
+ </div>
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as FieldRenderer } from './FieldRenderer.svelte';
5
+ export { default as EditableFieldRenderer } from './EditableFieldRenderer.svelte';
@@ -0,0 +1,130 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { Control, FieldSchema } from '$lib/types';
6
+ import { EditableFieldRenderer } from '../renderers';
7
+
8
+ interface Props {
9
+ control: Control;
10
+ fieldSchema: Record<string, FieldSchema>;
11
+ onFieldChange: (fieldName: string, value: any) => void;
12
+ }
13
+
14
+ let { control, fieldSchema, onFieldChange }: Props = $props();
15
+
16
+ // Get fields for custom tab
17
+ function getCustomFields(): Array<[string, FieldSchema]> {
18
+ return Object.entries(fieldSchema)
19
+ .filter(([_, field]) => {
20
+ const fieldTab = field.tab || getDefaultTabForCategory(field.category);
21
+ return fieldTab === 'custom' && field.visible;
22
+ })
23
+ .sort((a, b) => a[1].display_order - b[1].display_order);
24
+ }
25
+
26
+ function getDefaultTabForCategory(category: string): 'overview' | 'implementation' | 'custom' {
27
+ switch (category) {
28
+ case 'core':
29
+ case 'metadata':
30
+ return 'overview';
31
+ case 'compliance':
32
+ case 'content':
33
+ return 'implementation';
34
+ default:
35
+ return 'custom';
36
+ }
37
+ }
38
+
39
+ // Helper to determine field layout class based on field type
40
+ function getFieldLayoutClass(field: FieldSchema): string {
41
+ // Textareas and long text fields get full width
42
+ if (field.ui_type === 'textarea' || field.ui_type === 'long_text') {
43
+ return 'col-span-full';
44
+ }
45
+ // Medium text fields also get full width
46
+ if (field.ui_type === 'medium_text' && field.max_length && field.max_length > 100) {
47
+ return 'col-span-full';
48
+ }
49
+ // Dropdowns and short fields can be side by side
50
+ if (
51
+ field.ui_type === 'select' ||
52
+ field.ui_type === 'boolean' ||
53
+ field.ui_type === 'date' ||
54
+ field.ui_type === 'number' ||
55
+ (field.ui_type === 'short_text' && field.max_length && field.max_length <= 50)
56
+ ) {
57
+ return 'col-span-1';
58
+ }
59
+ // Default to full width for everything else
60
+ return 'col-span-full';
61
+ }
62
+
63
+ // Helper to group fields by layout type
64
+ function groupFieldsForLayout(fields: Array<[string, FieldSchema]>) {
65
+ const groups: Array<Array<[string, FieldSchema]>> = [];
66
+ let currentGroup: Array<[string, FieldSchema]> = [];
67
+
68
+ for (const field of fields) {
69
+ const layoutClass = getFieldLayoutClass(field[1]);
70
+
71
+ if (layoutClass === 'col-span-full') {
72
+ // Full width fields go in their own group
73
+ if (currentGroup.length > 0) {
74
+ groups.push(currentGroup);
75
+ currentGroup = [];
76
+ }
77
+ groups.push([field]);
78
+ } else {
79
+ // Half width fields can be grouped
80
+ currentGroup.push(field);
81
+ if (currentGroup.length === 2) {
82
+ groups.push(currentGroup);
83
+ currentGroup = [];
84
+ }
85
+ }
86
+ }
87
+
88
+ // Add any remaining fields
89
+ if (currentGroup.length > 0) {
90
+ groups.push(currentGroup);
91
+ }
92
+
93
+ return groups;
94
+ }
95
+
96
+ const customFields = $derived(getCustomFields());
97
+ const fieldGroups = $derived(groupFieldsForLayout(customFields));
98
+ </script>
99
+
100
+ <div class="space-y-8">
101
+ {#if customFields.length > 0}
102
+ <div class="space-y-4">
103
+ <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">
104
+ <div class="text-sm text-gray-500 dark:text-gray-400 mb-4">
105
+ Additional fields specific to your organization
106
+ </div>
107
+ <div class="space-y-8">
108
+ {#each fieldGroups as fieldGroup}
109
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
110
+ {#each fieldGroup as [fieldName, field]}
111
+ <div class={getFieldLayoutClass(field)}>
112
+ <EditableFieldRenderer
113
+ {fieldName}
114
+ {field}
115
+ bind:value={control[fieldName]}
116
+ onChange={() => onFieldChange(fieldName, control[fieldName])}
117
+ />
118
+ </div>
119
+ {/each}
120
+ </div>
121
+ {/each}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ {:else}
126
+ <div class="text-center py-12">
127
+ <p class="text-gray-500 dark:text-gray-400">No custom fields configured</p>
128
+ </div>
129
+ {/if}
130
+ </div>
@@ -0,0 +1,127 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { Control, FieldSchema } from '$lib/types';
6
+ import { FieldRenderer } from '../renderers';
7
+
8
+ interface Props {
9
+ control: Control;
10
+ fieldSchema: Record<string, FieldSchema>;
11
+ }
12
+
13
+ let { control, fieldSchema }: Props = $props();
14
+
15
+ // Get fields for implementation tab
16
+ function getImplementationFields(): Array<[string, FieldSchema]> {
17
+ return Object.entries(fieldSchema)
18
+ .filter(([_, field]) => {
19
+ const fieldTab = field.tab || getDefaultTabForCategory(field.category);
20
+ return fieldTab === 'implementation' && field.visible;
21
+ })
22
+ .sort((a, b) => a[1].display_order - b[1].display_order);
23
+ }
24
+
25
+ function getDefaultTabForCategory(category: string): 'overview' | 'implementation' | 'custom' {
26
+ switch (category) {
27
+ case 'core':
28
+ case 'metadata':
29
+ return 'overview';
30
+ case 'compliance':
31
+ case 'content':
32
+ return 'implementation';
33
+ default:
34
+ return 'custom';
35
+ }
36
+ }
37
+
38
+ // Helper to determine field layout class based on field type
39
+ function getFieldLayoutClass(field: FieldSchema): string {
40
+ // Textareas and long text fields get full width
41
+ if (field.ui_type === 'textarea' || field.ui_type === 'long_text') {
42
+ return 'col-span-full';
43
+ }
44
+ // Medium text fields also get full width
45
+ if (field.ui_type === 'medium_text' && field.max_length && field.max_length > 100) {
46
+ return 'col-span-full';
47
+ }
48
+ // Dropdowns and short fields can be side by side
49
+ if (
50
+ field.ui_type === 'select' ||
51
+ field.ui_type === 'boolean' ||
52
+ field.ui_type === 'date' ||
53
+ field.ui_type === 'number' ||
54
+ (field.ui_type === 'short_text' && field.max_length && field.max_length <= 50)
55
+ ) {
56
+ return 'col-span-1';
57
+ }
58
+ // Default to full width for everything else
59
+ return 'col-span-full';
60
+ }
61
+
62
+ // Helper to group fields by layout type
63
+ function groupFieldsForLayout(fields: Array<[string, FieldSchema]>) {
64
+ const groups: Array<Array<[string, FieldSchema]>> = [];
65
+ let currentGroup: Array<[string, FieldSchema]> = [];
66
+
67
+ for (const field of fields) {
68
+ const layoutClass = getFieldLayoutClass(field[1]);
69
+
70
+ if (layoutClass === 'col-span-full') {
71
+ // Full width fields go in their own group
72
+ if (currentGroup.length > 0) {
73
+ groups.push(currentGroup);
74
+ currentGroup = [];
75
+ }
76
+ groups.push([field]);
77
+ } else {
78
+ // Half width fields can be grouped
79
+ currentGroup.push(field);
80
+ if (currentGroup.length === 2) {
81
+ groups.push(currentGroup);
82
+ currentGroup = [];
83
+ }
84
+ }
85
+ }
86
+
87
+ // Add any remaining fields
88
+ if (currentGroup.length > 0) {
89
+ groups.push(currentGroup);
90
+ }
91
+
92
+ return groups;
93
+ }
94
+
95
+ const implementationFields = $derived(getImplementationFields());
96
+ const fieldGroups = $derived(groupFieldsForLayout(implementationFields));
97
+ </script>
98
+
99
+ <!-- Readonly Implementation Tab with Clean Styling -->
100
+ <div class="space-y-8">
101
+ {#if implementationFields.length > 0}
102
+ <div class="space-y-4">
103
+ <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">
104
+ <div class="space-y-8">
105
+ {#each fieldGroups as fieldGroup}
106
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
107
+ {#each fieldGroup as [fieldName, field]}
108
+ <div class={getFieldLayoutClass(field)}>
109
+ <FieldRenderer
110
+ {fieldName}
111
+ {field}
112
+ value={control[fieldName]}
113
+ readonly={true}
114
+ />
115
+ </div>
116
+ {/each}
117
+ </div>
118
+ {/each}
119
+ </div>
120
+ </div>
121
+ </div>
122
+ {:else}
123
+ <div class="text-center py-12">
124
+ <p class="text-gray-500 dark:text-gray-400">No implementation fields available</p>
125
+ </div>
126
+ {/if}
127
+ </div>
@@ -0,0 +1,182 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { Control, Mapping } from '$lib/types';
6
+ import { wsClient } from '$lib/websocket';
7
+ import { Add, Document } from 'carbon-icons-svelte';
8
+ import MappingCard from '../MappingCard.svelte';
9
+ import MappingForm from '../MappingForm.svelte';
10
+
11
+ interface Props {
12
+ control: Control;
13
+ mappings: Mapping[];
14
+ }
15
+
16
+ let { control, mappings }: Props = $props();
17
+
18
+ // Component state
19
+ let showNewMappingForm = $state(false);
20
+ let editingMapping = $state<Mapping | null>(null);
21
+
22
+ // Form state
23
+ let newMappingData = $state({
24
+ justification: '',
25
+ status: 'planned' as 'planned' | 'implemented' | 'verified',
26
+ source_entries: [] as { location: string; shasum?: string }[]
27
+ });
28
+
29
+ // Event handlers
30
+ async function handleCreateMapping(data: typeof newMappingData) {
31
+ try {
32
+ const mappingData = {
33
+ control_id: control.id,
34
+ justification: data.justification,
35
+ status: data.status,
36
+ source_entries: data.source_entries,
37
+ uuid: '' // Will be generated by backend
38
+ };
39
+
40
+ await wsClient.createMapping(mappingData);
41
+ resetMappingForm();
42
+ } catch (error) {
43
+ console.error('Failed to create mapping:', error);
44
+ }
45
+ }
46
+
47
+ function cancelNewMapping() {
48
+ resetMappingForm();
49
+ }
50
+
51
+ function resetMappingForm() {
52
+ newMappingData = {
53
+ justification: '',
54
+ status: 'planned',
55
+ source_entries: []
56
+ };
57
+ showNewMappingForm = false;
58
+ editingMapping = null;
59
+ }
60
+
61
+ function startEditMapping(mapping: Mapping) {
62
+ editingMapping = { ...mapping };
63
+ showNewMappingForm = true;
64
+ newMappingData = {
65
+ justification: mapping.justification,
66
+ status: mapping.status,
67
+ source_entries: mapping.source_entries || []
68
+ };
69
+ }
70
+
71
+ async function handleUpdateMapping(data: typeof newMappingData) {
72
+ if (!editingMapping) return;
73
+
74
+ try {
75
+ const updatedMapping = {
76
+ ...editingMapping,
77
+ justification: data.justification,
78
+ status: data.status,
79
+ source_entries: data.source_entries
80
+ };
81
+
82
+ await wsClient.updateMapping(updatedMapping);
83
+ resetMappingForm();
84
+ } catch (error) {
85
+ console.error('Failed to update mapping:', error);
86
+ }
87
+ }
88
+
89
+ async function handleDeleteMapping(uuid: string) {
90
+ try {
91
+ await wsClient.deleteMapping(uuid);
92
+ } catch (error) {
93
+ console.error('Failed to delete mapping:', error);
94
+ }
95
+ }
96
+ </script>
97
+
98
+ <div class="space-y-6">
99
+ <!-- Existing Mappings -->
100
+ {#if mappings.length > 0}
101
+ <div class="space-y-4">
102
+ {#each mappings as mapping}
103
+ {#if editingMapping && editingMapping.uuid === mapping.uuid}
104
+ <!-- Edit Form in place of the mapping being edited -->
105
+ <div
106
+ class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm"
107
+ >
108
+ <MappingForm
109
+ initialData={newMappingData}
110
+ onSubmit={handleUpdateMapping}
111
+ onCancel={cancelNewMapping}
112
+ submitLabel="Update Mapping"
113
+ />
114
+ </div>
115
+ {:else}
116
+ <MappingCard
117
+ {mapping}
118
+ showActions={true}
119
+ onEdit={startEditMapping}
120
+ onDelete={handleDeleteMapping}
121
+ />
122
+ {/if}
123
+ {/each}
124
+ </div>
125
+
126
+ <!-- Add New Mapping Button or Form -->
127
+ {#if showNewMappingForm && !editingMapping}
128
+ <!-- New Mapping Form appears after the button -->
129
+ <div
130
+ class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm"
131
+ >
132
+ <MappingForm
133
+ initialData={newMappingData}
134
+ onSubmit={handleCreateMapping}
135
+ onCancel={cancelNewMapping}
136
+ submitLabel="Create Mapping"
137
+ />
138
+ </div>
139
+ {:else if !editingMapping}
140
+ <button
141
+ onclick={() => (showNewMappingForm = true)}
142
+ class="w-full p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl hover:border-blue-400 dark:hover:border-blue-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors group"
143
+ >
144
+ <div class="flex items-center justify-center text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400">
145
+ <Add class="w-5 h-5 mr-2" />
146
+ <span class="text-sm font-medium">Add New Mapping</span>
147
+ </div>
148
+ </button>
149
+ {/if}
150
+ {:else}
151
+ {#if showNewMappingForm}
152
+ <!-- New Mapping Form for empty state -->
153
+ <div
154
+ class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm"
155
+ >
156
+ <MappingForm
157
+ initialData={newMappingData}
158
+ onSubmit={handleCreateMapping}
159
+ onCancel={cancelNewMapping}
160
+ submitLabel="Create Mapping"
161
+ />
162
+ </div>
163
+ {:else}
164
+ <!-- Empty State with integrated Add Button -->
165
+ <div class="text-center py-12">
166
+ <div class="mx-auto w-16 h-16 mb-4 text-gray-400 dark:text-gray-500">
167
+ <Document class="w-full h-full" />
168
+ </div>
169
+ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No mappings yet</h3>
170
+ <p class="text-gray-500 dark:text-gray-400 mb-6">Create your first mapping for this control.</p>
171
+
172
+ <button
173
+ onclick={() => (showNewMappingForm = true)}
174
+ class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
175
+ >
176
+ <Add class="w-4 h-4 mr-2" />
177
+ Add New Mapping
178
+ </button>
179
+ </div>
180
+ {/if}
181
+ {/if}
182
+ </div>