@svadmin/lite 0.1.0 → 0.2.1

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 (76) hide show
  1. package/README.md +49 -6
  2. package/package.json +5 -1
  3. package/src/components/LiteArrayField.svelte +112 -0
  4. package/src/components/LiteAuditLog.svelte +104 -0
  5. package/src/components/LiteBreadcrumbs.svelte +39 -0
  6. package/src/components/LiteChatDialog.svelte +101 -0
  7. package/src/components/LiteConfirmDialog.svelte +60 -0
  8. package/src/components/LiteEmptyState.svelte +39 -0
  9. package/src/components/LiteLayout.svelte +58 -10
  10. package/src/components/LitePermissionMatrix.svelte +147 -0
  11. package/src/components/LiteShow.svelte +3 -38
  12. package/src/components/LiteShowField.svelte +51 -0
  13. package/src/components/LiteStatsCard.svelte +70 -0
  14. package/src/components/LiteTabs.svelte +57 -0
  15. package/src/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
  16. package/src/components/advanced/LiteDraggableHeader.svelte +33 -0
  17. package/src/components/advanced/LiteDrawerForm.svelte +42 -0
  18. package/src/components/advanced/LiteInlineEdit.svelte +32 -0
  19. package/src/components/advanced/LiteModalForm.svelte +44 -0
  20. package/src/components/advanced/LiteToast.svelte +34 -0
  21. package/src/components/advanced/LiteUndoableNotification.svelte +25 -0
  22. package/src/components/advanced/LiteVirtualTable.svelte +44 -0
  23. package/src/components/advanced/index.ts +8 -0
  24. package/src/components/buttons/LiteCloneButton.svelte +33 -0
  25. package/src/components/buttons/LiteCreateButton.svelte +31 -0
  26. package/src/components/buttons/LiteDeleteButton.svelte +57 -0
  27. package/src/components/buttons/LiteEditButton.svelte +33 -0
  28. package/src/components/buttons/LiteExportButton.svelte +31 -0
  29. package/src/components/buttons/LiteImportButton.svelte +50 -0
  30. package/src/components/buttons/LiteListButton.svelte +31 -0
  31. package/src/components/buttons/LiteRefreshButton.svelte +27 -0
  32. package/src/components/buttons/LiteSaveButton.svelte +27 -0
  33. package/src/components/buttons/LiteShowButton.svelte +33 -0
  34. package/src/components/buttons/index.ts +10 -0
  35. package/src/components/fields/LiteBooleanField.svelte +41 -0
  36. package/src/components/fields/LiteDateField.svelte +51 -0
  37. package/src/components/fields/LiteEmailField.svelte +40 -0
  38. package/src/components/fields/LiteFileField.svelte +53 -0
  39. package/src/components/fields/LiteImageField.svelte +57 -0
  40. package/src/components/fields/LiteJsonField.svelte +43 -0
  41. package/src/components/fields/LiteMarkdownField.svelte +33 -0
  42. package/src/components/fields/LiteMultiSelectField.svelte +57 -0
  43. package/src/components/fields/LiteNumberField.svelte +34 -0
  44. package/src/components/fields/LiteRelationField.svelte +66 -0
  45. package/src/components/fields/LiteRichTextField.svelte +45 -0
  46. package/src/components/fields/LiteSelectField.svelte +47 -0
  47. package/src/components/fields/LiteTagField.svelte +44 -0
  48. package/src/components/fields/LiteTextField.svelte +34 -0
  49. package/src/components/fields/LiteUrlField.svelte +40 -0
  50. package/src/components/fields/index.ts +15 -0
  51. package/src/components/layout/LiteAuthenticated.svelte +23 -0
  52. package/src/components/layout/LiteCanAccess.svelte +27 -0
  53. package/src/components/layout/LiteCatchAllNavigate.svelte +20 -0
  54. package/src/components/layout/LiteErrorBoundary.svelte +20 -0
  55. package/src/components/layout/LiteErrorComponent.svelte +37 -0
  56. package/src/components/layout/LiteHeader.svelte +19 -0
  57. package/src/components/layout/LiteNavigateToResource.svelte +25 -0
  58. package/src/components/layout/LiteSidebar.svelte +93 -0
  59. package/src/components/layout/index.ts +8 -0
  60. package/src/components/pages/LiteCreatePage.svelte +39 -0
  61. package/src/components/pages/LiteEditPage.svelte +54 -0
  62. package/src/components/pages/LiteForgotPasswordPage.svelte +60 -0
  63. package/src/components/pages/LiteListPage.svelte +77 -0
  64. package/src/components/pages/LiteProfilePage.svelte +61 -0
  65. package/src/components/pages/LiteRegisterPage.svelte +64 -0
  66. package/src/components/pages/LiteShowPage.svelte +61 -0
  67. package/src/components/pages/LiteUpdatePasswordPage.svelte +51 -0
  68. package/src/components/pages/index.ts +8 -0
  69. package/src/components/widgets/LiteAnomalyBadge.svelte +40 -0
  70. package/src/components/widgets/LiteBarChart.svelte +45 -0
  71. package/src/components/widgets/LiteInsightCard.svelte +28 -0
  72. package/src/components/widgets/LiteLineChart.svelte +48 -0
  73. package/src/components/widgets/LitePieChart.svelte +44 -0
  74. package/src/components/widgets/index.ts +5 -0
  75. package/src/index.ts +28 -0
  76. package/src/lite.css +372 -124
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Copy } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ recordItemId: string | number;
8
+ basePath?: string;
9
+ hideText?: boolean;
10
+ class?: string;
11
+ size?: 'sm' | 'default' | 'icon';
12
+ }
13
+
14
+ let {
15
+ resource,
16
+ recordItemId,
17
+ basePath = '/lite',
18
+ hideText = false,
19
+ class: className = '',
20
+ size = 'default'
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <a
25
+ href="{basePath}/{resource}/create?cloneId={recordItemId}"
26
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
+ title={t('common.clone') || 'Clone'}
28
+ >
29
+ <Copy class="h-4 w-4" />
30
+ {#if !hideText}
31
+ <span style="marginLeft: 4px">{t('common.clone') || 'Clone'}</span>
32
+ {/if}
33
+ </a>
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Plus } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ basePath?: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ size?: 'sm' | 'default' | 'icon';
11
+ }
12
+
13
+ let {
14
+ resource,
15
+ basePath = '/lite',
16
+ hideText = false,
17
+ class: className = '',
18
+ size = 'default'
19
+ }: Props = $props();
20
+ </script>
21
+
22
+ <a
23
+ href="{basePath}/{resource}/create"
24
+ class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
+ title={t('common.create') || 'Create'}
26
+ >
27
+ <Plus class="h-4 w-4" />
28
+ {#if !hideText}
29
+ <span style="marginLeft: 4px">{t('common.create') || 'Create'}</span>
30
+ {/if}
31
+ </a>
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Trash2 } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ recordItemId: string | number;
8
+ basePath?: string;
9
+ hideText?: boolean;
10
+ class?: string;
11
+ size?: 'sm' | 'default' | 'icon';
12
+ redirectUrl?: string;
13
+ }
14
+
15
+ let {
16
+ resource,
17
+ recordItemId,
18
+ basePath = '/lite',
19
+ hideText = false,
20
+ class: className = '',
21
+ size = 'default',
22
+ redirectUrl = ''
23
+ }: Props = $props();
24
+ </script>
25
+
26
+ <details class="lite-confirm-details {className}">
27
+ <summary
28
+ class="lite-btn lite-btn-danger {size === 'sm' ? 'lite-btn-sm' : ''}"
29
+ title={t('common.delete') || 'Delete'}
30
+ >
31
+ <Trash2 class="h-4 w-4" />
32
+ {#if !hideText}
33
+ <span style="marginLeft: 4px">{t('common.delete') || 'Delete'}</span>
34
+ {/if}
35
+ </summary>
36
+ <div class="lite-confirm-panel" style="right: 0; left: auto;">
37
+ <p style="margin: 0 0 8px; font-size: 13px; color: #111827;">
38
+ {t('common.areYouSure') || 'Are you sure?'}
39
+ </p>
40
+ <form method="POST" action="?/{resource}_delete" style="display:inline-flex; gap: 8px;">
41
+ <input type="hidden" name="id" value={String(recordItemId)} />
42
+ {#if redirectUrl}
43
+ <input type="hidden" name="redirect" value={redirectUrl} />
44
+ {/if}
45
+ <button
46
+ type="button"
47
+ class="lite-btn lite-btn-sm"
48
+ onclick="this.closest('details').removeAttribute('open')"
49
+ >
50
+ {t('common.cancel') || 'Cancel'}
51
+ </button>
52
+ <button type="submit" class="lite-btn lite-btn-sm lite-btn-danger">
53
+ {t('common.confirm') || 'Confirm'}
54
+ </button>
55
+ </form>
56
+ </div>
57
+ </details>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Pencil } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ recordItemId: string | number;
8
+ basePath?: string;
9
+ hideText?: boolean;
10
+ class?: string;
11
+ size?: 'sm' | 'default' | 'icon';
12
+ }
13
+
14
+ let {
15
+ resource,
16
+ recordItemId,
17
+ basePath = '/lite',
18
+ hideText = false,
19
+ class: className = '',
20
+ size = 'default'
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <a
25
+ href="{basePath}/{resource}/edit/{recordItemId}"
26
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
+ title={t('common.edit') || 'Edit'}
28
+ >
29
+ <Pencil class="h-4 w-4" />
30
+ {#if !hideText}
31
+ <span style="marginLeft: 4px">{t('common.edit') || 'Edit'}</span>
32
+ {/if}
33
+ </a>
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Download } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ basePath?: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ size?: 'sm' | 'default' | 'icon';
11
+ }
12
+
13
+ let {
14
+ resource,
15
+ basePath = '/lite',
16
+ hideText = false,
17
+ class: className = '',
18
+ size = 'default'
19
+ }: Props = $props();
20
+ </script>
21
+
22
+ <a
23
+ href="?action=export"
24
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
+ title={t('common.export') || 'Export'}
26
+ >
27
+ <Download class="h-4 w-4" />
28
+ {#if !hideText}
29
+ <span style="marginLeft: 4px">{t('common.export') || 'Export'}</span>
30
+ {/if}
31
+ </a>
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Upload } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ basePath?: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ size?: 'sm' | 'default' | 'icon';
11
+ }
12
+
13
+ let {
14
+ resource,
15
+ basePath = '/lite',
16
+ hideText = false,
17
+ class: className = '',
18
+ size = 'default'
19
+ }: Props = $props();
20
+ </script>
21
+
22
+ <details class="lite-confirm-details {className}">
23
+ <summary
24
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''}"
25
+ title={t('common.import') || 'Import'}
26
+ >
27
+ <Upload class="h-4 w-4" />
28
+ {#if !hideText}
29
+ <span style="marginLeft: 4px">{t('common.import') || 'Import'}</span>
30
+ {/if}
31
+ </summary>
32
+ <div class="lite-confirm-panel">
33
+ <p style="margin: 0 0 8px; font-size: 13px;">{t('common.importData') || 'Import data (CSV/JSON)'}</p>
34
+ <form method="POST" action="?/{resource}_import" enctype="multipart/form-data" style="display:flex; flex-direction: column; gap: 8px;">
35
+ <input type="file" name="file" accept=".csv,.json" required style="font-size: 13px;" />
36
+ <div style="display:flex; gap: 8px; justify-content: flex-end;">
37
+ <button
38
+ type="button"
39
+ class="lite-btn lite-btn-sm"
40
+ onclick="this.closest('details').removeAttribute('open')"
41
+ >
42
+ {t('common.cancel') || 'Cancel'}
43
+ </button>
44
+ <button type="submit" class="lite-btn lite-btn-sm lite-btn-primary">
45
+ {t('common.upload') || 'Upload'}
46
+ </button>
47
+ </div>
48
+ </form>
49
+ </div>
50
+ </details>
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { List } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ basePath?: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ size?: 'sm' | 'default' | 'icon';
11
+ }
12
+
13
+ let {
14
+ resource,
15
+ basePath = '/lite',
16
+ hideText = false,
17
+ class: className = '',
18
+ size = 'default'
19
+ }: Props = $props();
20
+ </script>
21
+
22
+ <a
23
+ href="{basePath}/{resource}"
24
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
+ title={t('common.list') || 'List'}
26
+ >
27
+ <List class="h-4 w-4" />
28
+ {#if !hideText}
29
+ <span style="marginLeft: 4px">{t('common.list') || 'List'}</span>
30
+ {/if}
31
+ </a>
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { RefreshCw } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ hideText?: boolean;
7
+ class?: string;
8
+ size?: 'sm' | 'default' | 'icon';
9
+ }
10
+
11
+ let {
12
+ hideText = false,
13
+ class: className = '',
14
+ size = 'default'
15
+ }: Props = $props();
16
+ </script>
17
+
18
+ <a
19
+ href="?"
20
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
21
+ title={t('common.refresh') || 'Refresh'}
22
+ >
23
+ <RefreshCw class="h-4 w-4" />
24
+ {#if !hideText}
25
+ <span style="marginLeft: 4px">{t('common.refresh') || 'Refresh'}</span>
26
+ {/if}
27
+ </a>
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Save } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ hideText?: boolean;
7
+ class?: string;
8
+ size?: 'sm' | 'default' | 'icon';
9
+ }
10
+
11
+ let {
12
+ hideText = false,
13
+ class: className = '',
14
+ size = 'default'
15
+ }: Props = $props();
16
+ </script>
17
+
18
+ <button
19
+ type="submit"
20
+ class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
21
+ title={t('common.save') || 'Save'}
22
+ >
23
+ <Save class="h-4 w-4" />
24
+ {#if !hideText}
25
+ <span style="marginLeft: 4px">{t('common.save') || 'Save'}</span>
26
+ {/if}
27
+ </button>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { Eye } from 'lucide-svelte';
4
+
5
+ interface Props {
6
+ resource: string;
7
+ recordItemId: string | number;
8
+ basePath?: string;
9
+ hideText?: boolean;
10
+ class?: string;
11
+ size?: 'sm' | 'default' | 'icon';
12
+ }
13
+
14
+ let {
15
+ resource,
16
+ recordItemId,
17
+ basePath = '/lite',
18
+ hideText = false,
19
+ class: className = '',
20
+ size = 'default'
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <a
25
+ href="{basePath}/{resource}/show/{recordItemId}"
26
+ class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
+ title={t('common.show') || 'Show'}
28
+ >
29
+ <Eye class="h-4 w-4" />
30
+ {#if !hideText}
31
+ <span style="marginLeft: 4px">{t('common.show') || 'Show'}</span>
32
+ {/if}
33
+ </a>
@@ -0,0 +1,10 @@
1
+ export { default as LiteCreateButton } from './LiteCreateButton.svelte';
2
+ export { default as LiteEditButton } from './LiteEditButton.svelte';
3
+ export { default as LiteShowButton } from './LiteShowButton.svelte';
4
+ export { default as LiteListButton } from './LiteListButton.svelte';
5
+ export { default as LiteDeleteButton } from './LiteDeleteButton.svelte';
6
+ export { default as LiteCloneButton } from './LiteCloneButton.svelte';
7
+ export { default as LiteExportButton } from './LiteExportButton.svelte';
8
+ export { default as LiteImportButton } from './LiteImportButton.svelte';
9
+ export { default as LiteRefreshButton } from './LiteRefreshButton.svelte';
10
+ export { default as LiteSaveButton } from './LiteSaveButton.svelte';
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+ </script>
14
+
15
+ {#if mode === 'show'}
16
+ <div>
17
+ <span class="lite-bool {value ? 'lite-bool-true' : ''}"></span>
18
+ {value ? '✓ Yes' : '✗ No'}
19
+ </div>
20
+ {:else}
21
+ <div class="lite-checkbox-group">
22
+ <input
23
+ type="checkbox"
24
+ name={field.key}
25
+ id={field.key}
26
+ checked={!!value}
27
+ value="true"
28
+ />
29
+ <!-- Fallback hidden input so unchecked boxes submit "false" -->
30
+ <input type="hidden" name={field.key} value="false" />
31
+
32
+ <label for={field.key}>
33
+ {field.placeholder ?? (field.label || 'Yes')}
34
+ </label>
35
+ {#if hasError}
36
+ {#each error as err}
37
+ <div class="lite-error-text">{err}</div>
38
+ {/each}
39
+ {/if}
40
+ </div>
41
+ {/if}
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+
14
+ function formatDate(v: unknown): string {
15
+ if (!v) return '';
16
+ try {
17
+ return new Date(v as string).toISOString().substring(0, 16); // Formats for html datetime-local
18
+ } catch {
19
+ return '';
20
+ }
21
+ }
22
+
23
+ function displayDate(v: unknown): string {
24
+ if (!v) return '—';
25
+ try {
26
+ return new Date(v as string).toLocaleString();
27
+ } catch {
28
+ return String(v);
29
+ }
30
+ }
31
+ </script>
32
+
33
+ {#if mode === 'show'}
34
+ <span>{displayDate(value)}</span>
35
+ {:else}
36
+ <div>
37
+ <input
38
+ type="datetime-local"
39
+ name={field.key}
40
+ id={field.key}
41
+ value={formatDate(value)}
42
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
43
+ {...field.required ? { required: true } : {}}
44
+ />
45
+ {#if hasError}
46
+ {#each error as err}
47
+ <div class="lite-error-text">{err}</div>
48
+ {/each}
49
+ {/if}
50
+ </div>
51
+ {/if}
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+ </script>
14
+
15
+ {#if mode === 'show'}
16
+ <span>
17
+ {#if value}
18
+ <a href="mailto:{value}">{value}</a>
19
+ {:else}
20
+
21
+ {/if}
22
+ </span>
23
+ {:else}
24
+ <div>
25
+ <input
26
+ type="email"
27
+ name={field.key}
28
+ id={field.key}
29
+ value={String(value ?? '')}
30
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
31
+ placeholder={field.placeholder ?? field.label}
32
+ {...field.required ? { required: true } : {}}
33
+ />
34
+ {#if hasError}
35
+ {#each error as err}
36
+ <div class="lite-error-text">{err}</div>
37
+ {/each}
38
+ {/if}
39
+ </div>
40
+ {/if}
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+
14
+ function getFiles(v: unknown): string[] {
15
+ if (!v) return [];
16
+ if (Array.isArray(v)) return v.map(String);
17
+ if (typeof v === 'string') return [v];
18
+ return [];
19
+ }
20
+ </script>
21
+
22
+ {#if mode === 'show'}
23
+ {@const files = getFiles(value)}
24
+ <div style="display:flex; flex-direction: column; gap: 4px;">
25
+ {#each files as f}
26
+ <a href={f} target="_blank" rel="noopener noreferrer">{f.split('/').pop() || 'Download'}</a>
27
+ {:else}
28
+ <span>—</span>
29
+ {/each}
30
+ </div>
31
+ {:else}
32
+ <div>
33
+ {@const files = getFiles(value)}
34
+ {#if files.length > 0 && mode === 'edit'}
35
+ <div style="margin-bottom: 8px;">
36
+ <span style="font-size: 12px;">Current files: {files.length}</span>
37
+ </div>
38
+ {/if}
39
+ <input
40
+ type="file"
41
+ name={field.key}
42
+ id={field.key}
43
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
44
+ {...field.type === 'files' ? { multiple: true } : {}}
45
+ {...field.required && !files.length ? { required: true } : {}}
46
+ />
47
+ {#if hasError}
48
+ {#each error as err}
49
+ <div class="lite-error-text">{err}</div>
50
+ {/each}
51
+ {/if}
52
+ </div>
53
+ {/if}
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+
14
+ function getUrls(v: unknown): string[] {
15
+ if (!v) return [];
16
+ if (Array.isArray(v)) return v.map(String);
17
+ if (typeof v === 'string') return [v];
18
+ return [];
19
+ }
20
+ </script>
21
+
22
+ {#if mode === 'show'}
23
+ {@const urls = getUrls(value)}
24
+ <div style="display:flex; gap: 8px; flex-wrap: wrap;">
25
+ {#each urls as url}
26
+ <img src={url} alt={field.label} style="max-height: 100px; border-radius: 4px; border: 1px solid #e5e7eb;" />
27
+ {:else}
28
+ <span>—</span>
29
+ {/each}
30
+ </div>
31
+ {:else}
32
+ <div>
33
+ {@const urls = getUrls(value)}
34
+ {#if urls.length > 0 && mode === 'edit'}
35
+ <div style="margin-bottom: 8px; display:flex; gap: 8px;">
36
+ {#each urls as url}
37
+ <img src={url} alt="Current" style="height: 60px; border-radius: 4px; border: 1px solid #e5e7eb; opacity: 0.6;" />
38
+ {/each}
39
+ </div>
40
+ <p style="font-size: 11px; color:#6b7280; margin: 0 0 4px;">Uploading new ones will overwrite.</p>
41
+ {/if}
42
+ <input
43
+ type="file"
44
+ name={field.key}
45
+ id={field.key}
46
+ accept="image/*"
47
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
48
+ {...field.type === 'images' ? { multiple: true } : {}}
49
+ {...field.required && !urls.length ? { required: true } : {}}
50
+ />
51
+ {#if hasError}
52
+ {#each error as err}
53
+ <div class="lite-error-text">{err}</div>
54
+ {/each}
55
+ {/if}
56
+ </div>
57
+ {/if}
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ field: FieldDefinition;
6
+ value?: unknown;
7
+ error?: string[];
8
+ mode?: 'show' | 'edit' | 'create';
9
+ }
10
+
11
+ let { field, value, error = [], mode = 'show' }: Props = $props();
12
+ const hasError = error.length > 0;
13
+
14
+ function getJsonString(v: unknown): string {
15
+ if (v == null) return '';
16
+ if (typeof v === 'string') return v;
17
+ try {
18
+ return JSON.stringify(v, null, 2);
19
+ } catch {
20
+ return String(v);
21
+ }
22
+ }
23
+ </script>
24
+
25
+ {#if mode === 'show'}
26
+ <pre style="padding: 12px; background: #1e293b; color: #f8fafc; border-radius: 4px; overflow-x: auto; font-size: 13px;">{getJsonString(value)}</pre>
27
+ {:else}
28
+ <div>
29
+ <textarea
30
+ name={field.key}
31
+ id={field.key}
32
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
33
+ style="min-height: 150px; font-family: monospace;"
34
+ placeholder="{}"
35
+ {...field.required ? { required: true } : {}}
36
+ >{getJsonString(value)}</textarea>
37
+ {#if hasError}
38
+ {#each error as err}
39
+ <div class="lite-error-text">{err}</div>
40
+ {/each}
41
+ {/if}
42
+ </div>
43
+ {/if}