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.
- package/README.md +291 -8
- package/dist/_app/env.js +1 -0
- package/dist/_app/immutable/assets/0.DtiRW3lO.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/Bg_R1qWi.js +3 -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/DsnmJJEf.js +1 -0
- package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
- package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
- package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
- package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
- package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
- package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
- package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
- package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
- package/dist/_app/immutable/nodes/4.Blq-4WQS.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 +30 -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 +2924 -37
- package/dist/lula.png +0 -0
- package/dist/lula2 +2 -0
- package/package.json +120 -72
- 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,107 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
7
|
+
import { fade } from 'svelte/transition';
|
|
8
|
+
|
|
9
|
+
import { ChevronDown } from 'carbon-icons-svelte';
|
|
10
|
+
import { twMerge } from 'tailwind-merge';
|
|
11
|
+
|
|
12
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
13
|
+
buttonLabel?: string;
|
|
14
|
+
buttonLabelClass?: string;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
buttonIcon?: any;
|
|
17
|
+
buttonClass?: string;
|
|
18
|
+
dropdownClass?: string;
|
|
19
|
+
iconClass?: string;
|
|
20
|
+
expandIconClass?: string;
|
|
21
|
+
isOpen?: boolean;
|
|
22
|
+
buttonContents?: Snippet;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
children?: any;
|
|
25
|
+
class?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
buttonLabel = '',
|
|
30
|
+
buttonLabelClass = 'min-w-0 truncate',
|
|
31
|
+
buttonIcon = undefined,
|
|
32
|
+
buttonClass = '',
|
|
33
|
+
dropdownClass = '',
|
|
34
|
+
iconClass = '',
|
|
35
|
+
expandIconClass = '',
|
|
36
|
+
isOpen = $bindable(false),
|
|
37
|
+
children,
|
|
38
|
+
buttonContents,
|
|
39
|
+
class: propsClass = '',
|
|
40
|
+
...restProps
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
|
|
43
|
+
// Toggle dropdown open/closed
|
|
44
|
+
function toggleDropdown() {
|
|
45
|
+
isOpen = !isOpen;
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
class={twMerge(
|
|
51
|
+
'relative min-w-0 flex-shrink-0 text-left',
|
|
52
|
+
dropdownClass?.includes('inline-block') ? '' : 'block',
|
|
53
|
+
propsClass
|
|
54
|
+
)}
|
|
55
|
+
{...restProps}
|
|
56
|
+
>
|
|
57
|
+
<button
|
|
58
|
+
onclick={toggleDropdown}
|
|
59
|
+
class={twMerge(
|
|
60
|
+
'btn relative flex w-full items-center justify-between rounded-lg bg-gray-800 px-2 py-1 text-white hover:bg-gray-700 focus:bg-gray-700 md:px-4 md:py-2',
|
|
61
|
+
buttonClass
|
|
62
|
+
)}
|
|
63
|
+
data-testid="dropdown-button"
|
|
64
|
+
aria-label={buttonLabel}
|
|
65
|
+
>
|
|
66
|
+
{#if buttonContents}
|
|
67
|
+
{@render buttonContents()}
|
|
68
|
+
{:else}
|
|
69
|
+
<div class="flex min-w-0 items-center">
|
|
70
|
+
{#if buttonIcon}
|
|
71
|
+
{@const SvelteComponent = buttonIcon}
|
|
72
|
+
<div class={twMerge('mr-2 flex h-5 w-5 items-center justify-center', iconClass)}>
|
|
73
|
+
<SvelteComponent />
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
<span class={buttonLabelClass}>{buttonLabel}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<ChevronDown class={twMerge('ml-2 h-[12px] w-[12px]', expandIconClass)} />
|
|
79
|
+
{/if}
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
{#if isOpen}
|
|
83
|
+
<!-- full-screen invisible button mask - closes when clicking off dropdown without clicking underlying element -->
|
|
84
|
+
<button
|
|
85
|
+
class="fixed inset-0 z-40 m-0 border-none bg-transparent p-0"
|
|
86
|
+
aria-label="Close dropdown"
|
|
87
|
+
onclick={(e) => {
|
|
88
|
+
isOpen = false;
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
}}
|
|
91
|
+
onkeydown={(e) => {
|
|
92
|
+
// close on Enter, Space or Escape
|
|
93
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape') {
|
|
94
|
+
isOpen = false;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
></button>
|
|
100
|
+
<div
|
|
101
|
+
transition:fade={{ duration: 100 }}
|
|
102
|
+
class={`absolute right-0 z-100 mt-2 min-w-60 origin-top-right rounded-lg border border-gray-700 bg-gray-800 p-2 shadow-lg focus:outline-none ${dropdownClass}`}
|
|
103
|
+
>
|
|
104
|
+
{@render children?.()}
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
icon?: Snippet;
|
|
11
|
+
action?: Snippet;
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { title, description, icon, action, size = 'md' }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const getSizeClasses = (size: string) => {
|
|
18
|
+
switch (size) {
|
|
19
|
+
case 'sm':
|
|
20
|
+
return {
|
|
21
|
+
container: 'py-6',
|
|
22
|
+
icon: 'h-8 w-8',
|
|
23
|
+
title: 'text-sm',
|
|
24
|
+
description: 'text-xs'
|
|
25
|
+
};
|
|
26
|
+
case 'lg':
|
|
27
|
+
return {
|
|
28
|
+
container: 'py-16',
|
|
29
|
+
icon: 'h-16 w-16',
|
|
30
|
+
title: 'text-lg',
|
|
31
|
+
description: 'text-base'
|
|
32
|
+
};
|
|
33
|
+
default: // md
|
|
34
|
+
return {
|
|
35
|
+
container: 'py-12',
|
|
36
|
+
icon: 'h-12 w-12',
|
|
37
|
+
title: 'text-base',
|
|
38
|
+
description: 'text-sm'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const classes = getSizeClasses(size);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="text-center {classes.container}">
|
|
47
|
+
{#if icon}
|
|
48
|
+
<div class="mx-auto {classes.icon} mb-4 text-gray-400 dark:text-gray-500">
|
|
49
|
+
{@render icon()}
|
|
50
|
+
</div>
|
|
51
|
+
{:else}
|
|
52
|
+
<!-- Default icon -->
|
|
53
|
+
<svg
|
|
54
|
+
class="mx-auto {classes.icon} mb-4 text-gray-400 dark:text-gray-500"
|
|
55
|
+
fill="none"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
>
|
|
59
|
+
<path
|
|
60
|
+
stroke-linecap="round"
|
|
61
|
+
stroke-linejoin="round"
|
|
62
|
+
stroke-width="2"
|
|
63
|
+
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<h3 class="font-medium text-gray-900 dark:text-white {classes.title}">
|
|
69
|
+
{title}
|
|
70
|
+
</h3>
|
|
71
|
+
<p class="mt-2 text-gray-500 dark:text-gray-400 {classes.description}">
|
|
72
|
+
{description}
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
{#if action}
|
|
76
|
+
<div class="mt-6">
|
|
77
|
+
{@render action()}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
interface Props {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
onToggle: (enabled: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { enabled = $bindable(), label, description, onToggle }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function handleToggle() {
|
|
15
|
+
enabled = !enabled;
|
|
16
|
+
onToggle(enabled);
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="flex items-center justify-between py-3">
|
|
21
|
+
<div class="flex-1">
|
|
22
|
+
<div class="flex items-center">
|
|
23
|
+
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
|
|
24
|
+
{label}
|
|
25
|
+
</h3>
|
|
26
|
+
</div>
|
|
27
|
+
{#if description}
|
|
28
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
29
|
+
{description}
|
|
30
|
+
</p>
|
|
31
|
+
{/if}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onclick={handleToggle}
|
|
37
|
+
class="{enabled
|
|
38
|
+
? 'bg-blue-600'
|
|
39
|
+
: 'bg-gray-200 dark:bg-gray-800'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
|
40
|
+
role="switch"
|
|
41
|
+
aria-checked={enabled}
|
|
42
|
+
>
|
|
43
|
+
<span class="sr-only">{label}</span>
|
|
44
|
+
<span
|
|
45
|
+
class="{enabled
|
|
46
|
+
? 'translate-x-5'
|
|
47
|
+
: 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
|
48
|
+
></span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { complianceStore } from '$stores/compliance';
|
|
6
|
+
|
|
7
|
+
let searchInput = $state('');
|
|
8
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
9
|
+
|
|
10
|
+
function debouncedSearch(term: string) {
|
|
11
|
+
clearTimeout(debounceTimer);
|
|
12
|
+
debounceTimer = setTimeout(() => {
|
|
13
|
+
complianceStore.setSearchTerm(term);
|
|
14
|
+
}, 300);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Watch for changes to searchInput and trigger debounced search
|
|
18
|
+
$effect(() => {
|
|
19
|
+
debouncedSearch(searchInput);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function clearSearch() {
|
|
23
|
+
searchInput = '';
|
|
24
|
+
complianceStore.setSearchTerm('');
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div class="relative">
|
|
29
|
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
30
|
+
<svg
|
|
31
|
+
class="w-5 h-5 text-gray-400 dark:text-gray-500"
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
stroke-linecap="round"
|
|
38
|
+
stroke-linejoin="round"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
41
|
+
></path>
|
|
42
|
+
</svg>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<input
|
|
46
|
+
bind:value={searchInput}
|
|
47
|
+
type="text"
|
|
48
|
+
placeholder="Search controls and mappings..."
|
|
49
|
+
class="block w-full pl-10 pr-12 py-2 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
{#if searchInput}
|
|
53
|
+
<button
|
|
54
|
+
onclick={clearSearch}
|
|
55
|
+
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
56
|
+
aria-label="Clear search"
|
|
57
|
+
>
|
|
58
|
+
<svg
|
|
59
|
+
class="w-5 h-5 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
>
|
|
64
|
+
<path
|
|
65
|
+
stroke-linecap="round"
|
|
66
|
+
stroke-linejoin="round"
|
|
67
|
+
stroke-width="2"
|
|
68
|
+
d="M6 18L18 6M6 6l12 12"
|
|
69
|
+
></path>
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
interface Props {
|
|
6
|
+
status: string;
|
|
7
|
+
type?: 'control' | 'mapping' | 'compliance';
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { status, type = 'control', size = 'sm' }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const getStatusColor = (status: string, type: string) => {
|
|
14
|
+
if (!status) {
|
|
15
|
+
return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (type === 'mapping') {
|
|
19
|
+
switch (status.toLowerCase()) {
|
|
20
|
+
case 'implemented':
|
|
21
|
+
return 'bg-green-500 text-white';
|
|
22
|
+
case 'verified':
|
|
23
|
+
return 'bg-blue-500 text-white';
|
|
24
|
+
case 'planned':
|
|
25
|
+
return 'bg-yellow-500 text-white';
|
|
26
|
+
default:
|
|
27
|
+
return 'bg-gray-600 text-white';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (type === 'compliance') {
|
|
32
|
+
switch (status.toLowerCase()) {
|
|
33
|
+
case 'compliant':
|
|
34
|
+
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
|
35
|
+
case 'non-compliant':
|
|
36
|
+
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
|
37
|
+
case 'not assessed':
|
|
38
|
+
return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
|
39
|
+
default:
|
|
40
|
+
return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Default control status colors
|
|
45
|
+
switch (status.toLowerCase()) {
|
|
46
|
+
case 'implemented':
|
|
47
|
+
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
|
48
|
+
case 'planned':
|
|
49
|
+
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
|
50
|
+
case 'not implemented':
|
|
51
|
+
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
|
52
|
+
default:
|
|
53
|
+
return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getSizeClass = (size: string) => {
|
|
58
|
+
switch (size) {
|
|
59
|
+
case 'sm':
|
|
60
|
+
return 'px-2.5 py-0.5 text-xs';
|
|
61
|
+
case 'md':
|
|
62
|
+
return 'px-3 py-1 text-sm';
|
|
63
|
+
case 'lg':
|
|
64
|
+
return 'px-4 py-2 text-base';
|
|
65
|
+
default:
|
|
66
|
+
return 'px-2.5 py-0.5 text-xs';
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<span
|
|
72
|
+
class="inline-flex items-center rounded-lg font-medium shadow-sm {getStatusColor(
|
|
73
|
+
status,
|
|
74
|
+
type
|
|
75
|
+
)} {getSizeClass(size)}"
|
|
76
|
+
title={`${type} status: ${status}`}
|
|
77
|
+
>
|
|
78
|
+
{status}
|
|
79
|
+
</span>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
interface Tab {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: any;
|
|
9
|
+
count?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
tabs: Tab[];
|
|
15
|
+
active: string;
|
|
16
|
+
onSelect: (tabId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { tabs, active, onSelect }: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<nav class="border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
23
|
+
<div class="flex space-x-4">
|
|
24
|
+
{#each tabs as tab}
|
|
25
|
+
<button
|
|
26
|
+
onclick={() => !tab.disabled && onSelect(tab.id)}
|
|
27
|
+
disabled={tab.disabled}
|
|
28
|
+
class="inline-flex items-center py-4 border-b-2 font-medium text-sm transition-colors px-4 {active ===
|
|
29
|
+
tab.id
|
|
30
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
31
|
+
: tab.disabled
|
|
32
|
+
? 'border-transparent text-gray-300 dark:text-gray-600 cursor-not-allowed'
|
|
33
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
34
|
+
>
|
|
35
|
+
{#if tab.icon}
|
|
36
|
+
{@const IconComponent = tab.icon}
|
|
37
|
+
<IconComponent class="w-4 h-4 mr-2" />
|
|
38
|
+
{/if}
|
|
39
|
+
{tab.label}
|
|
40
|
+
{#if tab.count !== undefined}
|
|
41
|
+
<span class="ml-1 text-xs bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full">
|
|
42
|
+
{tab.count}
|
|
43
|
+
</span>
|
|
44
|
+
{/if}
|
|
45
|
+
</button>
|
|
46
|
+
{/each}
|
|
47
|
+
</div>
|
|
48
|
+
</nav>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { onDestroy, onMount } from 'svelte';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
content: string;
|
|
9
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
10
|
+
maxWidth?: string;
|
|
11
|
+
multiline?: boolean;
|
|
12
|
+
children: () => any;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
content,
|
|
17
|
+
placement = 'top',
|
|
18
|
+
maxWidth = '300px',
|
|
19
|
+
multiline = false,
|
|
20
|
+
children
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
let tooltip: HTMLDivElement | null = $state(null);
|
|
24
|
+
let trigger: HTMLDivElement;
|
|
25
|
+
let isVisible = $state(false);
|
|
26
|
+
let position = $state({ x: 0, y: 0 });
|
|
27
|
+
|
|
28
|
+
function calculatePosition() {
|
|
29
|
+
if (!trigger || !tooltip) return;
|
|
30
|
+
|
|
31
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
32
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
33
|
+
const spacing = 8;
|
|
34
|
+
|
|
35
|
+
let x = 0;
|
|
36
|
+
let y = 0;
|
|
37
|
+
|
|
38
|
+
switch (placement) {
|
|
39
|
+
case 'top':
|
|
40
|
+
x = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
|
41
|
+
y = triggerRect.top - tooltipRect.height - spacing;
|
|
42
|
+
break;
|
|
43
|
+
case 'bottom':
|
|
44
|
+
x = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
|
45
|
+
y = triggerRect.bottom + spacing;
|
|
46
|
+
break;
|
|
47
|
+
case 'left':
|
|
48
|
+
x = triggerRect.left - tooltipRect.width - spacing;
|
|
49
|
+
y = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
|
50
|
+
break;
|
|
51
|
+
case 'right':
|
|
52
|
+
x = triggerRect.right + spacing;
|
|
53
|
+
y = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Keep tooltip within viewport
|
|
58
|
+
const padding = 8;
|
|
59
|
+
x = Math.max(padding, Math.min(x, window.innerWidth - tooltipRect.width - padding));
|
|
60
|
+
y = Math.max(padding, Math.min(y, window.innerHeight - tooltipRect.height - padding));
|
|
61
|
+
|
|
62
|
+
position = { x, y };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function showTooltip() {
|
|
66
|
+
isVisible = true;
|
|
67
|
+
requestAnimationFrame(calculatePosition);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hideTooltip() {
|
|
71
|
+
isVisible = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onMount(() => {
|
|
75
|
+
window.addEventListener('scroll', calculatePosition, true);
|
|
76
|
+
window.addEventListener('resize', calculatePosition);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
onDestroy(() => {
|
|
80
|
+
window.removeEventListener('scroll', calculatePosition, true);
|
|
81
|
+
window.removeEventListener('resize', calculatePosition);
|
|
82
|
+
});
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div
|
|
86
|
+
bind:this={trigger}
|
|
87
|
+
class="inline-flex items-center"
|
|
88
|
+
onmouseenter={showTooltip}
|
|
89
|
+
onmouseleave={hideTooltip}
|
|
90
|
+
onfocus={showTooltip}
|
|
91
|
+
onblur={hideTooltip}
|
|
92
|
+
role="button"
|
|
93
|
+
tabindex="0"
|
|
94
|
+
>
|
|
95
|
+
{@render children?.()}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{#if isVisible}
|
|
99
|
+
<div
|
|
100
|
+
bind:this={tooltip}
|
|
101
|
+
class="fixed z-50 px-3 py-2 text-sm text-white bg-gray-900 dark:bg-gray-700 rounded-md shadow-lg pointer-events-none transition-opacity duration-200 {multiline
|
|
102
|
+
? 'whitespace-pre-wrap'
|
|
103
|
+
: 'whitespace-nowrap'}"
|
|
104
|
+
style="left: {position.x}px; top: {position.y}px; max-width: {maxWidth};"
|
|
105
|
+
role="tooltip"
|
|
106
|
+
>
|
|
107
|
+
{content}
|
|
108
|
+
<div
|
|
109
|
+
class="absolute w-2 h-2 bg-gray-900 dark:bg-gray-700 transform rotate-45"
|
|
110
|
+
class:bottom-[-4px]={placement === 'top'}
|
|
111
|
+
class:left-[50%]={placement === 'top' || placement === 'bottom'}
|
|
112
|
+
class:translate-x-[-50%]={placement === 'top' || placement === 'bottom'}
|
|
113
|
+
class:top-[-4px]={placement === 'bottom'}
|
|
114
|
+
class:right-[-4px]={placement === 'left'}
|
|
115
|
+
class:top-[50%]={placement === 'left' || placement === 'right'}
|
|
116
|
+
class:translate-y-[-50%]={placement === 'left' || placement === 'right'}
|
|
117
|
+
class:left-[-4px]={placement === 'right'}
|
|
118
|
+
></div>
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export { default as Dropdown } from './Dropdown.svelte';
|
|
5
|
+
export { default as EmptyState } from './EmptyState.svelte';
|
|
6
|
+
export { default as FeatureToggle } from './FeatureToggle.svelte';
|
|
7
|
+
export { default as SearchBar } from './SearchBar.svelte';
|
|
8
|
+
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
9
|
+
export { default as TabNavigation } from './TabNavigation.svelte';
|
|
10
|
+
export { default as Tooltip } from './Tooltip.svelte';
|