@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.
Files changed (51) hide show
  1. package/README.md +1 -19
  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/LiteFileUpload.svelte +53 -0
  8. package/dist/components/LiteFileUpload.svelte.d.ts +16 -0
  9. package/dist/components/LiteFilterBuilder.svelte +107 -153
  10. package/dist/components/LiteFilterBuilder.svelte.d.ts +0 -1
  11. package/dist/components/LiteForm.svelte +2 -2
  12. package/dist/components/LiteImportWizard.svelte +2 -1
  13. package/dist/components/LiteJsonSchemaForm.svelte +136 -70
  14. package/dist/components/LiteJsonSchemaForm.svelte.d.ts +12 -2
  15. package/dist/components/LiteMasterDetailView.svelte +2 -2
  16. package/dist/components/LiteMediaLibraryModal.svelte +5 -2
  17. package/dist/components/LiteSensitiveDataMask.svelte +2 -1
  18. package/dist/components/LiteShowField.svelte +6 -4
  19. package/dist/components/LiteSignaturePad.svelte +3 -1
  20. package/dist/components/advanced/LiteAutoSaveIndicator.svelte +2 -2
  21. package/dist/components/advanced/LiteToast.svelte +2 -2
  22. package/dist/components/advanced/LiteVirtualTable.svelte +6 -5
  23. package/dist/components/buttons/LiteCloneButton.svelte +3 -0
  24. package/dist/components/buttons/LiteCreateButton.svelte +3 -0
  25. package/dist/components/buttons/LiteDeleteButton.svelte +3 -0
  26. package/dist/components/buttons/LiteEditButton.svelte +3 -0
  27. package/dist/components/buttons/LiteExportButton.svelte +3 -0
  28. package/dist/components/buttons/LiteImportButton.svelte +5 -1
  29. package/dist/components/buttons/LiteListButton.svelte +3 -0
  30. package/dist/components/buttons/LiteRefreshButton.svelte +3 -0
  31. package/dist/components/buttons/LiteSaveButton.svelte +3 -0
  32. package/dist/components/buttons/LiteShowButton.svelte +3 -0
  33. package/dist/components/compatibility/LiteDirectoryUpload.svelte +4 -3
  34. package/dist/components/fields/LiteAvatarField.svelte +3 -3
  35. package/dist/components/fields/LiteCascader.svelte +4 -2
  36. package/dist/components/fields/LiteDateField.svelte +3 -0
  37. package/dist/components/fields/LiteDateRangeField.svelte +9 -4
  38. package/dist/components/fields/LiteFileField.svelte +2 -2
  39. package/dist/components/fields/LiteTreeSelect.svelte +4 -2
  40. package/dist/components/pages/LiteListPage.svelte +4 -2
  41. package/dist/filter-form.d.ts +3 -0
  42. package/dist/filter-form.js +34 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/dist/lite.css +86 -0
  46. package/dist/server-adapter.d.ts +15 -15
  47. package/dist/server-adapter.js +62 -8
  48. package/package.json +7 -14
  49. package/dist/polyfill-entry.d.ts +0 -2
  50. package/dist/polyfill-entry.js +0 -3
  51. 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: Record<string, unknown>;
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
- interface FieldMeta {
19
- key: string;
20
- title: string;
21
- type: string;
22
- enum?: Array<string | number>;
23
- required?: boolean;
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
- 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[]) : [];
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
- return Object.entries(propsObj).map(([key, propDef]) => ({
31
- 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,
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
- <div class="lite-schema-form-header">
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
- {#each properties as field (field.key)}
49
- <div class="lite-schema-field">
50
- <label for="lite_json_{field.key}" class="lite-label">
51
- {field.title} {#if field.required}<span style="color: #ef4444;">*</span>{/if}
52
- </label>
53
- {#if field.enum}
54
- <select id="lite_json_{field.key}" name={field.key} class="lite-select" required={field.required}>
55
- {#each field.enum as option (option)}
56
- <option value={String(option)} selected={String(value[field.key]) === String(option)}>
57
- {option}
58
- </option>
59
- {/each}
60
- </select>
61
- {:else if field.type === 'boolean'}
62
- <input id="lite_json_{field.key}" type="checkbox" name={field.key} checked={Boolean(value[field.key])} />
63
- {:else if field.type === 'number' || field.type === 'integer'}
64
- <input id="lite_json_{field.key}" type="number" name={field.key} value={Number(value[field.key] ?? 0)} class="lite-input" required={field.required} />
65
- {:else}
66
- <input id="lite_json_{field.key}" type="text" name={field.key} value={String(value[field.key] ?? '')} class="lite-input" required={field.required} />
67
- {/if}
68
- </div>
69
- {/each}
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
- background: #ffffff;
82
- border: 1px solid #cbd5e1;
83
- border-radius: 6px;
84
- padding: 16px;
85
- margin-bottom: 16px;
86
- }
87
- .lite-schema-form-header {
88
- padding-bottom: 8px;
89
- border-bottom: 1px solid #e2e8f0;
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: Record<string, unknown>;
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.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;
@@ -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)}