@smartnet360/svelte-components 0.0.123 → 0.0.125
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/dist/apps/antenna-tools/components/AntennaControls.svelte +71 -9
- package/dist/apps/antenna-tools/components/AntennaControls.svelte.d.ts +2 -0
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +4 -174
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +48 -82
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +5 -8
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +377 -52
- package/dist/apps/antenna-tools/db.js +4 -0
- package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
- package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
- package/dist/apps/antenna-tools/utils/msi-parser.d.ts +35 -1
- package/dist/apps/antenna-tools/utils/msi-parser.js +105 -35
- package/dist/core/Auth/LoginForm.svelte +397 -0
- package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
- package/dist/core/Auth/auth.svelte.d.ts +22 -0
- package/dist/core/Auth/auth.svelte.js +184 -0
- package/dist/core/Auth/config.d.ts +25 -0
- package/dist/core/Auth/config.js +256 -0
- package/dist/core/Auth/index.d.ts +4 -0
- package/dist/core/Auth/index.js +5 -0
- package/dist/core/Auth/types.d.ts +140 -0
- package/dist/core/Auth/types.js +2 -0
- package/dist/core/Benchmark/Benchmark.svelte +662 -0
- package/dist/core/Benchmark/Benchmark.svelte.d.ts +3 -0
- package/dist/core/Benchmark/benchmark-utils.d.ts +48 -0
- package/dist/core/Benchmark/benchmark-utils.js +80 -0
- package/dist/core/Benchmark/index.d.ts +2 -0
- package/dist/core/Benchmark/index.js +3 -0
- package/dist/core/LandingPage/App.svelte +102 -0
- package/dist/core/LandingPage/App.svelte.d.ts +20 -0
- package/dist/core/LandingPage/LandingPage.svelte +480 -0
- package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
- package/dist/core/LandingPage/index.d.ts +2 -0
- package/dist/core/LandingPage/index.js +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +6 -0
- package/package.json +1 -1
|
@@ -24,12 +24,46 @@ export interface ParseProgress {
|
|
|
24
24
|
directoriesScanned: number;
|
|
25
25
|
/** Number of MSI/PNT files found */
|
|
26
26
|
antennaFilesFound: number;
|
|
27
|
+
/** Number of files that failed to parse */
|
|
28
|
+
failedFiles: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Error information for failed file/folder parsing
|
|
32
|
+
*/
|
|
33
|
+
export interface ParseError {
|
|
34
|
+
/** Path to the file or folder that failed */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Error message */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Type of error */
|
|
39
|
+
type: 'file' | 'folder';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Result of folder parsing including errors
|
|
43
|
+
*/
|
|
44
|
+
export interface ParseFolderResult {
|
|
45
|
+
/** Successfully parsed antennas */
|
|
46
|
+
antennas: RawAntenna[];
|
|
47
|
+
/** Errors encountered during parsing */
|
|
48
|
+
errors: ParseError[];
|
|
49
|
+
/** Total files attempted */
|
|
50
|
+
totalFilesAttempted: number;
|
|
51
|
+
/** Total directories scanned */
|
|
52
|
+
totalDirectoriesScanned: number;
|
|
27
53
|
}
|
|
28
54
|
/**
|
|
29
55
|
* Parse a folder of MSI files recursively
|
|
30
56
|
* @param directoryHandle - File system directory handle
|
|
31
57
|
* @param recursive - Whether to process subdirectories
|
|
32
58
|
* @param onProgress - Optional callback for progress updates
|
|
33
|
-
* @returns Array of parsed antennas
|
|
59
|
+
* @returns Array of parsed antennas (for backward compatibility)
|
|
34
60
|
*/
|
|
35
61
|
export declare function parseFolder(directoryHandle: FileSystemDirectoryHandle, recursive?: boolean, onProgress?: (progress: ParseProgress) => void): Promise<RawAntenna[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Parse a folder of MSI files recursively with detailed error reporting
|
|
64
|
+
* @param directoryHandle - File system directory handle
|
|
65
|
+
* @param recursive - Whether to process subdirectories
|
|
66
|
+
* @param onProgress - Optional callback for progress updates
|
|
67
|
+
* @returns Object containing parsed antennas and any errors encountered
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseFolderWithErrors(directoryHandle: FileSystemDirectoryHandle, recursive?: boolean, onProgress?: (progress: ParseProgress) => void): Promise<ParseFolderResult>;
|
|
@@ -174,55 +174,125 @@ export async function parseMSIFile(file) {
|
|
|
174
174
|
* @param directoryHandle - File system directory handle
|
|
175
175
|
* @param recursive - Whether to process subdirectories
|
|
176
176
|
* @param onProgress - Optional callback for progress updates
|
|
177
|
-
* @returns Array of parsed antennas
|
|
177
|
+
* @returns Array of parsed antennas (for backward compatibility)
|
|
178
178
|
*/
|
|
179
179
|
export async function parseFolder(directoryHandle, recursive = true, onProgress) {
|
|
180
|
+
const result = await parseFolderWithErrors(directoryHandle, recursive, onProgress);
|
|
181
|
+
return result.antennas;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Parse a folder of MSI files recursively with detailed error reporting
|
|
185
|
+
* @param directoryHandle - File system directory handle
|
|
186
|
+
* @param recursive - Whether to process subdirectories
|
|
187
|
+
* @param onProgress - Optional callback for progress updates
|
|
188
|
+
* @returns Object containing parsed antennas and any errors encountered
|
|
189
|
+
*/
|
|
190
|
+
export async function parseFolderWithErrors(directoryHandle, recursive = true, onProgress) {
|
|
180
191
|
const antennas = [];
|
|
192
|
+
const errors = [];
|
|
181
193
|
let filesProcessed = 0;
|
|
182
194
|
let directoriesScanned = 0;
|
|
195
|
+
let failedFiles = 0;
|
|
183
196
|
// Process directory recursively
|
|
184
197
|
async function processDirectory(dirHandle, path = '') {
|
|
185
198
|
directoriesScanned++;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
199
|
+
let iterator;
|
|
200
|
+
try {
|
|
201
|
+
iterator = dirHandle.values();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const dirPath = path || dirHandle.name;
|
|
205
|
+
errors.push({
|
|
206
|
+
path: dirPath,
|
|
207
|
+
message: error instanceof Error ? error.message : 'Failed to read directory',
|
|
208
|
+
type: 'folder'
|
|
209
|
+
});
|
|
210
|
+
console.error(`Error reading directory ${dirPath}:`, error);
|
|
211
|
+
return; // Skip this directory but continue with others
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
for await (const entry of iterator) {
|
|
215
|
+
if (entry.kind === 'file') {
|
|
216
|
+
try {
|
|
217
|
+
// Type assertion to access getFile method
|
|
218
|
+
const fileHandle = entry;
|
|
219
|
+
const file = await fileHandle.getFile();
|
|
220
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
221
|
+
if (ext === 'msi' || ext === 'pnt') {
|
|
222
|
+
filesProcessed++;
|
|
223
|
+
const currentPath = path ? `${path}/${file.name}` : file.name;
|
|
224
|
+
// Report progress
|
|
225
|
+
onProgress?.({
|
|
226
|
+
currentFile: currentPath,
|
|
227
|
+
filesProcessed,
|
|
228
|
+
directoriesScanned,
|
|
229
|
+
antennaFilesFound: antennas.length,
|
|
230
|
+
failedFiles
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
const antenna = await parseMSIFile(file);
|
|
234
|
+
// Add the path info to help identify where the file was found
|
|
235
|
+
antenna.sourcePath = currentPath;
|
|
236
|
+
antennas.push(antenna);
|
|
237
|
+
}
|
|
238
|
+
catch (parseError) {
|
|
239
|
+
failedFiles++;
|
|
240
|
+
errors.push({
|
|
241
|
+
path: currentPath,
|
|
242
|
+
message: parseError instanceof Error ? parseError.message : 'Failed to parse file',
|
|
243
|
+
type: 'file'
|
|
244
|
+
});
|
|
245
|
+
console.error(`Error parsing file ${currentPath}:`, parseError);
|
|
246
|
+
// Continue with next file
|
|
247
|
+
}
|
|
212
248
|
}
|
|
213
249
|
}
|
|
250
|
+
catch (fileError) {
|
|
251
|
+
const filePath = path ? `${path}/${entry.name}` : entry.name;
|
|
252
|
+
failedFiles++;
|
|
253
|
+
errors.push({
|
|
254
|
+
path: filePath,
|
|
255
|
+
message: fileError instanceof Error ? fileError.message : 'Failed to access file',
|
|
256
|
+
type: 'file'
|
|
257
|
+
});
|
|
258
|
+
console.error(`Error accessing file ${filePath}:`, fileError);
|
|
259
|
+
// Continue with next file
|
|
260
|
+
}
|
|
214
261
|
}
|
|
215
|
-
|
|
216
|
-
|
|
262
|
+
else if (entry.kind === 'directory' && recursive) {
|
|
263
|
+
// Process subdirectory if recursive flag is true
|
|
264
|
+
const subDirPath = path ? `${path}/${entry.name}` : entry.name;
|
|
265
|
+
try {
|
|
266
|
+
await processDirectory(entry, subDirPath);
|
|
267
|
+
}
|
|
268
|
+
catch (subDirError) {
|
|
269
|
+
errors.push({
|
|
270
|
+
path: subDirPath,
|
|
271
|
+
message: subDirError instanceof Error ? subDirError.message : 'Failed to process subdirectory',
|
|
272
|
+
type: 'folder'
|
|
273
|
+
});
|
|
274
|
+
console.error(`Error processing subdirectory ${subDirPath}:`, subDirError);
|
|
275
|
+
// Continue with next entry
|
|
276
|
+
}
|
|
217
277
|
}
|
|
218
278
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
279
|
+
}
|
|
280
|
+
catch (iterError) {
|
|
281
|
+
const dirPath = path || dirHandle.name;
|
|
282
|
+
errors.push({
|
|
283
|
+
path: dirPath,
|
|
284
|
+
message: iterError instanceof Error ? iterError.message : 'Error iterating directory contents',
|
|
285
|
+
type: 'folder'
|
|
286
|
+
});
|
|
287
|
+
console.error(`Error iterating directory ${dirPath}:`, iterError);
|
|
288
|
+
// Continue - the directory iteration failed but we can still return what we have
|
|
224
289
|
}
|
|
225
290
|
}
|
|
226
291
|
await processDirectory(directoryHandle);
|
|
227
|
-
return
|
|
292
|
+
return {
|
|
293
|
+
antennas,
|
|
294
|
+
errors,
|
|
295
|
+
totalFilesAttempted: filesProcessed,
|
|
296
|
+
totalDirectoriesScanned: directoriesScanned
|
|
297
|
+
};
|
|
228
298
|
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AuthState } from './auth.svelte.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Auth state instance */
|
|
6
|
+
auth: AuthState;
|
|
7
|
+
/** Title for login form */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Subtitle/description */
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
/** Logo URL (optional) */
|
|
12
|
+
logo?: string;
|
|
13
|
+
/** Callback on successful login */
|
|
14
|
+
onLogin?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
auth,
|
|
19
|
+
title = 'Welcome',
|
|
20
|
+
subtitle = 'Sign in to access your tools',
|
|
21
|
+
logo,
|
|
22
|
+
onLogin
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
// Form state
|
|
26
|
+
let username = $state('');
|
|
27
|
+
let password = $state('');
|
|
28
|
+
let agreedToMonitoring = $state(false);
|
|
29
|
+
let showPassword = $state(false);
|
|
30
|
+
|
|
31
|
+
// Form validation
|
|
32
|
+
const isFormValid = $derived(
|
|
33
|
+
username.trim().length > 0 &&
|
|
34
|
+
password.length > 0 &&
|
|
35
|
+
agreedToMonitoring
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
|
|
41
|
+
if (!isFormValid) return;
|
|
42
|
+
|
|
43
|
+
const success = await auth.login(username.trim(), password, agreedToMonitoring);
|
|
44
|
+
|
|
45
|
+
if (success) {
|
|
46
|
+
username = '';
|
|
47
|
+
password = '';
|
|
48
|
+
agreedToMonitoring = false;
|
|
49
|
+
onLogin?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function togglePassword() {
|
|
54
|
+
showPassword = !showPassword;
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="login-page">
|
|
59
|
+
<div class="login-card">
|
|
60
|
+
<!-- Header -->
|
|
61
|
+
<div class="login-header">
|
|
62
|
+
{#if logo}
|
|
63
|
+
<img src={logo} alt="Logo" class="login-logo" />
|
|
64
|
+
{:else}
|
|
65
|
+
<div class="login-icon">
|
|
66
|
+
<i class="bi bi-broadcast-pin"></i>
|
|
67
|
+
</div>
|
|
68
|
+
{/if}
|
|
69
|
+
<h1>{title}</h1>
|
|
70
|
+
<p>{subtitle}</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Error -->
|
|
74
|
+
{#if auth.error}
|
|
75
|
+
<div class="login-error">
|
|
76
|
+
<i class="bi bi-exclamation-circle"></i>
|
|
77
|
+
<span>{auth.error}</span>
|
|
78
|
+
<button type="button" onclick={() => auth.clearError()} aria-label="Dismiss error">
|
|
79
|
+
<i class="bi bi-x"></i>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<!-- Form -->
|
|
85
|
+
<form onsubmit={handleSubmit} class="login-form">
|
|
86
|
+
<div class="form-field">
|
|
87
|
+
<label for="username">Username</label>
|
|
88
|
+
<div class="input-wrapper">
|
|
89
|
+
<i class="bi bi-person"></i>
|
|
90
|
+
<input
|
|
91
|
+
type="text"
|
|
92
|
+
id="username"
|
|
93
|
+
bind:value={username}
|
|
94
|
+
placeholder="Enter username"
|
|
95
|
+
autocomplete="username"
|
|
96
|
+
disabled={auth.isLoading}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="form-field">
|
|
102
|
+
<label for="password">Password</label>
|
|
103
|
+
<div class="input-wrapper">
|
|
104
|
+
<i class="bi bi-lock"></i>
|
|
105
|
+
<input
|
|
106
|
+
type={showPassword ? 'text' : 'password'}
|
|
107
|
+
id="password"
|
|
108
|
+
bind:value={password}
|
|
109
|
+
placeholder="Enter password"
|
|
110
|
+
autocomplete="current-password"
|
|
111
|
+
disabled={auth.isLoading}
|
|
112
|
+
/>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="toggle-password"
|
|
116
|
+
onclick={togglePassword}
|
|
117
|
+
tabindex={-1}
|
|
118
|
+
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
119
|
+
>
|
|
120
|
+
<i class="bi {showPassword ? 'bi-eye-slash' : 'bi-eye'}"></i>
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<label class="checkbox-field">
|
|
126
|
+
<input
|
|
127
|
+
type="checkbox"
|
|
128
|
+
bind:checked={agreedToMonitoring}
|
|
129
|
+
disabled={auth.isLoading}
|
|
130
|
+
/>
|
|
131
|
+
<span class="checkmark"></span>
|
|
132
|
+
<span class="checkbox-text">
|
|
133
|
+
I agree to activity monitoring for security purposes
|
|
134
|
+
</span>
|
|
135
|
+
</label>
|
|
136
|
+
|
|
137
|
+
<button
|
|
138
|
+
type="submit"
|
|
139
|
+
class="login-button"
|
|
140
|
+
disabled={!isFormValid || auth.isLoading}
|
|
141
|
+
>
|
|
142
|
+
{#if auth.isLoading}
|
|
143
|
+
<span class="spinner"></span>
|
|
144
|
+
Signing in...
|
|
145
|
+
{:else}
|
|
146
|
+
Sign In
|
|
147
|
+
<i class="bi bi-arrow-right"></i>
|
|
148
|
+
{/if}
|
|
149
|
+
</button>
|
|
150
|
+
</form>
|
|
151
|
+
|
|
152
|
+
<div class="login-footer">
|
|
153
|
+
<i class="bi bi-shield-check"></i>
|
|
154
|
+
LDAP Authentication
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<style>
|
|
160
|
+
.login-page {
|
|
161
|
+
min-height: 100vh;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
padding: 1.5rem;
|
|
166
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.login-card {
|
|
170
|
+
width: 100%;
|
|
171
|
+
max-width: 380px;
|
|
172
|
+
background: #fff;
|
|
173
|
+
border-radius: 16px;
|
|
174
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.login-header {
|
|
179
|
+
text-align: center;
|
|
180
|
+
padding: 2.5rem 2rem 1.5rem;
|
|
181
|
+
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
|
182
|
+
color: #fff;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.login-logo {
|
|
186
|
+
height: 48px;
|
|
187
|
+
margin-bottom: 1rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.login-icon {
|
|
191
|
+
font-size: 2.5rem;
|
|
192
|
+
margin-bottom: 0.75rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.login-header h1 {
|
|
196
|
+
font-size: 1.5rem;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
margin: 0 0 0.25rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.login-header p {
|
|
202
|
+
font-size: 0.875rem;
|
|
203
|
+
opacity: 0.85;
|
|
204
|
+
margin: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.login-error {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 0.5rem;
|
|
211
|
+
margin: 1rem 1.5rem 0;
|
|
212
|
+
padding: 0.75rem 1rem;
|
|
213
|
+
background: #fef2f2;
|
|
214
|
+
border: 1px solid #fecaca;
|
|
215
|
+
border-radius: 8px;
|
|
216
|
+
color: #dc2626;
|
|
217
|
+
font-size: 0.875rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.login-error button {
|
|
221
|
+
margin-left: auto;
|
|
222
|
+
background: none;
|
|
223
|
+
border: none;
|
|
224
|
+
color: inherit;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
padding: 0;
|
|
227
|
+
font-size: 1.25rem;
|
|
228
|
+
line-height: 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.login-form {
|
|
232
|
+
padding: 1.5rem 2rem 2rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.form-field {
|
|
236
|
+
margin-bottom: 1.25rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.form-field label {
|
|
240
|
+
display: block;
|
|
241
|
+
font-size: 0.8125rem;
|
|
242
|
+
font-weight: 500;
|
|
243
|
+
color: #374151;
|
|
244
|
+
margin-bottom: 0.5rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.input-wrapper {
|
|
248
|
+
position: relative;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.input-wrapper > i:first-child {
|
|
254
|
+
position: absolute;
|
|
255
|
+
left: 0.875rem;
|
|
256
|
+
color: #9ca3af;
|
|
257
|
+
font-size: 1rem;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.input-wrapper input {
|
|
261
|
+
width: 100%;
|
|
262
|
+
padding: 0.75rem 0.875rem 0.75rem 2.5rem;
|
|
263
|
+
border: 1px solid #e5e7eb;
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
font-size: 0.9375rem;
|
|
266
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.input-wrapper input:focus {
|
|
270
|
+
outline: none;
|
|
271
|
+
border-color: #3b82f6;
|
|
272
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.input-wrapper input:disabled {
|
|
276
|
+
background: #f9fafb;
|
|
277
|
+
cursor: not-allowed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.toggle-password {
|
|
281
|
+
position: absolute;
|
|
282
|
+
right: 0.5rem;
|
|
283
|
+
background: none;
|
|
284
|
+
border: none;
|
|
285
|
+
color: #9ca3af;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
padding: 0.5rem;
|
|
288
|
+
font-size: 1rem;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.toggle-password:hover {
|
|
292
|
+
color: #6b7280;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.checkbox-field {
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: flex-start;
|
|
298
|
+
gap: 0.625rem;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
margin-bottom: 1.5rem;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.checkbox-field input {
|
|
304
|
+
position: absolute;
|
|
305
|
+
opacity: 0;
|
|
306
|
+
pointer-events: none;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.checkmark {
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
width: 18px;
|
|
312
|
+
height: 18px;
|
|
313
|
+
border: 2px solid #d1d5db;
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
transition: all 0.15s;
|
|
316
|
+
display: flex;
|
|
317
|
+
align-items: center;
|
|
318
|
+
justify-content: center;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.checkbox-field input:checked + .checkmark {
|
|
322
|
+
background: #3b82f6;
|
|
323
|
+
border-color: #3b82f6;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.checkbox-field input:checked + .checkmark::after {
|
|
327
|
+
content: '';
|
|
328
|
+
width: 5px;
|
|
329
|
+
height: 9px;
|
|
330
|
+
border: solid #fff;
|
|
331
|
+
border-width: 0 2px 2px 0;
|
|
332
|
+
transform: rotate(45deg);
|
|
333
|
+
margin-bottom: 2px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.checkbox-text {
|
|
337
|
+
font-size: 0.8125rem;
|
|
338
|
+
color: #4b5563;
|
|
339
|
+
line-height: 1.4;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.login-button {
|
|
343
|
+
width: 100%;
|
|
344
|
+
padding: 0.875rem 1.5rem;
|
|
345
|
+
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
|
346
|
+
color: #fff;
|
|
347
|
+
border: none;
|
|
348
|
+
border-radius: 8px;
|
|
349
|
+
font-size: 0.9375rem;
|
|
350
|
+
font-weight: 500;
|
|
351
|
+
cursor: pointer;
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
justify-content: center;
|
|
355
|
+
gap: 0.5rem;
|
|
356
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.login-button:hover:not(:disabled) {
|
|
360
|
+
opacity: 0.9;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.login-button:active:not(:disabled) {
|
|
364
|
+
transform: scale(0.98);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.login-button:disabled {
|
|
368
|
+
opacity: 0.5;
|
|
369
|
+
cursor: not-allowed;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.spinner {
|
|
373
|
+
width: 16px;
|
|
374
|
+
height: 16px;
|
|
375
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
376
|
+
border-top-color: #fff;
|
|
377
|
+
border-radius: 50%;
|
|
378
|
+
animation: spin 0.8s linear infinite;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@keyframes spin {
|
|
382
|
+
to { transform: rotate(360deg); }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.login-footer {
|
|
386
|
+
text-align: center;
|
|
387
|
+
padding: 1rem;
|
|
388
|
+
background: #f8fafc;
|
|
389
|
+
border-top: 1px solid #e5e7eb;
|
|
390
|
+
font-size: 0.75rem;
|
|
391
|
+
color: #64748b;
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
gap: 0.375rem;
|
|
396
|
+
}
|
|
397
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthState } from './auth.svelte.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Auth state instance */
|
|
4
|
+
auth: AuthState;
|
|
5
|
+
/** Title for login form */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Subtitle/description */
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
/** Logo URL (optional) */
|
|
10
|
+
logo?: string;
|
|
11
|
+
/** Callback on successful login */
|
|
12
|
+
onLogin?: () => void;
|
|
13
|
+
}
|
|
14
|
+
declare const LoginForm: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type LoginForm = ReturnType<typeof LoginForm>;
|
|
16
|
+
export default LoginForm;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UserSession, User, FeatureAccess, AuthConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create auth state store with Svelte 5 runes
|
|
4
|
+
* This must be called within a component context or .svelte.ts file
|
|
5
|
+
*/
|
|
6
|
+
export declare function createAuthState(config?: Partial<AuthConfig>): {
|
|
7
|
+
readonly session: UserSession | null;
|
|
8
|
+
readonly isLoading: boolean;
|
|
9
|
+
readonly error: string | null;
|
|
10
|
+
readonly initialized: boolean;
|
|
11
|
+
readonly isAuthenticated: boolean;
|
|
12
|
+
readonly user: User | null;
|
|
13
|
+
readonly permissions: FeatureAccess[];
|
|
14
|
+
readonly token: string | null;
|
|
15
|
+
login: (username: string, password: string, agreedToMonitoring: boolean) => Promise<boolean>;
|
|
16
|
+
loginDev: (devUser: User, devPermissions: FeatureAccess[]) => void;
|
|
17
|
+
logout: () => Promise<void>;
|
|
18
|
+
hasPermission: (featureId: string, level?: "view" | "edit" | "admin") => boolean;
|
|
19
|
+
canAccessRoute: (route: string, requiredPermission?: string) => boolean;
|
|
20
|
+
clearError: () => void;
|
|
21
|
+
};
|
|
22
|
+
export type AuthState = ReturnType<typeof createAuthState>;
|