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
package/dist/lula.png ADDED
Binary file
package/dist/lula2 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './index.js';
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "lula2",
3
- "version": "0.0.5",
3
+ "version": "0.0.7-nightly.0",
4
4
  "description": "A tool for managing compliance as code in your GitHub repositories.",
5
- "bin": "./dist/index.js",
5
+ "bin": {
6
+ "lula2": "./dist/lula2"
7
+ },
6
8
  "main": "dist/index.js",
7
9
  "types": "dist/index.d.ts",
8
10
  "type": "module",
@@ -11,7 +13,7 @@
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
14
- "url": "git+https://github.com/defenseunicorns/lula.git"
16
+ "url": "git+https://github.com/defenseunicorns/lula-next.git"
15
17
  },
16
18
  "keywords": [
17
19
  "compliance",
@@ -21,9 +23,9 @@
21
23
  "author": "Defense Unicorns",
22
24
  "license": "Apache-2.0",
23
25
  "bugs": {
24
- "url": "https://github.com/defenseunicorns/lula/issues"
26
+ "url": "https://github.com/defenseunicorns/lula-next/issues"
25
27
  },
26
- "homepage": "https://github.com/defenseunicorns/lula#readme",
28
+ "homepage": "https://github.com/defenseunicorns/lula-next#readme",
27
29
  "files": [
28
30
  "/src",
29
31
  "/dist",
@@ -31,44 +33,89 @@
31
33
  "!dist/**/*.test.js*",
32
34
  "!dist/**/*.test.d.ts*"
33
35
  ],
34
- "scripts": {
35
- "prebuild": "rm -rf dist",
36
- "build": "tsc",
37
- "semantic-release": "semantic-release",
38
- "format:check": "eslint src && prettier . --check",
39
- "format:fix": "eslint --fix src && prettier . --write",
40
- "prepare": "if [ \"$NODE_ENV\" != 'production' ]; then husky; fi",
41
- "test": "vitest run --coverage"
42
- },
43
36
  "dependencies": {
44
37
  "@octokit/rest": "^22.0.0",
38
+ "@types/ws": "^8.18.1",
45
39
  "commander": "^14.0.0",
46
- "compliance-reporter": "^0.1.2",
47
- "undici": "^7.13.0"
40
+ "cors": "^2.8.5",
41
+ "csv-parse": "^6.1.0",
42
+ "exceljs": "^4.4.0",
43
+ "express": "^4.21.2",
44
+ "flowbite": "^2.5.2",
45
+ "glob": "^10.4.5",
46
+ "isomorphic-git": "^1.33.1",
47
+ "js-yaml": "^4.1.0",
48
+ "multer": "^2.0.2",
49
+ "open": "^9.1.0",
50
+ "undici": "^7.15.0",
51
+ "ws": "^8.18.3",
52
+ "yaml": "^2.8.1"
48
53
  },
49
54
  "devDependencies": {
50
- "@commitlint/cli": "^19.8.0",
51
- "@commitlint/config-conventional": "^19.8.0",
52
- "@eslint/js": "^9.23.0",
53
- "@typescript-eslint/eslint-plugin": "^8.41.0",
54
- "@typescript-eslint/parser": "^8.41.0",
55
+ "@commitlint/cli": "^19.8.1",
56
+ "@commitlint/config-conventional": "^19.8.1",
57
+ "@eslint/compat": "^1.3.2",
58
+ "@eslint/js": "^9.35.0",
59
+ "@sveltejs/adapter-static": "^3.0.9",
60
+ "@sveltejs/kit": "^2.37.1",
61
+ "@sveltejs/vite-plugin-svelte": "^6.1.4",
62
+ "@tailwindcss/vite": "^4.1.13",
63
+ "@types/cors": "^2.8.19",
64
+ "@types/express": "^4.17.23",
65
+ "@types/js-yaml": "^4.0.9",
66
+ "@types/multer": "^2.0.0",
67
+ "@typescript-eslint/eslint-plugin": "^8.42.0",
68
+ "@typescript-eslint/parser": "^8.42.0",
69
+ "@vitest/browser": "^3.2.4",
55
70
  "@vitest/coverage-v8": "^3.2.4",
56
- "esbuild": "^0.25.1",
57
- "eslint": "^9.26.0",
58
- "eslint-config-prettier": "^10.0.2",
59
- "eslint-plugin-jsdoc": "^54.1.1",
60
- "globals": "^16.0.0",
71
+ "carbon-icons-svelte": "^13.5.0",
72
+ "carbon-preprocess-svelte": "^0.11.11",
73
+ "concurrently": "^9.2.1",
74
+ "esbuild": "^0.25.9",
75
+ "eslint": "^9.35.0",
76
+ "eslint-config-prettier": "^10.1.8",
77
+ "eslint-plugin-jsdoc": "^54.5.0",
78
+ "eslint-plugin-svelte": "^3.12.2",
79
+ "globals": "^16.3.0",
61
80
  "husky": "^9.1.7",
81
+ "playwright": "^1.55.0",
62
82
  "prettier": "3.6.2",
63
- "semantic-release": "^24.2.3",
83
+ "prettier-plugin-svelte": "^3.4.0",
84
+ "semantic-release": "^24.2.7",
85
+ "shellcheck": "^4.1.0",
86
+ "svelte": "^5.38.7",
87
+ "svelte-check": "^4.3.1",
88
+ "tailwind-merge": "^3.3.1",
89
+ "tailwindcss": "^4.1.13",
90
+ "tsx": "^4.20.5",
64
91
  "typescript": "5.9.2",
65
- "typescript-eslint": "^8.41.0",
66
- "vitest": "^3.2.4"
92
+ "typescript-eslint": "^8.42.0",
93
+ "vite": "^7.1.4",
94
+ "vitest": "^3.2.4",
95
+ "vitest-browser-svelte": "^0.1.0"
67
96
  },
68
97
  "release": {
69
98
  "branches": [
70
99
  "main",
71
100
  "next"
72
101
  ]
102
+ },
103
+ "scripts": {
104
+ "dev": "vite dev --port 5173",
105
+ "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
106
+ "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
107
+ "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
108
+ "build:svelte": "vite build",
109
+ "build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
110
+ "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
111
+ "preview": "vite preview",
112
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
113
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
114
+ "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts'",
115
+ "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts'",
116
+ "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' && eslint src cli",
117
+ "test": "npm run test:unit -- --run --coverage",
118
+ "test:integration": "vitest --config integration/vitest.config.integration.ts",
119
+ "test:unit": "vitest"
73
120
  }
74
- }
121
+ }
package/src/app.css ADDED
@@ -0,0 +1,192 @@
1
+ /*
2
+ Copyright 2025 Defense Unicorns
3
+ SPDX-License-Identifier: LicenseRef-Defense-Unicorns-Commercial
4
+ */
5
+
6
+ @import 'tailwindcss';
7
+ @plugin 'flowbite/plugin';
8
+
9
+ :root {
10
+ --navbar-height: 4rem;
11
+ --sidebar-width: 16rem;
12
+ --table-height: 350px;
13
+ }
14
+
15
+ @theme {
16
+ --breakpoint-xs: 31rem;
17
+ }
18
+
19
+ @layer base {
20
+ html {
21
+ @apply overflow-y-auto;
22
+
23
+ color-scheme: dark; /* Dark scrollbar color for Safari, the webkit overrides do not work */
24
+ /* Standard scrollbar properties (Firefox) */
25
+ scrollbar-width: thin;
26
+ scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
27
+
28
+ /* WebKit scrollbar styling (Chrome, Edge) */
29
+ &::-webkit-scrollbar {
30
+ width: 5px; /* Width that's narrow but still usable */
31
+ z-index: 20;
32
+ }
33
+
34
+ /* Scrollbar thumb styling */
35
+ &::-webkit-scrollbar-thumb {
36
+ border-radius: 4px; /* Rounded corners for better aesthetics */
37
+ background-color: rgba(156, 163, 175, 0.5); /* Semi-transparent gray */
38
+ }
39
+
40
+ /* Transparent track to focus attention on the thumb */
41
+ &::-webkit-scrollbar-track {
42
+ background: transparent;
43
+ }
44
+
45
+ button:not([disabled]),
46
+ [role='button']:not([disabled]) {
47
+ cursor: pointer;
48
+ }
49
+ }
50
+ }
51
+
52
+ @layer components {
53
+ .sticky-header {
54
+ position: sticky;
55
+ z-index: 40;
56
+ }
57
+
58
+ /* Global button styling */
59
+ .btn {
60
+ @apply inline-flex cursor-pointer items-center rounded-lg border border-gray-600 bg-transparent px-3 py-1.5 text-center text-sm font-medium text-gray-400 transition-all ease-in-out hover:bg-gray-700 hover:text-white;
61
+ }
62
+
63
+ .btn-primary {
64
+ @apply cursor-pointer rounded-lg border border-blue-600 bg-gray-900 px-5 py-2.5 text-sm font-medium text-blue-500 transition-colors hover:bg-blue-600 hover:text-white focus:outline-none focus-visible:z-10 focus-visible:bg-blue-600 focus-visible:text-white focus-visible:ring-4 focus-visible:ring-blue-200 disabled:cursor-not-allowed disabled:border-transparent disabled:bg-gray-500 disabled:text-white;
65
+ }
66
+
67
+ .btn-secondary {
68
+ @apply cursor-pointer rounded-lg border border-transparent bg-blue-600 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus-visible:z-10 focus-visible:bg-blue-500 focus-visible:text-white focus-visible:ring-4 focus-visible:ring-blue-200 disabled:cursor-not-allowed disabled:border-transparent disabled:bg-gray-500 disabled:text-white;
69
+ }
70
+
71
+ .btn-alternative {
72
+ @apply cursor-pointer rounded-lg border border-gray-600 bg-gray-800 px-5 py-2.5 text-sm font-medium text-gray-400 transition-colors hover:bg-gray-700 hover:text-white focus:z-10 focus:outline-none focus-visible:ring-4 focus-visible:ring-gray-700 disabled:cursor-not-allowed disabled:bg-gray-800 disabled:text-gray-400;
73
+ }
74
+
75
+ .card {
76
+ @apply mb-4 rounded-lg border border-gray-800 bg-gray-900 p-4 shadow;
77
+ }
78
+
79
+ .search-input {
80
+ @apply block w-full rounded-lg border border-gray-600 bg-gray-800 p-2.5 pr-10 pl-10 text-sm text-gray-100 placeholder-gray-300 focus:border-blue-500 focus:text-white focus:ring-1 focus:ring-blue-500 focus:outline-none [&::-webkit-search-cancel-button]:hidden;
81
+ }
82
+
83
+ select {
84
+ @apply block w-full rounded-lg border border-gray-700 bg-gray-800 p-2.5 text-sm text-gray-100 placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none;
85
+ }
86
+
87
+ svg.icon {
88
+ @apply flex-shrink-0 text-gray-400 transition duration-300;
89
+ }
90
+
91
+ .text-link {
92
+ @apply text-blue-400 decoration-blue-400 hover:underline;
93
+ }
94
+
95
+ .no-scrollbar {
96
+ scrollbar-width: none;
97
+ }
98
+ }
99
+
100
+ @utility scrollable-container {
101
+ overflow-x: hidden;
102
+ overflow-y: auto;
103
+ padding-bottom: 1rem;
104
+
105
+ position: static !important;
106
+ scrollbar-width: thin; /* duplication required here despite global override */
107
+
108
+ &:not(.no-fade) {
109
+ /* Create a fade overlay at the bottom */
110
+
111
+ &::after {
112
+ content: '';
113
+ @apply pointer-events-none absolute right-0 bottom-0 left-0;
114
+ height: 28px;
115
+ @apply bg-gradient-to-b from-gray-900/0 to-gray-900;
116
+ }
117
+ }
118
+
119
+ /* Specific styling for UI elements inside scrollable containers */
120
+
121
+ .btn {
122
+ &:not(.keep-default-btn-style) {
123
+ @apply border-none;
124
+ }
125
+ }
126
+
127
+ table {
128
+ @apply w-full table-fixed text-left text-sm text-gray-400;
129
+
130
+ thead {
131
+ @apply sticky top-0 z-10 text-xs text-gray-400 uppercase shadow-sm;
132
+
133
+ tr {
134
+ @apply bg-gray-900/30 text-left text-xs font-medium tracking-wider text-gray-400 uppercase;
135
+
136
+ th {
137
+ @apply bg-gray-800 px-2 py-4 whitespace-nowrap;
138
+ }
139
+
140
+ th:first-child {
141
+ @apply rounded-tl-lg;
142
+ }
143
+
144
+ th:last-child {
145
+ @apply rounded-tr-lg;
146
+ }
147
+ }
148
+ }
149
+
150
+ tbody {
151
+ tr {
152
+ @apply bg-gray-900;
153
+
154
+ &.group-boundary {
155
+ @apply border-t-5 border-t-gray-800;
156
+ }
157
+
158
+ &.expandable-row {
159
+ td {
160
+ @apply bg-gray-950;
161
+ }
162
+ }
163
+
164
+ td {
165
+ @apply whitespace-nowrap text-gray-400;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ /* Create a fade overlay at the bottom for catalog views */
173
+ @utility fade-overlay {
174
+ position: sticky;
175
+ right: 0;
176
+ bottom: 0;
177
+ left: 0;
178
+ z-index: 10;
179
+ pointer-events: none;
180
+ height: 32px;
181
+ @apply bg-gradient-to-b from-gray-950/0 to-gray-950;
182
+ }
183
+
184
+ @custom-variant mobileLandscape {
185
+ @media only screen and ((
186
+ ((max-height: 990px) and (max-width: 640px)) or
187
+ ((max-height: 640px) and (max-width: 990px))
188
+ )
189
+ and (orientation: landscape)) {
190
+ @slot;
191
+ }
192
+ }
package/src/app.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
+ }
12
+
13
+ export {};
package/src/app.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <!-- Set background color to match bg-gray-950 before styles are available -->
3
+ <html class="dark" lang="en" style="background-color: oklch(0.13 0.028 261.692)">
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon" href="%sveltekit.assets%/lula.png" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ %sveltekit.head%
9
+ </head>
10
+ <body data-sveltekit-preload-data="hover">
11
+ <div style="display: contents">%sveltekit.body%</div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ // Action to apply or remove the 'no-fade' class based on when an element is scrollable (overflow-y-scroll)
5
+ // Our 'scrollable-container' class uses a CSS pseudo-element to apply a fade effect at the bottom of the container
6
+ // The 'no-fade' class removes that effect
7
+ export function fadeWhenScrollable(node: HTMLElement) {
8
+ // `check` adds “no-fade” if there is NO vertical overflow,
9
+ // removes it once overflow appears.
10
+ function check() {
11
+ if (node.scrollHeight > node.clientHeight) {
12
+ node.classList.remove('no-fade');
13
+ } else {
14
+ node.classList.add('no-fade');
15
+ }
16
+ }
17
+
18
+ // 1) watch for size/box changes (e.g. window resize, parent CSS changes, etc.)
19
+ const resizeObs = new ResizeObserver(check);
20
+ resizeObs.observe(node);
21
+
22
+ // 2) watch for DOM mutations under `node` (e.g. new rows inserted into a table)
23
+ const mutationObs = new MutationObserver(check);
24
+ mutationObs.observe(node, { childList: true, subtree: true });
25
+
26
+ // 3) also re-check on window resize
27
+ window.addEventListener('resize', check);
28
+
29
+ // initial check
30
+ check();
31
+
32
+ return {
33
+ destroy() {
34
+ resizeObs.disconnect();
35
+ mutationObs.disconnect();
36
+ window.removeEventListener('resize', check);
37
+ }
38
+ };
39
+ }
@@ -0,0 +1,230 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ /**
5
+ * Modal action for Svelte components
6
+ * Creates a modal dialog that appears when the trigger element is clicked
7
+ */
8
+
9
+ // Types for the modal action
10
+ type ModalOptions = {
11
+ openOnInit?: boolean; // Whether to open the modal immediately when component mounts
12
+ closeOnEscape?: boolean; // Whether to close on Escape key
13
+ closeOnOutsideClick?: boolean; // Whether to close when clicking outside
14
+ onOpen?: () => void; // Callback for when modal opens
15
+ onClose?: () => void; // Callback for when modal closes
16
+ };
17
+
18
+ /**
19
+ * Creates a click-to-open modal from an element with a .modal-content child
20
+ * @param node The HTML element to attach the modal trigger to
21
+ * @param options Configuration options
22
+ * @returns Svelte action object
23
+ */
24
+ // Store a map of modal nodes to their closeModal functions for external access
25
+ const modalRegistry = new Map<string, () => void>();
26
+
27
+ /**
28
+ * Closes a modal by its ID
29
+ * @param modalId The ID of the modal to close
30
+ */
31
+ export function closeModalById(modalId: string): void {
32
+ const closeFunction = modalRegistry.get(modalId);
33
+ if (closeFunction) {
34
+ closeFunction();
35
+ } else {
36
+ console.warn(`No modal with ID ${modalId} found in registry`);
37
+ }
38
+ }
39
+
40
+ export function modal(node: HTMLElement, options: ModalOptions = {}) {
41
+ const defaults: ModalOptions = {
42
+ openOnInit: false,
43
+ closeOnEscape: true,
44
+ closeOnOutsideClick: true
45
+ };
46
+
47
+ // Merge defaults with provided options
48
+ const settings = { ...defaults, ...options };
49
+
50
+ // Find modal content element
51
+ const modalContent = node.querySelector('.modal-content') as HTMLElement;
52
+ // Use the parent element of the node as the trigger element
53
+ const triggerElement = node.parentElement as HTMLElement;
54
+
55
+ if (!modalContent) {
56
+ return {};
57
+ }
58
+
59
+ if (!triggerElement) {
60
+ return {};
61
+ }
62
+
63
+ // Create backdrop/overlay element
64
+ const backdrop = document.createElement('div');
65
+ backdrop.className = 'modal-backdrop';
66
+ backdrop.style.display = 'none';
67
+ document.body.appendChild(backdrop);
68
+
69
+ // Set initial ARIA attributes
70
+ modalContent.setAttribute('role', 'dialog');
71
+ modalContent.setAttribute('aria-modal', 'true');
72
+ modalContent.setAttribute('aria-hidden', 'true');
73
+
74
+ // State to track if modal is open
75
+ let isOpen = false;
76
+
77
+ // Function to open the modal
78
+ const openModal = () => {
79
+ if (isOpen) return;
80
+
81
+ isOpen = true;
82
+ // Move modal content to body to avoid stacking context issues
83
+ document.body.appendChild(modalContent);
84
+ modalContent.setAttribute('aria-hidden', 'false');
85
+ backdrop.style.display = 'block';
86
+
87
+ // Prevent scrolling on the body
88
+ document.body.style.overflow = 'hidden';
89
+
90
+ // Call onOpen callback if provided
91
+ if (settings.onOpen) settings.onOpen();
92
+
93
+ // Focus first focusable element in modal
94
+ const focusableElements = modalContent.querySelectorAll(
95
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
96
+ );
97
+ if (focusableElements.length > 0) {
98
+ (focusableElements[0] as HTMLElement).focus();
99
+ }
100
+ };
101
+
102
+ // Function to close the modal
103
+ const closeModal = () => {
104
+ if (!isOpen) return;
105
+
106
+ isOpen = false;
107
+ modalContent.setAttribute('aria-hidden', 'true');
108
+ backdrop.style.display = 'none';
109
+
110
+ // Move modal back to original position
111
+ if (document.body.contains(modalContent)) {
112
+ node.appendChild(modalContent);
113
+ }
114
+
115
+ // Restore scrolling on body
116
+ document.body.style.overflow = '';
117
+
118
+ // Call onClose callback if provided
119
+ if (settings.onClose) settings.onClose();
120
+
121
+ // Return focus to trigger element
122
+ if (triggerElement) {
123
+ triggerElement.focus();
124
+ }
125
+ };
126
+
127
+ // Register the modal in our registry if it has an ID
128
+ const modalId = node.getAttribute('id');
129
+ if (modalId) {
130
+ modalRegistry.set(modalId, closeModal);
131
+ }
132
+
133
+ // Handle click on trigger to open modal
134
+ const handleTriggerClick = (e: Event) => {
135
+ e.preventDefault();
136
+ e.stopPropagation();
137
+ openModal();
138
+ };
139
+
140
+ // Handle click on backdrop to close modal if closeOnOutsideClick is true
141
+ const handleBackdropClick = (e: MouseEvent) => {
142
+ if (!settings.closeOnOutsideClick) return;
143
+
144
+ // Only close if the click was directly on the backdrop, not on modal content
145
+ if (e.target === backdrop) {
146
+ closeModal();
147
+ }
148
+ };
149
+
150
+ // Handle escape key to close modal if closeOnEscape is true
151
+ const handleKeyDown = (e: KeyboardEvent) => {
152
+ if (!settings.closeOnEscape) return;
153
+
154
+ if (e.key === 'Escape' && isOpen) {
155
+ closeModal();
156
+ }
157
+ };
158
+
159
+ // Add close button to modal
160
+ const closeButton = document.createElement('button');
161
+ closeButton.className = 'modal-close';
162
+ closeButton.innerHTML = '×';
163
+ closeButton.setAttribute('aria-label', 'Close modal');
164
+ modalContent.appendChild(closeButton);
165
+
166
+ // Handle click on close button
167
+ const handleCloseClick = (e: Event) => {
168
+ e.preventDefault();
169
+ e.stopPropagation();
170
+ closeModal();
171
+ };
172
+
173
+ // Set up event listeners
174
+ triggerElement.addEventListener('click', handleTriggerClick);
175
+
176
+ closeButton.addEventListener('click', handleCloseClick);
177
+ backdrop.addEventListener('click', handleBackdropClick);
178
+ document.addEventListener('keydown', handleKeyDown);
179
+
180
+ // Open modal on init if specified
181
+ if (settings.openOnInit) {
182
+ setTimeout(openModal, 0);
183
+ }
184
+
185
+ return {
186
+ update(newOptions: ModalOptions) {
187
+ // Update settings
188
+ Object.assign(settings, newOptions);
189
+ },
190
+ destroy() {
191
+ // Clean up event listeners
192
+ triggerElement.removeEventListener('click', handleTriggerClick);
193
+
194
+ closeButton.removeEventListener('click', handleCloseClick);
195
+ backdrop.removeEventListener('click', handleBackdropClick);
196
+ document.removeEventListener('keydown', handleKeyDown);
197
+
198
+ // Remove backdrop from DOM
199
+ if (document.body.contains(backdrop)) {
200
+ document.body.removeChild(backdrop);
201
+ }
202
+
203
+ // Return modal to original position if needed
204
+ if (document.body.contains(modalContent)) {
205
+ node.appendChild(modalContent);
206
+ }
207
+
208
+ // Remove close button from modal
209
+ if (modalContent.contains(closeButton)) {
210
+ modalContent.removeChild(closeButton);
211
+ }
212
+
213
+ // Reset attributes
214
+ modalContent.removeAttribute('role');
215
+ modalContent.removeAttribute('aria-modal');
216
+ modalContent.removeAttribute('aria-hidden');
217
+
218
+ // If modal is open when destroyed, restore body scrolling
219
+ if (isOpen) {
220
+ document.body.style.overflow = '';
221
+ }
222
+
223
+ // Remove from registry if it was registered
224
+ const modalId = node.getAttribute('id');
225
+ if (modalId && modalRegistry.has(modalId)) {
226
+ modalRegistry.delete(modalId);
227
+ }
228
+ }
229
+ };
230
+ }