@svadmin/lite 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -18
- package/dist/compatibility.js +29 -29
- package/dist/components/LiteArrayItem.svelte +2 -2
- package/dist/components/LiteCanvasAnnotation.svelte +3 -1
- package/dist/components/LiteFileInput.svelte +40 -0
- package/dist/components/LiteFileInput.svelte.d.ts +11 -0
- package/dist/components/LiteForm.svelte +2 -2
- package/dist/components/LiteImportWizard.svelte +2 -1
- package/dist/components/LiteJsonSchemaForm.svelte +11 -9
- package/dist/components/LiteMasterDetailView.svelte +2 -2
- package/dist/components/LiteMediaLibraryModal.svelte +5 -2
- package/dist/components/LiteSensitiveDataMask.svelte +2 -1
- package/dist/components/LiteShowField.svelte +6 -4
- package/dist/components/LiteSignaturePad.svelte +3 -1
- package/dist/components/advanced/LiteAutoSaveIndicator.svelte +2 -2
- package/dist/components/advanced/LiteToast.svelte +2 -2
- package/dist/components/advanced/LiteVirtualTable.svelte +6 -5
- package/dist/components/buttons/LiteCloneButton.svelte +3 -0
- package/dist/components/buttons/LiteCreateButton.svelte +3 -0
- package/dist/components/buttons/LiteDeleteButton.svelte +3 -0
- package/dist/components/buttons/LiteEditButton.svelte +3 -0
- package/dist/components/buttons/LiteExportButton.svelte +3 -0
- package/dist/components/buttons/LiteImportButton.svelte +5 -1
- package/dist/components/buttons/LiteListButton.svelte +3 -0
- package/dist/components/buttons/LiteRefreshButton.svelte +3 -0
- package/dist/components/buttons/LiteSaveButton.svelte +3 -0
- package/dist/components/buttons/LiteShowButton.svelte +3 -0
- package/dist/components/compatibility/LiteDirectoryUpload.svelte +4 -3
- package/dist/components/fields/LiteAvatarField.svelte +3 -3
- package/dist/components/fields/LiteCascader.svelte +4 -2
- package/dist/components/fields/LiteDateField.svelte +3 -0
- package/dist/components/fields/LiteDateRangeField.svelte +9 -4
- package/dist/components/fields/LiteFileField.svelte +2 -2
- package/dist/components/fields/LiteTreeSelect.svelte +4 -2
- package/dist/components/pages/LiteListPage.svelte +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lite.css +86 -0
- package/dist/server-adapter.d.ts +15 -15
- package/dist/server-adapter.js +8 -7
- package/package.json +7 -14
- package/dist/polyfill-entry.d.ts +0 -2
- package/dist/polyfill-entry.js +0 -3
- package/dist/polyfill.js +0 -20707
package/README.md
CHANGED
|
@@ -207,24 +207,6 @@ import { createLegacyRedirectHook } from '@svadmin/lite';
|
|
|
207
207
|
export const handle = createLegacyRedirectHook('/lite');
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
### 3b. Optional IE11 polyfill and enhancement
|
|
211
|
-
|
|
212
|
-
The Lite SSR baseline does not require a polyfill: native links, native forms,
|
|
213
|
-
server actions, and full-page navigation work without browser JavaScript. If
|
|
214
|
-
you enable `enhance.js`, load the small ES5 DOM baseline first:
|
|
215
|
-
|
|
216
|
-
```html
|
|
217
|
-
<script src="/polyfill.js"></script>
|
|
218
|
-
<script src="/enhance.js"></script>
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Copy both assets from the package exports `@svadmin/lite/polyfill.js` and
|
|
222
|
-
`@svadmin/lite/enhance.js` into the application's static directory. The
|
|
223
|
-
published polyfill bundles `core-js/stable` and `whatwg-fetch` for the legacy
|
|
224
|
-
enhancement layer. It does not polyfill WebSocket, WASM, Canvas, or the Svelte
|
|
225
|
-
runtime. Those capabilities must continue to use the documented server
|
|
226
|
-
fallback.
|
|
227
|
-
|
|
228
210
|
### 4. Optional legacy-browser metadata
|
|
229
211
|
|
|
230
212
|
```html
|
package/dist/compatibility.js
CHANGED
|
@@ -14,17 +14,17 @@ function hasFunction(record, key) {
|
|
|
14
14
|
}
|
|
15
15
|
function supportsCanvas(documentValue, context) {
|
|
16
16
|
const documentRecord = asRecord(documentValue);
|
|
17
|
-
if (!documentRecord || typeof documentRecord
|
|
17
|
+
if (!documentRecord || typeof documentRecord['createElement'] !== 'function')
|
|
18
18
|
return false;
|
|
19
|
-
const canvas = documentRecord
|
|
19
|
+
const canvas = documentRecord['createElement']('canvas');
|
|
20
20
|
const canvasRecord = asRecord(canvas);
|
|
21
|
-
if (!canvasRecord || typeof canvasRecord
|
|
21
|
+
if (!canvasRecord || typeof canvasRecord['getContext'] !== 'function')
|
|
22
22
|
return false;
|
|
23
|
-
return Boolean(canvasRecord
|
|
23
|
+
return Boolean(canvasRecord['getContext'](context));
|
|
24
24
|
}
|
|
25
25
|
function supportsDirectoryUpload(environment) {
|
|
26
|
-
const inputConstructor = asRecord(environment
|
|
27
|
-
const prototype = asRecord(inputConstructor?.prototype);
|
|
26
|
+
const inputConstructor = asRecord(environment['HTMLInputElement']);
|
|
27
|
+
const prototype = asRecord(inputConstructor?.['prototype']);
|
|
28
28
|
return prototype !== undefined && ('webkitdirectory' in prototype || 'directory' in prototype);
|
|
29
29
|
}
|
|
30
30
|
export const LITE_COMPATIBILITY_CATALOG = [
|
|
@@ -56,34 +56,34 @@ export function detectLiteCapabilities(environment) {
|
|
|
56
56
|
const env = asRecord(explicitEnvironment
|
|
57
57
|
? environment
|
|
58
58
|
: typeof globalThis === 'undefined' ? undefined : globalThis) ?? {};
|
|
59
|
-
const navigator = asRecord(env
|
|
60
|
-
const clipboard = asRecord(navigator?.clipboard);
|
|
61
|
-
const webAssembly = asRecord(env
|
|
62
|
-
const readableStream = asRecord(env
|
|
59
|
+
const navigator = asRecord(env['navigator']);
|
|
60
|
+
const clipboard = asRecord(navigator?.['clipboard']);
|
|
61
|
+
const webAssembly = asRecord(env['WebAssembly']);
|
|
62
|
+
const readableStream = asRecord(env['ReadableStream']);
|
|
63
63
|
return {
|
|
64
|
-
'canvas-2d': supportsCanvas(env
|
|
65
|
-
webgl: supportsCanvas(env
|
|
64
|
+
'canvas-2d': supportsCanvas(env['document'], '2d'),
|
|
65
|
+
webgl: supportsCanvas(env['document'], 'webgl'),
|
|
66
66
|
wasm: hasFunction(webAssembly, 'instantiate'),
|
|
67
67
|
'wasm-streaming': hasFunction(webAssembly, 'instantiateStreaming'),
|
|
68
|
-
websocket: typeof env
|
|
69
|
-
'event-source': typeof env
|
|
70
|
-
worker: typeof env
|
|
68
|
+
websocket: typeof env['WebSocket'] === 'function',
|
|
69
|
+
'event-source': typeof env['EventSource'] === 'function',
|
|
70
|
+
worker: typeof env['Worker'] === 'function',
|
|
71
71
|
'directory-upload': supportsDirectoryUpload(env),
|
|
72
|
-
'file-system-access': typeof env
|
|
73
|
-
|| typeof env
|
|
72
|
+
'file-system-access': typeof env['showOpenFilePicker'] === 'function'
|
|
73
|
+
|| typeof env['showDirectoryPicker'] === 'function',
|
|
74
74
|
clipboard: hasFunction(clipboard, 'writeText'),
|
|
75
|
-
'broadcast-channel': typeof env
|
|
76
|
-
'intersection-observer': typeof env
|
|
77
|
-
'resize-observer': typeof env
|
|
78
|
-
'service-worker': asRecord(navigator?.serviceWorker) !== undefined,
|
|
79
|
-
'indexed-db': asRecord(env
|
|
80
|
-
notifications: typeof env
|
|
81
|
-
'media-capture': hasFunction(asRecord(navigator?.mediaDevices), 'getUserMedia')
|
|
82
|
-
|| typeof env
|
|
83
|
-
'web-rtc': typeof env
|
|
84
|
-
geolocation: asRecord(navigator?.geolocation) !== undefined,
|
|
85
|
-
'web-streams': asRecord(env
|
|
86
|
-
&& (hasFunction(readableStream, 'from') || typeof env
|
|
75
|
+
'broadcast-channel': typeof env['BroadcastChannel'] === 'function',
|
|
76
|
+
'intersection-observer': typeof env['IntersectionObserver'] === 'function',
|
|
77
|
+
'resize-observer': typeof env['ResizeObserver'] === 'function',
|
|
78
|
+
'service-worker': asRecord(navigator?.['serviceWorker']) !== undefined,
|
|
79
|
+
'indexed-db': asRecord(env['indexedDB']) !== undefined,
|
|
80
|
+
notifications: typeof env['Notification'] === 'function',
|
|
81
|
+
'media-capture': hasFunction(asRecord(navigator?.['mediaDevices']), 'getUserMedia')
|
|
82
|
+
|| typeof env['MediaRecorder'] === 'function',
|
|
83
|
+
'web-rtc': typeof env['RTCPeerConnection'] === 'function',
|
|
84
|
+
geolocation: asRecord(navigator?.['geolocation']) !== undefined,
|
|
85
|
+
'web-streams': asRecord(env['ReadableStream']) !== undefined
|
|
86
|
+
&& (hasFunction(readableStream, 'from') || typeof env['WritableStream'] === 'function'),
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
export function resolveLiteCompatibility(capability, support = detectLiteCapabilities()) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FieldDefinition } from '@svadmin/core';
|
|
3
3
|
import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
|
|
4
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
4
5
|
import { isExplicitBooleanTrue } from '../value-normalization';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
@@ -142,10 +143,9 @@
|
|
|
142
143
|
<input type="hidden" name={fieldName(sub.key)} value={reference} />
|
|
143
144
|
{/each}
|
|
144
145
|
{/if}
|
|
145
|
-
<
|
|
146
|
+
<LiteFileInput
|
|
146
147
|
id={fieldId(sub.key)}
|
|
147
148
|
name={fieldName(sub.key)}
|
|
148
|
-
type="file"
|
|
149
149
|
class="lite-input"
|
|
150
150
|
required={sub.required
|
|
151
151
|
&& !isDraft
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
3
|
+
|
|
2
4
|
export interface AnnotationItem {
|
|
3
5
|
id: string;
|
|
4
6
|
type: string;
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
{#if formAction}
|
|
51
53
|
<div class="sv-lite-anno-upload">
|
|
52
54
|
<form method="POST" action={formAction} enctype="multipart/form-data">
|
|
53
|
-
<
|
|
55
|
+
<LiteFileInput name="annotatedImage" accept="image/*" class="sv-lite-file-input" />
|
|
54
56
|
<button type="submit" class="sv-lite-upload-btn">Upload Markup</button>
|
|
55
57
|
</form>
|
|
56
58
|
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = Omit<HTMLInputAttributes, 'type' | 'value' | 'files'> & {
|
|
6
|
+
files?: FileList;
|
|
7
|
+
label?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
webkitdirectory?: boolean;
|
|
10
|
+
directory?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
files = $bindable(),
|
|
15
|
+
label,
|
|
16
|
+
class: className = '',
|
|
17
|
+
...restProps
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
const selectedFiles = $derived(files ? Array.from(files) : []);
|
|
21
|
+
const selectedLabel = $derived.by(() => {
|
|
22
|
+
const firstFile = selectedFiles[0];
|
|
23
|
+
if (firstFile === undefined) return t('common.noFileChosen') || 'No file chosen';
|
|
24
|
+
if (selectedFiles.length === 1) return firstFile.name;
|
|
25
|
+
return t('common.filesSelected', { count: selectedFiles.length }) || `${selectedFiles.length} files selected`;
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<span class={`lite-file-picker ${className}`} data-disabled={restProps.disabled ? 'true' : undefined}>
|
|
30
|
+
<input
|
|
31
|
+
class="lite-file-picker__native"
|
|
32
|
+
type="file"
|
|
33
|
+
bind:files
|
|
34
|
+
{...restProps}
|
|
35
|
+
/>
|
|
36
|
+
<span class="lite-file-picker__visual" aria-hidden="true">
|
|
37
|
+
<span class="lite-file-picker__button">{label ?? (t('common.chooseFile') || 'Choose file')}</span>
|
|
38
|
+
<span class="lite-file-picker__name">{selectedLabel}</span>
|
|
39
|
+
</span>
|
|
40
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = Omit<HTMLInputAttributes, 'type' | 'value' | 'files'> & {
|
|
3
|
+
files?: FileList;
|
|
4
|
+
label?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
webkitdirectory?: boolean;
|
|
7
|
+
directory?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const LiteFileInput: import("svelte").Component<Props, {}, "files">;
|
|
10
|
+
type LiteFileInput = ReturnType<typeof LiteFileInput>;
|
|
11
|
+
export default LiteFileInput;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
|
|
10
10
|
import { isExplicitBooleanTrue } from '../value-normalization';
|
|
11
11
|
import LiteArrayField from './LiteArrayField.svelte';
|
|
12
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
fields: FieldDefinition[];
|
|
@@ -156,8 +157,7 @@
|
|
|
156
157
|
{/if}
|
|
157
158
|
</select>
|
|
158
159
|
{:else if inputType === 'file'}
|
|
159
|
-
<
|
|
160
|
-
type="file"
|
|
160
|
+
<LiteFileInput
|
|
161
161
|
name={field.key}
|
|
162
162
|
id={field.key}
|
|
163
163
|
class="lite-input {hasError ? 'lite-input-error' : ''}"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FieldDefinition } from '@svadmin/core';
|
|
3
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
resourceName: string;
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
<form {action} {method} enctype="multipart/form-data" class="lite-import-form">
|
|
26
27
|
<div class="lite-file-row">
|
|
27
28
|
<label for="import_file" class="lite-label">Select CSV / JSON file:</label>
|
|
28
|
-
<
|
|
29
|
+
<LiteFileInput id="import_file" name="file" accept=".csv,.json" class="lite-input" required />
|
|
29
30
|
</div>
|
|
30
31
|
|
|
31
32
|
{#if fields.length > 0}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
schema: Record<string, unknown>;
|
|
4
6
|
value?: Record<string, unknown>;
|
|
@@ -24,23 +26,23 @@
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const properties = $derived.by<FieldMeta[]>(() => {
|
|
27
|
-
const propsObj = (schema
|
|
28
|
-
const requiredKeys = Array.isArray(schema
|
|
29
|
+
const propsObj = (schema['properties'] ?? {}) as Record<string, Record<string, unknown>>;
|
|
30
|
+
const requiredKeys = Array.isArray(schema['required']) ? (schema['required'] as string[]) : [];
|
|
29
31
|
|
|
30
|
-
return Object.entries(propsObj).map(([key, propDef]) => ({
|
|
32
|
+
return Object.entries(propsObj).map(([key, propDef]) => (definedOptions({
|
|
31
33
|
key,
|
|
32
|
-
title: String(propDef
|
|
33
|
-
type: String(propDef
|
|
34
|
-
enum: Array.isArray(propDef
|
|
34
|
+
title: String(propDef['title'] ?? key),
|
|
35
|
+
type: String(propDef['type'] ?? 'string'),
|
|
36
|
+
enum: Array.isArray(propDef['enum']) ? (propDef['enum'] as Array<string | number>) : undefined,
|
|
35
37
|
required: requiredKeys.includes(key),
|
|
36
|
-
}));
|
|
38
|
+
})));
|
|
37
39
|
});
|
|
38
40
|
</script>
|
|
39
41
|
|
|
40
42
|
<div class="lite-schema-form-card">
|
|
41
|
-
{#if schema
|
|
43
|
+
{#if schema['title']}
|
|
42
44
|
<div class="lite-schema-form-header">
|
|
43
|
-
<strong>{schema
|
|
45
|
+
<strong>{schema['title']}</strong>
|
|
44
46
|
</div>
|
|
45
47
|
{/if}
|
|
46
48
|
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
{@const isSelected = String(item[idKey]) === String(activeItem?.[idKey])}
|
|
34
34
|
<li class="lite-master-item {isSelected ? 'lite-master-item-active' : ''}">
|
|
35
35
|
<a href="?selectedId={item[idKey]}" class="lite-master-link">
|
|
36
|
-
{item[titleKey] ?? item
|
|
36
|
+
{item[titleKey] ?? item['name'] ?? `Item #${item[idKey]}`}
|
|
37
37
|
</a>
|
|
38
38
|
</li>
|
|
39
39
|
{/each}
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
{@render detailSnippet(activeItem)}
|
|
48
48
|
{:else}
|
|
49
49
|
<div class="lite-detail-content">
|
|
50
|
-
<h4 style="margin: 0 0 12px 0;">{activeItem[titleKey] ?? activeItem
|
|
50
|
+
<h4 style="margin: 0 0 12px 0;">{activeItem[titleKey] ?? activeItem['name'] ?? `Item #${activeItem[idKey]}`}</h4>
|
|
51
51
|
<table class="lite-table">
|
|
52
52
|
<tbody>
|
|
53
53
|
{#each Object.entries(activeItem) as [k, v] (k)}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
4
|
+
|
|
2
5
|
interface LiteMediaItem {
|
|
3
6
|
id: string;
|
|
4
7
|
name: string;
|
|
@@ -33,9 +36,9 @@
|
|
|
33
36
|
<!-- Upload Section -->
|
|
34
37
|
<div class="lite-media-upload-row">
|
|
35
38
|
<label for="media_upload_input" class="lite-label">Upload New Media:</label>
|
|
36
|
-
<
|
|
39
|
+
<LiteFileInput id="media_upload_input" name="files" multiple={multiple} class="lite-file-input" />
|
|
37
40
|
<button type="submit" name="_media_action" value="upload" class="lite-btn lite-btn-sm lite-btn-outline" style="margin-left: 8px;">
|
|
38
|
-
Upload File
|
|
41
|
+
{t('common.uploadFile') || 'Upload File'}
|
|
39
42
|
</button>
|
|
40
43
|
</div>
|
|
41
44
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
return val.length >= 10 ? `${val.slice(0, 6)}********${val.slice(-4)}` : '********';
|
|
21
21
|
case 'email': {
|
|
22
22
|
const parts = val.split('@');
|
|
23
|
-
|
|
23
|
+
const [name, domain] = parts;
|
|
24
|
+
return parts.length === 2 && name && domain ? `${name.slice(0, 1)}***@${domain}` : '***@***.***';
|
|
24
25
|
}
|
|
25
26
|
default:
|
|
26
27
|
return '••••••••';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* LiteShowField — SSR-compatible field renderer for detail views.
|
|
4
6
|
* Renders a single field value based on its type definition.
|
|
@@ -50,13 +52,13 @@
|
|
|
50
52
|
{:else if field.type === "avatar"}
|
|
51
53
|
<LiteAvatarField {field} value={value as unknown} mode="show" />
|
|
52
54
|
{:else if field.type === "rating"}
|
|
53
|
-
<LiteRatingField {field}
|
|
55
|
+
<LiteRatingField {field} {...definedOptions({ "value": value as string | number | null | undefined })} mode="show" />
|
|
54
56
|
{:else if field.type === "code"}
|
|
55
|
-
<LiteCodeField {field}
|
|
57
|
+
<LiteCodeField {field} {...definedOptions({ "value": value as string | number | Record<string, unknown> | unknown[] | null | undefined })} mode="show" />
|
|
56
58
|
{:else if field.type === "currency"}
|
|
57
|
-
<LiteCurrencyField {field}
|
|
59
|
+
<LiteCurrencyField {field} {...definedOptions({ "value": value as string | number | null | undefined })} mode="show" />
|
|
58
60
|
{:else if field.type === "phone"}
|
|
59
|
-
<LitePhoneField {field}
|
|
61
|
+
<LitePhoneField {field} {...definedOptions({ "value": value as string | number | null | undefined })} mode="show" />
|
|
60
62
|
{:else if field.type === "password"}
|
|
61
63
|
<span>••••••••</span>
|
|
62
64
|
{:else if field.type === "color" && value}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import LiteFileInput from './LiteFileInput.svelte';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
name?: string;
|
|
4
6
|
value?: string;
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
|
|
37
39
|
<div class="sv-lite-sig-actions">
|
|
38
40
|
<form method="POST" action={formAction} enctype="multipart/form-data">
|
|
39
|
-
<
|
|
41
|
+
<LiteFileInput name={name} accept="image/*" {disabled} class="sv-lite-sig-file" />
|
|
40
42
|
<button type="submit" {disabled} class="sv-lite-sig-btn">Upload Signature</button>
|
|
41
43
|
</form>
|
|
42
44
|
</div>
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
let { status = 'idle', message }: Props = $props();
|
|
13
13
|
|
|
14
|
-
const statusConfig: Record<
|
|
14
|
+
const statusConfig: Record<NonNullable<Props['status']>, { icon: string; color: string; text: string }> = {
|
|
15
15
|
idle: { icon: '☁', color: '#64748b', text: 'All changes saved' },
|
|
16
16
|
saved: { icon: '✓', color: '#10b981', text: 'Saved' },
|
|
17
17
|
error: { icon: '✕', color: '#ef4444', text: 'Save failed' },
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const cfg = $derived(statusConfig[status] ?? statusConfig
|
|
20
|
+
const cfg = $derived(statusConfig[status] ?? statusConfig['idle']);
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
23
|
<span class="lite-inline-sm" style="display: inline-flex; align-items: center; font-size: 13px; color: {cfg.color};">
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
let { message, type = 'info', dismissUrl }: Props = $props();
|
|
15
15
|
|
|
16
|
-
const colors: Record<
|
|
16
|
+
const colors: Record<NonNullable<Props['type']>, { bg: string; border: string; text: string }> = {
|
|
17
17
|
success: { bg: '#ecfdf5', border: '#a7f3d0', text: '#065f46' },
|
|
18
18
|
error: { bg: '#fef2f2', border: '#fecaca', text: '#991b1b' },
|
|
19
19
|
warning: { bg: '#fffbeb', border: '#fed7aa', text: '#92400e' },
|
|
20
20
|
info: { bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af' },
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const style = $derived(colors[type] ?? colors
|
|
23
|
+
const style = $derived(colors[type] ?? colors['info']);
|
|
24
24
|
</script>
|
|
25
25
|
|
|
26
26
|
<div
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* SSR VirtualTable — Identical to LiteTable.
|
|
4
6
|
* Virtual scrolling (rendering only visible rows) requires JS.
|
|
@@ -37,11 +39,10 @@
|
|
|
37
39
|
<LiteTable
|
|
38
40
|
{records}
|
|
39
41
|
{resource}
|
|
40
|
-
{currentSort}
|
|
42
|
+
{...definedOptions({ "currentSort": currentSort })}
|
|
41
43
|
{currentOrder}
|
|
42
|
-
{currentSearch}
|
|
44
|
+
{...definedOptions({ "currentSearch": currentSearch })}
|
|
43
45
|
{basePath}
|
|
44
|
-
{canShow}
|
|
45
|
-
{canEdit}
|
|
46
|
-
{canDelete}
|
|
46
|
+
{...definedOptions({ "canShow": canShow })}
|
|
47
|
+
{...definedOptions({ canEdit, canDelete })}
|
|
47
48
|
/>
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
<span id={`${confirmationId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
33
33
|
<a
|
|
34
34
|
href={`#${confirmationId}`}
|
|
35
|
+
data-svadmin-button
|
|
36
|
+
data-variant="destructive"
|
|
37
|
+
data-size={size}
|
|
35
38
|
class="lite-btn lite-btn-danger {size === 'sm' ? 'lite-btn-sm' : ''}"
|
|
36
39
|
title={t('common.delete') || 'Delete'}
|
|
37
40
|
aria-controls={confirmationId}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
3
|
import { Upload } from '@lucide/svelte';
|
|
4
4
|
import { liteFragmentId } from '../../fragment-id';
|
|
5
|
+
import LiteFileInput from '../LiteFileInput.svelte';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
resource: string;
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
<span id={`${importPanelId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
29
30
|
<a
|
|
30
31
|
href={`#${importPanelId}`}
|
|
32
|
+
data-svadmin-button
|
|
33
|
+
data-variant="outline"
|
|
34
|
+
data-size={size}
|
|
31
35
|
class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''}"
|
|
32
36
|
title={t('common.import') || 'Import'}
|
|
33
37
|
aria-controls={importPanelId}
|
|
@@ -41,7 +45,7 @@
|
|
|
41
45
|
<div id={importPanelId} class="lite-confirm-panel lite-confirm-target" role="dialog" aria-labelledby={importPanelTitleId} tabindex="-1">
|
|
42
46
|
<p id={importPanelTitleId} style="margin: 0 0 8px; font-size: 13px;">{t('common.importData') || 'Import data (CSV/JSON)'}</p>
|
|
43
47
|
<form method="POST" action={`${basePath}/${resource}?/${resource}_import`} enctype="multipart/form-data" class="lite-stack-sm">
|
|
44
|
-
<
|
|
48
|
+
<LiteFileInput name="file" accept=".csv,.json" required class="lite-import-file-input" />
|
|
45
49
|
<div class="lite-inline-actions lite-justify-end">
|
|
46
50
|
<a href={`#${importPanelId}-closed`} class="lite-btn lite-btn-sm">
|
|
47
51
|
{t('common.cancel') || 'Cancel'}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import LiteFileInput from '../LiteFileInput.svelte';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
name?: string;
|
|
4
6
|
label?: string;
|
|
@@ -26,10 +28,9 @@
|
|
|
26
28
|
<p class="lite-muted">{helpText}</p>
|
|
27
29
|
<div class="lite-form-group">
|
|
28
30
|
<label for={controlId}>{label}</label>
|
|
29
|
-
<
|
|
31
|
+
<LiteFileInput
|
|
30
32
|
id={controlId}
|
|
31
33
|
class="lite-input"
|
|
32
|
-
type="file"
|
|
33
34
|
{name}
|
|
34
35
|
{accept}
|
|
35
36
|
{disabled}
|
|
@@ -39,6 +40,6 @@
|
|
|
39
40
|
</div>
|
|
40
41
|
<div class="lite-form-group">
|
|
41
42
|
<label for={`${controlId}-zip`}>ZIP archive fallback</label>
|
|
42
|
-
<
|
|
43
|
+
<LiteFileInput id={`${controlId}-zip`} class="lite-input" name={zipName} accept=".zip,application/zip" {disabled} />
|
|
43
44
|
</div>
|
|
44
45
|
</fieldset>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
46
|
if (value && typeof value === 'object' && 'src' in (value as Record<string, unknown>)) {
|
|
47
|
-
return String((value as Record<string, unknown>)
|
|
47
|
+
return String((value as Record<string, unknown>)['src'] ?? '');
|
|
48
48
|
}
|
|
49
49
|
return null;
|
|
50
50
|
});
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
return value;
|
|
56
56
|
}
|
|
57
57
|
if (value && typeof value === 'object' && 'name' in (value as Record<string, unknown>)) {
|
|
58
|
-
return String((value as Record<string, unknown>)
|
|
58
|
+
return String((value as Record<string, unknown>)['name'] ?? '');
|
|
59
59
|
}
|
|
60
60
|
return null;
|
|
61
61
|
});
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
if (!trimmed) return '?';
|
|
66
66
|
const parts = trimmed.split(/\s+/);
|
|
67
67
|
if (parts.length >= 2) {
|
|
68
|
-
return `${
|
|
68
|
+
return `${trimmed.charAt(0)}${parts.at(-1)?.charAt(0) ?? ''}`.toUpperCase();
|
|
69
69
|
}
|
|
70
70
|
return trimmed.slice(0, 2).toUpperCase();
|
|
71
71
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
import type { FieldDefinition } from '@svadmin/core';
|
|
3
5
|
import { t } from '@svadmin/core/i18n';
|
|
4
6
|
|
|
@@ -59,11 +61,11 @@
|
|
|
59
61
|
if (node.children && node.children.length > 0) {
|
|
60
62
|
paths.push(...generatePaths(node.children, nextValues, nextLabels));
|
|
61
63
|
} else {
|
|
62
|
-
paths.push({
|
|
64
|
+
paths.push(definedOptions({
|
|
63
65
|
pathValue: nextValues.join('/'),
|
|
64
66
|
pathLabels: nextLabels,
|
|
65
67
|
disabled: node.disabled,
|
|
66
|
-
});
|
|
68
|
+
}));
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
return paths;
|