@svadmin/lite 0.10.1 → 0.13.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 +1 -19
- 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/LiteFileUpload.svelte +53 -0
- package/dist/components/LiteFileUpload.svelte.d.ts +16 -0
- package/dist/components/LiteFilterBuilder.svelte +107 -153
- package/dist/components/LiteFilterBuilder.svelte.d.ts +0 -1
- package/dist/components/LiteForm.svelte +2 -2
- package/dist/components/LiteImportWizard.svelte +2 -1
- package/dist/components/LiteJsonSchemaForm.svelte +136 -70
- package/dist/components/LiteJsonSchemaForm.svelte.d.ts +12 -2
- 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/filter-form.d.ts +3 -0
- package/dist/filter-form.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lite.css +86 -0
- package/dist/server-adapter.d.ts +15 -15
- package/dist/server-adapter.js +62 -8
- 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
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
interface JsonSchema {
|
|
3
|
+
type?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
properties?: Record<string, JsonSchema>;
|
|
7
|
+
items?: JsonSchema;
|
|
8
|
+
enum?: Array<string | number | boolean | null>;
|
|
9
|
+
default?: unknown;
|
|
10
|
+
required?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
2
13
|
interface Props {
|
|
3
|
-
schema:
|
|
14
|
+
schema: JsonSchema;
|
|
4
15
|
value?: Record<string, unknown>;
|
|
5
16
|
action?: string;
|
|
6
17
|
method?: 'get' | 'post' | 'dialog' | 'GET' | 'POST' | 'DIALOG' | null;
|
|
@@ -9,94 +20,149 @@
|
|
|
9
20
|
|
|
10
21
|
let {
|
|
11
22
|
schema,
|
|
12
|
-
value = {},
|
|
23
|
+
value = $bindable({}),
|
|
13
24
|
action = '',
|
|
14
25
|
method = 'POST',
|
|
15
26
|
submitText = 'Submit Form',
|
|
16
27
|
}: Props = $props();
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
function cloneDefault(node: JsonSchema): unknown {
|
|
30
|
+
if (node.default !== undefined) return structuredClone(node.default);
|
|
31
|
+
if (node.type === 'object' || node.properties) {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(node.properties ?? {})
|
|
34
|
+
.map(([key, child]) => [key, cloneDefault(child)])
|
|
35
|
+
.filter(([, child]) => child !== undefined),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (node.type === 'array') return [];
|
|
39
|
+
return undefined;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
function mergeDefaults(node: JsonSchema, current: unknown): unknown {
|
|
43
|
+
if (node.type === 'array') {
|
|
44
|
+
if (!Array.isArray(current)) return cloneDefault(node);
|
|
45
|
+
return current.map(item => node.items ? mergeDefaults(node.items, item) : item);
|
|
46
|
+
}
|
|
47
|
+
if (node.type !== 'object' && !node.properties) return current ?? cloneDefault(node);
|
|
48
|
+
const source = current && typeof current === 'object' && !Array.isArray(current)
|
|
49
|
+
? current as Record<string, unknown>
|
|
50
|
+
: {};
|
|
51
|
+
const result: Record<string, unknown> = { ...source };
|
|
52
|
+
for (const [key, child] of Object.entries(node.properties ?? {})) {
|
|
53
|
+
const next = mergeDefaults(child, result[key]);
|
|
54
|
+
if (next !== undefined) result[key] = next;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
29
58
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type: String(propDef.type ?? 'string'),
|
|
34
|
-
enum: Array.isArray(propDef.enum) ? (propDef.enum as Array<string | number>) : undefined,
|
|
35
|
-
required: requiredKeys.includes(key),
|
|
36
|
-
}));
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const next = mergeDefaults(schema, value);
|
|
61
|
+
if (JSON.stringify(next) !== JSON.stringify(value)) value = next as Record<string, unknown>;
|
|
37
62
|
});
|
|
63
|
+
|
|
64
|
+
function readPath(path: string[]): unknown {
|
|
65
|
+
return path.reduce<unknown>((current, key) => (
|
|
66
|
+
current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined
|
|
67
|
+
), value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writePath(path: string[], nextValue: unknown): void {
|
|
71
|
+
const next = structuredClone(value);
|
|
72
|
+
let target: Record<string, unknown> = next;
|
|
73
|
+
for (const key of path.slice(0, -1)) {
|
|
74
|
+
const child = target[key];
|
|
75
|
+
target[key] = child && typeof child === 'object' ? child : {};
|
|
76
|
+
target = target[key] as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
const lastKey = path[path.length - 1];
|
|
79
|
+
if (lastKey !== undefined) target[lastKey] = nextValue;
|
|
80
|
+
value = next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function enumValue(raw: string, options: JsonSchema['enum']): unknown {
|
|
84
|
+
return options?.find(option => String(option) === raw) ?? raw;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addArrayItem(path: string[], itemSchema: JsonSchema): void {
|
|
88
|
+
const current = readPath(path);
|
|
89
|
+
const items = Array.isArray(current) ? current : [];
|
|
90
|
+
writePath(path, [...items, cloneDefault(itemSchema) ?? (itemSchema.type === 'object' ? {} : '')]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeArrayItem(path: string[], index: number): void {
|
|
94
|
+
const current = readPath(path);
|
|
95
|
+
if (Array.isArray(current)) writePath(path, current.filter((_, itemIndex) => itemIndex !== index));
|
|
96
|
+
}
|
|
38
97
|
</script>
|
|
39
98
|
|
|
40
99
|
<div class="lite-schema-form-card">
|
|
41
|
-
{#if schema.title}
|
|
42
|
-
|
|
43
|
-
<strong>{schema.title}</strong>
|
|
44
|
-
</div>
|
|
45
|
-
{/if}
|
|
100
|
+
{#if schema.title}<div class="lite-schema-form-header"><strong>{schema.title}</strong></div>{/if}
|
|
101
|
+
{#if schema.description}<p class="lite-schema-description">{schema.description}</p>{/if}
|
|
46
102
|
|
|
47
103
|
<form {action} {method} class="lite-schema-form">
|
|
48
|
-
{#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
{#snippet renderField(node: JsonSchema, path: string[], title: string, required = false)}
|
|
105
|
+
{@const current = readPath(path)}
|
|
106
|
+
{@const id = `lite_json_${path.join('_')}`}
|
|
107
|
+
{#if node.type === 'object' || node.properties}
|
|
108
|
+
<fieldset class="lite-schema-fieldset">
|
|
109
|
+
<legend>{title}</legend>
|
|
110
|
+
{#if node.description}<p class="lite-schema-description">{node.description}</p>{/if}
|
|
111
|
+
{#each Object.entries(node.properties ?? {}) as [key, child] (key)}
|
|
112
|
+
{@render renderField(child, [...path, key], child.title ?? key, node.required?.includes(key) ?? false)}
|
|
113
|
+
{/each}
|
|
114
|
+
</fieldset>
|
|
115
|
+
{:else if node.type === 'array'}
|
|
116
|
+
{@const items = Array.isArray(current) ? current : []}
|
|
117
|
+
<fieldset class="lite-schema-fieldset">
|
|
118
|
+
<legend>{title}</legend>
|
|
119
|
+
{#if node.description}<p class="lite-schema-description">{node.description}</p>{/if}
|
|
120
|
+
{#each items as _, index (index)}
|
|
121
|
+
{@render renderField(node.items ?? { type: 'string' }, [...path, String(index)], `${title} ${index + 1}`)}
|
|
122
|
+
<button type="button" class="lite-btn lite-btn-sm" onclick={() => removeArrayItem(path, index)}>Remove</button>
|
|
123
|
+
{:else}
|
|
124
|
+
<span class="lite-hint">No items</span>
|
|
125
|
+
{/each}
|
|
126
|
+
<button type="button" class="lite-btn lite-btn-sm" onclick={() => addArrayItem(path, node.items ?? { type: 'string' })}>Add item</button>
|
|
127
|
+
</fieldset>
|
|
128
|
+
{:else}
|
|
129
|
+
<div class="lite-schema-field">
|
|
130
|
+
<label for={id} class="lite-label">{title}{#if required}<span aria-hidden="true">*</span>{/if}</label>
|
|
131
|
+
{#if node.description}<p class="lite-schema-description">{node.description}</p>{/if}
|
|
132
|
+
{#if node.enum}
|
|
133
|
+
<select id={id} required={required} value={String(current ?? '')} onchange={(event) => writePath(path, enumValue(event.currentTarget.value, node.enum))} class="lite-select">
|
|
134
|
+
<option value="">Select an option</option>
|
|
135
|
+
{#each node.enum as option (String(option))}<option value={String(option)}>{String(option)}</option>{/each}
|
|
136
|
+
</select>
|
|
137
|
+
{:else if node.type === 'boolean'}
|
|
138
|
+
<input id={id} type="checkbox" checked={Boolean(current)} onchange={(event) => writePath(path, event.currentTarget.checked)} />
|
|
139
|
+
{:else if node.type === 'number' || node.type === 'integer'}
|
|
140
|
+
<input id={id} type="number" required={required} value={current === undefined ? '' : String(current)} oninput={(event) => writePath(path, event.currentTarget.value === '' ? undefined : Number(event.currentTarget.value))} class="lite-input" />
|
|
141
|
+
{:else}
|
|
142
|
+
<input id={id} type="text" required={required} value={String(current ?? '')} oninput={(event) => writePath(path, event.currentTarget.value)} class="lite-input" />
|
|
143
|
+
{/if}
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
{/snippet}
|
|
70
147
|
|
|
148
|
+
{#each Object.entries(schema.properties ?? {}) as [key, node] (key)}
|
|
149
|
+
{@render renderField(node, [key], node.title ?? key, schema.required?.includes(key) ?? false)}
|
|
150
|
+
{/each}
|
|
71
151
|
<div class="lite-schema-footer">
|
|
72
|
-
<button type="submit" class="lite-btn lite-btn-primary lite-btn-sm">
|
|
73
|
-
{submitText}
|
|
74
|
-
</button>
|
|
152
|
+
<button type="submit" class="lite-btn lite-btn-primary lite-btn-sm">{submitText}</button>
|
|
75
153
|
</div>
|
|
76
154
|
</form>
|
|
77
155
|
</div>
|
|
78
156
|
|
|
79
157
|
<style>
|
|
80
|
-
.lite-schema-form-card {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
.lite-schema-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
margin-bottom: 12px;
|
|
91
|
-
font-size: 14px;
|
|
92
|
-
}
|
|
93
|
-
.lite-schema-field {
|
|
94
|
-
margin-bottom: 12px;
|
|
95
|
-
}
|
|
96
|
-
.lite-schema-footer {
|
|
97
|
-
display: flex;
|
|
98
|
-
justify-content: flex-end;
|
|
99
|
-
padding-top: 12px;
|
|
100
|
-
border-top: 1px solid #e2e8f0;
|
|
101
|
-
}
|
|
158
|
+
.lite-schema-form-card { background: #fff; border: 1px solid #cbd5e1; border-radius: 6px; padding: 16px; margin-bottom: 16px; }
|
|
159
|
+
.lite-schema-form-header { padding-bottom: 8px; border-bottom: 1px solid #e2e8f0; margin-bottom: 12px; font-size: 14px; }
|
|
160
|
+
.lite-schema-fieldset { border: 1px solid #e2e8f0; border-radius: 4px; padding: 12px; margin: 0 0 12px; }
|
|
161
|
+
.lite-schema-field { margin-bottom: 12px; }
|
|
162
|
+
.lite-schema-fieldset > .lite-schema-field:last-child { margin-bottom: 0; }
|
|
163
|
+
.lite-schema-footer { display: flex; justify-content: flex-end; padding-top: 12px; border-top: 1px solid #e2e8f0; }
|
|
164
|
+
.lite-label { display: block; font-size: 12px; color: #475569; margin-bottom: 4px; }
|
|
165
|
+
.lite-schema-description, .lite-hint { font-size: 12px; color: #64748b; }
|
|
166
|
+
.lite-input, .lite-select { width: 100%; }
|
|
167
|
+
.lite-btn { margin-right: 6px; }
|
|
102
168
|
</style>
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
interface JsonSchema {
|
|
2
|
+
type?: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
properties?: Record<string, JsonSchema>;
|
|
6
|
+
items?: JsonSchema;
|
|
7
|
+
enum?: Array<string | number | boolean | null>;
|
|
8
|
+
default?: unknown;
|
|
9
|
+
required?: string[];
|
|
10
|
+
}
|
|
1
11
|
interface Props {
|
|
2
|
-
schema:
|
|
12
|
+
schema: JsonSchema;
|
|
3
13
|
value?: Record<string, unknown>;
|
|
4
14
|
action?: string;
|
|
5
15
|
method?: 'get' | 'post' | 'dialog' | 'GET' | 'POST' | 'DIALOG' | null;
|
|
6
16
|
submitText?: string;
|
|
7
17
|
}
|
|
8
|
-
declare const LiteJsonSchemaForm: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
declare const LiteJsonSchemaForm: import("svelte").Component<Props, {}, "value">;
|
|
9
19
|
type LiteJsonSchemaForm = ReturnType<typeof LiteJsonSchemaForm>;
|
|
10
20
|
export default LiteJsonSchemaForm;
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FieldDefinition } from '@svadmin/core';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
field: FieldDefinition;
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
let { field, value, error = [], mode = 'show' }: Props = $props();
|
|
13
|
+
const i18n = useTranslation();
|
|
12
14
|
let hasError = $derived(error.length > 0);
|
|
13
15
|
|
|
14
16
|
function formatDate(v: unknown): string {
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
<div>
|
|
37
39
|
<input
|
|
38
40
|
type="datetime-local"
|
|
41
|
+
lang={i18n.locale}
|
|
39
42
|
name={field.key}
|
|
40
43
|
id={field.key}
|
|
41
44
|
value={formatDate(value)}
|