lula2 0.0.5 → 0.0.7-nightly.0

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.PJPcSyra.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/But0ls6Y.js +66 -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/DA_jjHdv.js +1 -0
  13. package/dist/_app/immutable/chunks/DUGOK95H.js +3 -0
  14. package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
  15. package/dist/_app/immutable/entry/app.BZauz5gw.js +2 -0
  16. package/dist/_app/immutable/entry/start._Y6yyYNP.js +1 -0
  17. package/dist/_app/immutable/nodes/0.D11TBcbi.js +1 -0
  18. package/dist/_app/immutable/nodes/1.n9wWXRXV.js +1 -0
  19. package/dist/_app/immutable/nodes/2.BlDlLeA4.js +1 -0
  20. package/dist/_app/immutable/nodes/3.B4RCsjeI.js +1 -0
  21. package/dist/_app/immutable/nodes/4.Bt1Qhh5l.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 +14 -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 +2908 -37
  36. package/dist/lula.png +0 -0
  37. package/dist/lula2 +2 -0
  38. package/package.json +77 -30
  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,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ /**
5
+ * Tooltip positioning action for Svelte components
6
+ * Adds dynamic positioning based on viewport and element bounds
7
+ */
8
+
9
+ /**
10
+ * Applies tooltip positioning to an element with a .tooltip child
11
+ * @param node The HTML element to attach the tooltip to
12
+ * @returns Svelte action object
13
+ */
14
+ export function tooltip(node: HTMLElement) {
15
+ const tooltipEl = node.querySelector('.tooltip') as HTMLElement;
16
+
17
+ // Add data attribute to allow CSS targeting
18
+ node.setAttribute('data-tooltip-trigger', 'true');
19
+
20
+ function updatePosition() {
21
+ if (!tooltipEl) return;
22
+
23
+ // Get element positions
24
+ const rect = node.getBoundingClientRect();
25
+
26
+ // Set position variables as CSS custom properties
27
+ Object.entries({
28
+ '--group-top': `${rect.top}px`,
29
+ '--group-right': `${rect.right}px`,
30
+ '--group-left': `${rect.left}px`,
31
+ '--group-width': `${rect.width}px`,
32
+ '--group-bottom': `${rect.bottom}px`,
33
+ '--group-height': `${rect.height}px`
34
+ }).forEach(([prop, val]) => tooltipEl.style.setProperty(prop, val));
35
+
36
+ // Check if tooltip already has a position class
37
+ const hasPositionClass = /tooltip-(left|right|top|bottom)/.test(tooltipEl.className);
38
+
39
+ // Only calculate and apply position if no position class is already specified
40
+ if (!hasPositionClass) {
41
+ // Determine tooltip positioning based on available space
42
+ const spaceOnRight = window.innerWidth - rect.right;
43
+ const spaceBelow = window.innerHeight - rect.bottom;
44
+ const tooltipWidth = 288; // Approximate tooltip min-width
45
+ const tooltipHeight = 100; // Approximate tooltip height
46
+
47
+ // Default horizontal position (right or left)
48
+ const horizontalPosition = spaceOnRight < tooltipWidth + 20 ? 'left' : 'right';
49
+
50
+ // Default vertical position (bottom or top)
51
+ const verticalPosition = spaceBelow < tooltipHeight + 20 ? 'top' : 'bottom';
52
+
53
+ // Determine if we should prioritize vertical positioning
54
+ const useVerticalPosition =
55
+ (horizontalPosition === 'left' && rect.left < tooltipWidth) ||
56
+ (spaceBelow < tooltipHeight && rect.top > tooltipHeight) ||
57
+ (rect.top < tooltipHeight && spaceBelow > tooltipHeight);
58
+
59
+ // Add appropriate positioning class
60
+ const position = useVerticalPosition
61
+ ? `tooltip-${verticalPosition}`
62
+ : `tooltip-${horizontalPosition}`;
63
+
64
+ tooltipEl.className =
65
+ tooltipEl.className.replace(/tooltip-(left|right|top|bottom)/g, '') + ' ' + position;
66
+ }
67
+ }
68
+
69
+ // Create a real function reference for event listener removal
70
+ const handleMouseEnter = () => window.requestAnimationFrame(updatePosition);
71
+
72
+ // Only update position on hover
73
+ node.addEventListener('mouseenter', handleMouseEnter);
74
+
75
+ return {
76
+ update: updatePosition,
77
+ destroy() {
78
+ node.removeEventListener('mouseenter', handleMouseEnter);
79
+ node.removeAttribute('data-tooltip-trigger');
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,20 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { appState } from '$lib/websocket';
6
+ import { ArrowsHorizontal } from 'carbon-icons-svelte';
7
+ </script>
8
+
9
+ {#if $appState.isConnected}
10
+ <a
11
+ href="/setup"
12
+ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
13
+ title="Switch control set"
14
+ >
15
+ <ArrowsHorizontal size={16} />
16
+ Switch
17
+ </a>
18
+ {:else}
19
+ <div class="text-sm text-gray-500 dark:text-gray-400">Loading...</div>
20
+ {/if}
@@ -0,0 +1,46 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { createEventDispatcher } from 'svelte';
6
+ import type { ControlSet } from '$lib/types';
7
+
8
+ interface Props {
9
+ currentSet: ControlSet;
10
+ availableSets: ControlSet[];
11
+ }
12
+
13
+ let { currentSet, availableSets }: Props = $props();
14
+
15
+ const dispatch = createEventDispatcher<{ change: string }>();
16
+
17
+ function handleChange(event: Event) {
18
+ const target = event.target as HTMLSelectElement;
19
+ dispatch('change', target.value);
20
+ }
21
+ </script>
22
+
23
+ <div class="control-set-selector">
24
+ <label for="control-set" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
25
+ Control Set
26
+ </label>
27
+ <select
28
+ id="control-set"
29
+ value={currentSet.id}
30
+ onchange={handleChange}
31
+ class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
32
+ >
33
+ {#each availableSets as set}
34
+ <option value={set.id}>{set.name} {set.version}</option>
35
+ {/each}
36
+ </select>
37
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
38
+ {currentSet.description}
39
+ </p>
40
+ </div>
41
+
42
+ <style>
43
+ .control-set-selector {
44
+ margin-bottom: 1rem;
45
+ }
46
+ </style>
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as ControlSetSelector } from './ControlSetSelector.svelte';
5
+ export { default as ControlSetInfo } from './ControlSetInfo.svelte';
@@ -0,0 +1,235 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { Control } from '$lib/types';
6
+ import { appState, wsClient } from '$lib/websocket';
7
+ import {
8
+ CheckmarkFilled,
9
+ Connect,
10
+ Edit,
11
+ Information,
12
+ InProgress,
13
+ Time,
14
+ WarningFilled
15
+ } from 'carbon-icons-svelte';
16
+ import { TabNavigation } from '../ui';
17
+ import {
18
+ CustomFieldsTab,
19
+ ImplementationTab,
20
+ MappingsTab,
21
+ OverviewTab,
22
+ TimelineTab
23
+ } from './tabs';
24
+
25
+ interface Props {
26
+ control: Control;
27
+ }
28
+
29
+ let { control }: Props = $props();
30
+
31
+ // Component state
32
+ let editedControl = $state({ ...control });
33
+ let originalControl = $state({ ...control });
34
+ let activeTab = $state<'details' | 'narrative' | 'custom' | 'mappings' | 'history'>('details');
35
+ let saveDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
36
+ let isSaving = $state(false);
37
+ let showSavedMessage = $state(false);
38
+ let savedMessageTimeout: ReturnType<typeof setTimeout> | null = null;
39
+
40
+ // Derived values
41
+ const fieldSchema = $derived($appState.fieldSchema.fields);
42
+ const hasChanges = $derived(JSON.stringify(editedControl) !== JSON.stringify(originalControl));
43
+ const associatedMappings = $derived(
44
+ $appState.mappings.filter((m) => m.control_id === control.id)
45
+ );
46
+ const saveStatus = $derived(
47
+ isSaving ? 'saving' : (hasChanges ? 'unsaved' : (showSavedMessage ? 'just-saved' : 'clean'))
48
+ );
49
+
50
+ // Check if tabs have any fields
51
+ const hasCustomFields = $derived(() => {
52
+ if (!fieldSchema) return false;
53
+ return Object.values(fieldSchema).some((field: any) => field.tab === 'custom');
54
+ });
55
+
56
+ const hasImplementationFields = $derived(() => {
57
+ if (!fieldSchema) return false;
58
+ return Object.values(fieldSchema).some((field: any) => field.tab === 'implementation');
59
+ });
60
+
61
+ // Watch for control changes - only reset when ID changes
62
+ $effect(() => {
63
+ if (control.id !== editedControl?.id) {
64
+ if (saveDebounceTimeout) {
65
+ clearTimeout(saveDebounceTimeout);
66
+ }
67
+ editedControl = { ...control };
68
+ originalControl = { ...control };
69
+ activeTab = 'details';
70
+ }
71
+ });
72
+
73
+ // Manual trigger for auto-save when field changes
74
+ function triggerAutoSave() {
75
+ if (!hasChanges) return;
76
+
77
+ // Clear any existing save timeout
78
+ if (saveDebounceTimeout) {
79
+ clearTimeout(saveDebounceTimeout);
80
+ }
81
+
82
+ // Debounce the save by 500ms to avoid too many saves while typing
83
+ saveDebounceTimeout = setTimeout(() => {
84
+ performSave();
85
+ }, 500);
86
+ }
87
+
88
+ // Perform the actual save
89
+ async function performSave() {
90
+ if (!hasChanges || isSaving) return;
91
+
92
+ isSaving = true;
93
+ try {
94
+ // Only send the fields that have actually changed
95
+ const changes: Record<string, any> = { id: editedControl.id };
96
+
97
+ // Compare each field and only include changed ones
98
+ for (const [key, value] of Object.entries(editedControl)) {
99
+ // Skip runtime fields
100
+ if (key === 'timeline' || key === 'unifiedHistory' || key === '_metadata') {
101
+ continue;
102
+ }
103
+
104
+ // Only include if value has changed
105
+ if (JSON.stringify(value) !== JSON.stringify(originalControl[key])) {
106
+ changes[key] = value;
107
+ }
108
+ }
109
+
110
+ // Only send if there are actual changes beyond the ID
111
+ if (Object.keys(changes).length > 1) {
112
+ await wsClient.updateControl(changes as Control);
113
+ originalControl = { ...editedControl };
114
+ showTemporarySavedMessage();
115
+ console.log('Saved changes:', Object.keys(changes).filter(k => k !== 'id').join(', '));
116
+ }
117
+ } catch (error) {
118
+ console.error('Save failed:', error);
119
+ } finally {
120
+ isSaving = false;
121
+ }
122
+ }
123
+
124
+ // Show the saved message temporarily
125
+ function showTemporarySavedMessage() {
126
+ // Clear any existing timeout
127
+ if (savedMessageTimeout) {
128
+ clearTimeout(savedMessageTimeout);
129
+ }
130
+
131
+ // Show the message
132
+ showSavedMessage = true;
133
+
134
+ // Hide it after 3 seconds
135
+ savedMessageTimeout = setTimeout(() => {
136
+ showSavedMessage = false;
137
+ savedMessageTimeout = null;
138
+ }, 3000);
139
+ }
140
+
141
+ // Handle field changes from custom tab
142
+ function handleFieldChange(fieldName: string, value: any) {
143
+ triggerAutoSave();
144
+ }
145
+ </script>
146
+
147
+ <!-- Header outside of any card -->
148
+ <header class="flex-shrink-0">
149
+ <div class="py-5">
150
+ <div class="flex items-center justify-between">
151
+ <div class="flex items-center space-x-4">
152
+ <div>
153
+ <h1 class="text-xl font-bold text-gray-900 dark:text-white tracking-tight">
154
+ {control.id}
155
+ </h1>
156
+ <p class="text-sm text-gray-500 dark:text-gray-400 font-medium mt-1">
157
+ {control.title}
158
+ </p>
159
+ </div>
160
+ </div>
161
+ <div class="flex items-center">
162
+ <!-- Save status icon indicator -->
163
+ {#if saveStatus === 'saving'}
164
+ <div
165
+ class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30"
166
+ title="Saving..."
167
+ >
168
+ <InProgress class="w-5 h-5 text-blue-600 dark:text-blue-400" />
169
+ </div>
170
+ {:else if saveStatus === 'unsaved'}
171
+ <div
172
+ class="w-8 h-8 flex items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30"
173
+ title="Unsaved changes"
174
+ >
175
+ <WarningFilled class="w-5 h-5 text-amber-600 dark:text-amber-400" />
176
+ </div>
177
+ {:else if saveStatus === 'just-saved'}
178
+ <div
179
+ class="w-8 h-8 flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30 animate-fade-in"
180
+ title="Saved"
181
+ >
182
+ <CheckmarkFilled class="w-5 h-5 text-green-600 dark:text-green-400" />
183
+ </div>
184
+ {/if}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </header>
189
+
190
+ <!-- Tab Navigation outside of any card -->
191
+ <div class="mb-2">
192
+ <TabNavigation
193
+ active={activeTab}
194
+ tabs={[
195
+ { id: 'details', label: 'Overview', icon: Information },
196
+ ...(hasImplementationFields() ? [{ id: 'narrative', label: 'Implementation', icon: Edit }] : []),
197
+ ...(hasCustomFields() ? [{ id: 'custom', label: 'Custom', icon: Edit }] : []),
198
+ { id: 'mappings', label: 'Mappings', icon: Connect, count: associatedMappings.length },
199
+ {
200
+ id: 'history',
201
+ label: 'Timeline',
202
+ icon: Time,
203
+ count: control.timeline?.totalCommits
204
+ }
205
+ ]}
206
+ onSelect={(tabId) => (activeTab = tabId as typeof activeTab)}
207
+ />
208
+ </div>
209
+
210
+ <!-- Tab content without card wrapper -->
211
+ <main class="flex-1 overflow-auto pt-4">
212
+ <div class="">
213
+ {#if activeTab === 'details'}
214
+ <OverviewTab control={editedControl} {fieldSchema} />
215
+ {:else if activeTab === 'narrative'}
216
+ <ImplementationTab control={editedControl} {fieldSchema} />
217
+ {:else if activeTab === 'custom'}
218
+ <CustomFieldsTab
219
+ control={editedControl}
220
+ {fieldSchema}
221
+ onFieldChange={handleFieldChange}
222
+ />
223
+ {:else if activeTab === 'mappings'}
224
+ <MappingsTab
225
+ {control}
226
+ mappings={associatedMappings}
227
+ />
228
+ {:else if activeTab === 'history'}
229
+ <TimelineTab
230
+ {control}
231
+ timeline={control.timeline}
232
+ />
233
+ {/if}
234
+ </div>
235
+ </main>