@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.
Files changed (44) hide show
  1. package/README.md +0 -18
  2. package/dist/compatibility.js +29 -29
  3. package/dist/components/LiteArrayItem.svelte +2 -2
  4. package/dist/components/LiteCanvasAnnotation.svelte +3 -1
  5. package/dist/components/LiteFileInput.svelte +40 -0
  6. package/dist/components/LiteFileInput.svelte.d.ts +11 -0
  7. package/dist/components/LiteForm.svelte +2 -2
  8. package/dist/components/LiteImportWizard.svelte +2 -1
  9. package/dist/components/LiteJsonSchemaForm.svelte +11 -9
  10. package/dist/components/LiteMasterDetailView.svelte +2 -2
  11. package/dist/components/LiteMediaLibraryModal.svelte +5 -2
  12. package/dist/components/LiteSensitiveDataMask.svelte +2 -1
  13. package/dist/components/LiteShowField.svelte +6 -4
  14. package/dist/components/LiteSignaturePad.svelte +3 -1
  15. package/dist/components/advanced/LiteAutoSaveIndicator.svelte +2 -2
  16. package/dist/components/advanced/LiteToast.svelte +2 -2
  17. package/dist/components/advanced/LiteVirtualTable.svelte +6 -5
  18. package/dist/components/buttons/LiteCloneButton.svelte +3 -0
  19. package/dist/components/buttons/LiteCreateButton.svelte +3 -0
  20. package/dist/components/buttons/LiteDeleteButton.svelte +3 -0
  21. package/dist/components/buttons/LiteEditButton.svelte +3 -0
  22. package/dist/components/buttons/LiteExportButton.svelte +3 -0
  23. package/dist/components/buttons/LiteImportButton.svelte +5 -1
  24. package/dist/components/buttons/LiteListButton.svelte +3 -0
  25. package/dist/components/buttons/LiteRefreshButton.svelte +3 -0
  26. package/dist/components/buttons/LiteSaveButton.svelte +3 -0
  27. package/dist/components/buttons/LiteShowButton.svelte +3 -0
  28. package/dist/components/compatibility/LiteDirectoryUpload.svelte +4 -3
  29. package/dist/components/fields/LiteAvatarField.svelte +3 -3
  30. package/dist/components/fields/LiteCascader.svelte +4 -2
  31. package/dist/components/fields/LiteDateField.svelte +3 -0
  32. package/dist/components/fields/LiteDateRangeField.svelte +9 -4
  33. package/dist/components/fields/LiteFileField.svelte +2 -2
  34. package/dist/components/fields/LiteTreeSelect.svelte +4 -2
  35. package/dist/components/pages/LiteListPage.svelte +4 -2
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/lite.css +86 -0
  39. package/dist/server-adapter.d.ts +15 -15
  40. package/dist/server-adapter.js +8 -7
  41. package/package.json +7 -14
  42. package/dist/polyfill-entry.d.ts +0 -2
  43. package/dist/polyfill-entry.js +0 -3
  44. 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
@@ -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.createElement !== 'function')
17
+ if (!documentRecord || typeof documentRecord['createElement'] !== 'function')
18
18
  return false;
19
- const canvas = documentRecord.createElement('canvas');
19
+ const canvas = documentRecord['createElement']('canvas');
20
20
  const canvasRecord = asRecord(canvas);
21
- if (!canvasRecord || typeof canvasRecord.getContext !== 'function')
21
+ if (!canvasRecord || typeof canvasRecord['getContext'] !== 'function')
22
22
  return false;
23
- return Boolean(canvasRecord.getContext(context));
23
+ return Boolean(canvasRecord['getContext'](context));
24
24
  }
