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.
- package/README.md +291 -8
- package/dist/_app/env.js +1 -0
- package/dist/_app/immutable/assets/0.PJPcSyra.css +1 -0
- package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
- package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
- package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
- package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
- package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
- package/dist/_app/immutable/chunks/But0ls6Y.js +66 -0
- package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
- package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
- package/dist/_app/immutable/chunks/DA_jjHdv.js +1 -0
- package/dist/_app/immutable/chunks/DUGOK95H.js +3 -0
- package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
- package/dist/_app/immutable/entry/app.BZauz5gw.js +2 -0
- package/dist/_app/immutable/entry/start._Y6yyYNP.js +1 -0
- package/dist/_app/immutable/nodes/0.D11TBcbi.js +1 -0
- package/dist/_app/immutable/nodes/1.n9wWXRXV.js +1 -0
- package/dist/_app/immutable/nodes/2.BlDlLeA4.js +1 -0
- package/dist/_app/immutable/nodes/3.B4RCsjeI.js +1 -0
- package/dist/_app/immutable/nodes/4.Bt1Qhh5l.js +9 -0
- package/dist/_app/version.json +1 -0
- package/dist/cli/commands/crawl.js +128 -0
- package/dist/cli/commands/ui.js +2769 -0
- package/dist/cli/commands/version.js +14 -0
- package/dist/cli/server/index.js +2713 -0
- package/dist/cli/server/server.js +2702 -0
- package/dist/cli/server/serverState.js +1199 -0
- package/dist/cli/server/spreadsheetRoutes.js +788 -0
- package/dist/cli/server/types.js +0 -0
- package/dist/cli/server/websocketServer.js +2625 -0
- package/dist/cli/utils/debug.js +24 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +38 -0
- package/dist/index.js +2908 -37
- package/dist/lula.png +0 -0
- package/dist/lula2 +2 -0
- package/package.json +77 -30
- package/src/app.css +192 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +13 -0
- package/src/lib/actions/fadeWhenScrollable.ts +39 -0
- package/src/lib/actions/modal.ts +230 -0
- package/src/lib/actions/tooltip.ts +82 -0
- package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
- package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
- package/src/lib/components/control-sets/index.ts +5 -0
- package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
- package/src/lib/components/controls/ControlsList.svelte +608 -0
- package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
- package/src/lib/components/controls/MappingCard.svelte +105 -0
- package/src/lib/components/controls/MappingForm.svelte +188 -0
- package/src/lib/components/controls/index.ts +9 -0
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
- package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
- package/src/lib/components/controls/renderers/index.ts +5 -0
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
- package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
- package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
- package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
- package/src/lib/components/controls/tabs/index.ts +8 -0
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
- package/src/lib/components/controls/utils/textProcessor.ts +164 -0
- package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
- package/src/lib/components/forms/DynamicField.svelte +494 -0
- package/src/lib/components/forms/FormField.svelte +107 -0
- package/src/lib/components/forms/index.ts +6 -0
- package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
- package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
- package/src/lib/components/setup/index.ts +5 -0
- package/src/lib/components/ui/Dropdown.svelte +107 -0
- package/src/lib/components/ui/EmptyState.svelte +80 -0
- package/src/lib/components/ui/FeatureToggle.svelte +50 -0
- package/src/lib/components/ui/SearchBar.svelte +73 -0
- package/src/lib/components/ui/StatusBadge.svelte +79 -0
- package/src/lib/components/ui/TabNavigation.svelte +48 -0
- package/src/lib/components/ui/Tooltip.svelte +120 -0
- package/src/lib/components/ui/index.ts +10 -0
- package/src/lib/components/version-control/DiffViewer.svelte +292 -0
- package/src/lib/components/version-control/TimelineItem.svelte +107 -0
- package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
- package/src/lib/components/version-control/index.ts +6 -0
- package/src/lib/form-types.ts +57 -0
- package/src/lib/formatUtils.ts +17 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/types.ts +180 -0
- package/src/lib/websocket.ts +359 -0
- package/src/routes/+layout.svelte +236 -0
- package/src/routes/+page.svelte +38 -0
- package/src/routes/control/[id]/+page.svelte +112 -0
- package/src/routes/setup/+page.svelte +241 -0
- package/src/stores/compliance.ts +95 -0
- package/src/styles/highlightjs.css +20 -0
- package/src/styles/modal.css +58 -0
- package/src/styles/tables.css +111 -0
- package/src/styles/tooltip.css +65 -0
- package/dist/controls/index.d.ts +0 -18
- package/dist/controls/index.d.ts.map +0 -1
- package/dist/controls/index.js +0 -18
- package/dist/crawl.d.ts +0 -62
- package/dist/crawl.d.ts.map +0 -1
- package/dist/crawl.js +0 -172
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/src/controls/index.ts +0 -19
- package/src/crawl.ts +0 -227
- 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,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>
|