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,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as SpreadsheetImport } from './SpreadsheetImport.svelte';
5
+ export { default as ExistingControlSets } from './ExistingControlSets.svelte';
@@ -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';