25
25
  function supportsDirectoryUpload(environment) {
26
- const inputConstructor = asRecord(environment.HTMLInputElement);
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.navigator);
60
- const clipboard = asRecord(navigator?.clipboard);
61
- const webAssembly = asRecord(env.WebAssembly);
62
- const readableStream = asRecord(env.ReadableStream);
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.document, '2d'),
65
- webgl: supportsCanvas(env.document, 'webgl'),
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.WebSocket === 'function',
69
- 'event-source': typeof env.EventSource === 'function',
70
- worker: typeof env.Worker === 'function',
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.showOpenFilePicker === 'function'
73
- || typeof env.showDirectoryPicker === 'function',
72
+ 'file-system-access': typeof env['showOpenFilePicker'] === 'function'
73
+ || typeof env['showDirectoryPicker'] === 'function',
74
74
  clipboard: hasFunction(clipboard, 'writeText'),
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'),
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
- <input
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
- <input type="file" name="annotatedImage" accept="image/*" class="sv-lite-file-input" />
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
- <input
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
- <input type="file" id="import_file" name="file" accept=".csv,.json" class="lite-input" required />
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.properties ?? {}) as Record<string, Record<string, unknown>>;
28
- const requiredKeys = Array.isArray(schema.required) ? (schema.required as string[]) : [];
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.title ?? key),
33
- type: String(propDef.type ?? 'string'),
34
- enum: Array.isArray(propDef.enum) ? (propDef.enum as Array<string | number>) : undefined,
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.title}
43
+ {#if schema['title']}
42
44
  <div class="lite-schema-form-header">
43
- <strong>{schema.title}</strong>
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.name ?? `Item #${item[idKey]}`}
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.name ?? `Item #${activeItem[idKey]}`}</h4>
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
- <input id="media_upload_input" type="file" name="files" multiple={multiple} class="lite-file-input" />
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
- return parts.length === 2 ? `${parts[0].slice(0, 1)}***@${parts[1]}` : '***@***.***';
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} value={value as string | number | null | undefined} mode="show" />
55
+ <LiteRatingField {field} {...definedOptions({ "value": value as string | number | null | undefined })} mode="show" />
54
56
  {:else if field.type === "code"}
55
- <LiteCodeField {field} value={value as string | number | Record<string, unknown> | unknown[] | null | undefined} mode="show" />
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} value={value as string | number | null | undefined} mode="show" />
59
+ <LiteCurrencyField {field} {...definedOptions({ "value": value as string | number | null | undefined })} mode="show" />
58
60
  {:else if field.type === "phone"}
59
- <LitePhoneField {field} value={value as string | number | null | undefined} mode="show" />
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
- <input type="file" name={name} accept="image/*" {disabled} class="sv-lite-sig-file" />
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<string, { icon: string; color: string; text: string }> = {
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.idle);
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<string, { bg: string; border: string; text: string }> = {
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.info);
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
  />
@@ -23,6 +23,9 @@
23
23
 
24
24
  <a
25
25
  href={`${basePath}/${resource}/clone/${recordItemId}`}
26
+ data-svadmin-button
27
+ data-variant="ghost"
28
+ data-size={size}
26
29
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
30
  title={t('common.clone') || 'Clone'}
28
31
  >
@@ -21,6 +21,9 @@
21
21
 
22
22
  <a
23
23
  href={`${basePath}/${resource}/create`}
24
+ data-svadmin-button
25
+ data-variant="default"
26
+ data-size={size}
24
27
  class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
28
  title={t('common.create') || 'Create'}
26
29
  >
@@ -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}
@@ -23,6 +23,9 @@
23
23
 
24
24
  <a
25
25
  href={`${basePath}/${resource}/edit/${recordItemId}`}
26
+ data-svadmin-button
27
+ data-variant="ghost"
28
+ data-size={size}
26
29
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
30
  title={t('common.edit') || 'Edit'}
28
31
  >
@@ -21,6 +21,9 @@
21
21
 
22
22
  <a
23
23
  href={`${basePath}/${resource}?action=export`}
24
+ data-svadmin-button
25
+ data-variant="ghost"
26
+ data-size={size}
24
27
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
28
  title={t('common.export') || 'Export'}
26
29
  >
@@ -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
- <input type="file" name="file" accept=".csv,.json" required style="font-size: 13px;" />
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'}
@@ -21,6 +21,9 @@
21
21
 
22
22
  <a
23
23
  href={`${basePath}/${resource}`}
24
+ data-svadmin-button
25
+ data-variant="ghost"
26
+ data-size={size}
24
27
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
28
  title={t('common.list') || 'List'}
26
29
  >
@@ -20,6 +20,9 @@
20
20
  <!-- svelte-ignore a11y_invalid_attribute -->
21
21
  <a
22
22
  href=""
23
+ data-svadmin-button
24
+ data-variant="ghost"
25
+ data-size={size}
23
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
24
27
  title={t('common.refresh') || 'Refresh'}
25
28
  >
@@ -17,6 +17,9 @@
17
17
 
18
18
  <button
19
19
  type="submit"
20
+ data-svadmin-button
21
+ data-variant="default"
22
+ data-size={size}
20
23
  class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
21
24
  title={t('common.save') || 'Save'}
22
25
  >
@@ -23,6 +23,9 @@
23
23
 
24
24
  <a
25
25
  href={`${basePath}/${resource}/show/${recordItemId}`}
26
+ data-svadmin-button
27
+ data-variant="ghost"
28
+ data-size={size}
26
29
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
30
  title={t('common.show') || 'Show'}
28
31
  >
@@ -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
- <input
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
- <input id={`${controlId}-zip`} class="lite-input" type="file" name={zipName} accept=".zip,application/zip" {disabled} />
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>).src ?? '');
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>).name ?? '');
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 `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
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;