@svadmin/lite 0.3.4 → 0.3.6

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/package.json +9 -6
  2. package/src/server-adapter.ts +1 -1
  3. package/src/components/LiteAlert.svelte +0 -19
  4. package/src/components/LiteArrayField.svelte +0 -113
  5. package/src/components/LiteAuditLog.svelte +0 -104
  6. package/src/components/LiteBreadcrumbs.svelte +0 -39
  7. package/src/components/LiteChatDialog.svelte +0 -101
  8. package/src/components/LiteConfirmDialog.svelte +0 -60
  9. package/src/components/LiteEmptyState.svelte +0 -39
  10. package/src/components/LiteForm.svelte +0 -138
  11. package/src/components/LiteLayout.svelte +0 -106
  12. package/src/components/LiteLogin.svelte +0 -41
  13. package/src/components/LitePagination.svelte +0 -73
  14. package/src/components/LitePermissionMatrix.svelte +0 -147
  15. package/src/components/LiteSearch.svelte +0 -41
  16. package/src/components/LiteShow.svelte +0 -61
  17. package/src/components/LiteShowField.svelte +0 -51
  18. package/src/components/LiteStatsCard.svelte +0 -70
  19. package/src/components/LiteTable.svelte +0 -132
  20. package/src/components/LiteTabs.svelte +0 -57
  21. package/src/components/advanced/LiteAutoSaveIndicator.svelte +0 -26
  22. package/src/components/advanced/LiteDraggableHeader.svelte +0 -33
  23. package/src/components/advanced/LiteDrawerForm.svelte +0 -42
  24. package/src/components/advanced/LiteInlineEdit.svelte +0 -32
  25. package/src/components/advanced/LiteModalForm.svelte +0 -44
  26. package/src/components/advanced/LiteToast.svelte +0 -34
  27. package/src/components/advanced/LiteUndoableNotification.svelte +0 -25
  28. package/src/components/advanced/LiteVirtualTable.svelte +0 -44
  29. package/src/components/buttons/LiteCloneButton.svelte +0 -33
  30. package/src/components/buttons/LiteCreateButton.svelte +0 -31
  31. package/src/components/buttons/LiteDeleteButton.svelte +0 -57
  32. package/src/components/buttons/LiteEditButton.svelte +0 -33
  33. package/src/components/buttons/LiteExportButton.svelte +0 -31
  34. package/src/components/buttons/LiteImportButton.svelte +0 -50
  35. package/src/components/buttons/LiteListButton.svelte +0 -31
  36. package/src/components/buttons/LiteRefreshButton.svelte +0 -27
  37. package/src/components/buttons/LiteSaveButton.svelte +0 -27
  38. package/src/components/buttons/LiteShowButton.svelte +0 -33
  39. package/src/components/fields/LiteBooleanField.svelte +0 -41
  40. package/src/components/fields/LiteDateField.svelte +0 -51
  41. package/src/components/fields/LiteEmailField.svelte +0 -40
  42. package/src/components/fields/LiteFileField.svelte +0 -53
  43. package/src/components/fields/LiteImageField.svelte +0 -57
  44. package/src/components/fields/LiteJsonField.svelte +0 -43
  45. package/src/components/fields/LiteMarkdownField.svelte +0 -33
  46. package/src/components/fields/LiteMultiSelectField.svelte +0 -57
  47. package/src/components/fields/LiteNumberField.svelte +0 -34
  48. package/src/components/fields/LiteRelationField.svelte +0 -66
  49. package/src/components/fields/LiteRichTextField.svelte +0 -45
  50. package/src/components/fields/LiteSelectField.svelte +0 -47
  51. package/src/components/fields/LiteTagField.svelte +0 -44
  52. package/src/components/fields/LiteTextField.svelte +0 -34
  53. package/src/components/fields/LiteUrlField.svelte +0 -40
  54. package/src/components/layout/LiteAuthenticated.svelte +0 -23
  55. package/src/components/layout/LiteCanAccess.svelte +0 -27
  56. package/src/components/layout/LiteCatchAllNavigate.svelte +0 -20
  57. package/src/components/layout/LiteErrorBoundary.svelte +0 -20
  58. package/src/components/layout/LiteErrorComponent.svelte +0 -37
  59. package/src/components/layout/LiteHeader.svelte +0 -19
  60. package/src/components/layout/LiteNavigateToResource.svelte +0 -25
  61. package/src/components/layout/LiteSidebar.svelte +0 -93
  62. package/src/components/pages/LiteCreatePage.svelte +0 -39
  63. package/src/components/pages/LiteEditPage.svelte +0 -54
  64. package/src/components/pages/LiteForgotPasswordPage.svelte +0 -60
  65. package/src/components/pages/LiteListPage.svelte +0 -81
  66. package/src/components/pages/LiteProfilePage.svelte +0 -61
  67. package/src/components/pages/LiteRegisterPage.svelte +0 -64
  68. package/src/components/pages/LiteShowPage.svelte +0 -61
  69. package/src/components/pages/LiteUpdatePasswordPage.svelte +0 -51
  70. package/src/components/widgets/LiteAnomalyBadge.svelte +0 -40
  71. package/src/components/widgets/LiteBarChart.svelte +0 -45
  72. package/src/components/widgets/LiteInsightCard.svelte +0 -28
  73. package/src/components/widgets/LiteLineChart.svelte +0 -48
  74. package/src/components/widgets/LitePieChart.svelte +0 -44
  75. package/src/lite.css +0 -708
  76. package/static/enhance.js +0 -56
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@svadmin/lite",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "SSR-compatible lightweight admin UI for @svadmin — zero client-side JS, works in IE11",
5
5
  "type": "module",
6
6
  "files": [
7
- "src/**/*",
8
- "static/**/*"
7
+ "src/**/*.ts",
8
+ "!src/**/*.test.*",
9
+ "!src/**/*.spec.*",
10
+ "!src/**/vitest.config.*",
11
+ "!src/**/setupTest.*"
9
12
  ],
10
13
  "main": "src/index.ts",
11
14
  "types": "src/index.ts",
@@ -21,13 +24,13 @@
21
24
  },
22
25
  "peerDependencies": {
23
26
  "svelte": "^5.0.0",
24
- "@svadmin/core": "^0.23.3",
27
+ "@svadmin/core": "^0.27.1",
25
28
  "@sveltejs/kit": "^2.0.0"
26
29
  },
27
30
  "dependencies": {
28
- "@lucide/svelte": "^1.7.0",
31
+ "@lucide/svelte": "^1.17.0",
29
32
  "sveltekit-superforms": "^2.30.1",
30
- "zod": "^4.3.6"
33
+ "zod": "^4.4.3"
31
34
  },
32
35
  "license": "MIT",
33
36
  "publishConfig": {
@@ -9,7 +9,7 @@ import type {
9
9
  ResourceDefinition, FieldDefinition,
10
10
  Sort, Filter,
11
11
  } from '@svadmin/core';
12
- import { fail, redirect, isRedirect, type RequestEvent } from '@sveltejs/kit';
12
+ import { redirect, isRedirect, type RequestEvent } from '@sveltejs/kit';
13
13
 
14
14
  // ─── List Loader ──────────────────────────────────────────────
15
15
 
@@ -1,19 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteAlert — Success/error feedback banner for form actions.
4
- * Renders from SvelteKit form action results.
5
- */
6
- interface Props {
7
- type: 'success' | 'error' | 'info';
8
- message: string;
9
- }
10
-
11
- let { type, message }: Props = $props();
12
- </script>
13
-
14
- {#if message}
15
- <div class="lite-alert {type === 'success' ? 'lite-alert-success' : type === 'error' ? 'lite-alert-error' : ''}">
16
- {#if type === 'success'}✓{:else if type === 'error'}✗{:else}ℹ{/if}
17
- {message}
18
- </div>
19
- {/if}
@@ -1,113 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteArrayField — SSR-compatible dynamic array sub-form.
4
- * Uses progressive enhancement: hidden inputs + JS optional add/remove.
5
- * Falls back to server-side form POST for non-JS browsers.
6
- */
7
- import type { FieldDefinition } from '@svadmin/core';
8
- import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
9
-
10
- interface Props {
11
- field: FieldDefinition;
12
- /** Current array values */
13
- values: Record<string, unknown>[];
14
- /** Form field name prefix (for nested form encoding) */
15
- namePrefix?: string;
16
- }
17
-
18
- let {
19
- field,
20
- values = [],
21
- namePrefix = field.key,
22
- }: Props = $props();
23
-
24
- const subFields = $derived(field.subFields || []);
25
- </script>
26
-
27
- <fieldset class="lite-array-field">
28
- <legend style="font-weight:600;font-size:14px;margin-bottom:8px;">
29
- {field.label}
30
- {#if field.required}<span style="color:#dc2626;">*</span>{/if}
31
- </legend>
32
-
33
- {#each values as item, i}
34
- <div class="lite-array-item" style="border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin-bottom:8px;background:#f8fafc;">
35
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
36
- <span style="font-size:12px;font-weight:600;color:#94a3b8;">#{i + 1}</span>
37
- <button type="button" class="lite-btn lite-btn-sm" style="color:#dc2626;border-color:#fecaca;" onclick={(e) => (e.currentTarget as HTMLElement).closest('.lite-array-item')?.remove()}>
38
- Remove
39
- </button>
40
- </div>
41
- {#each subFields as sub}
42
- <div style="margin-bottom:8px;">
43
- <label for="{namePrefix}_{i}_{sub.key}" style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:4px;">
44
- {sub.label}
45
- {#if sub.required}<span style="color:#dc2626;">*</span>{/if}
46
- </label>
47
- {#if sub.type === 'textarea'}
48
- <textarea
49
- id="{namePrefix}_{i}_{sub.key}"
50
- name="{namePrefix}[{i}][{sub.key}]"
51
- class="lite-input"
52
- rows="3"
53
- placeholder={fieldToPlaceholder(sub)}
54
- >{item[sub.key] ?? ''}</textarea>
55
- {:else if sub.type === 'boolean'}
56
- <input
57
- id="{namePrefix}_{i}_{sub.key}"
58
- name="{namePrefix}[{i}][{sub.key}]"
59
- type="checkbox"
60
- checked={!!item[sub.key]}
61
- style="width:18px;height:18px;"
62
- />
63
- {:else if sub.type === 'select' && sub.options}
64
- <select
65
- id="{namePrefix}_{i}_{sub.key}"
66
- name="{namePrefix}[{i}][{sub.key}]"
67
- class="lite-input"
68
- >
69
- {#each sub.options as opt}
70
- <option value={typeof opt === 'string' ? opt : opt.value} selected={item[sub.key] === (typeof opt === 'string' ? opt : opt.value)}>
71
- {typeof opt === 'string' ? opt : opt.label}
72
- </option>
73
- {/each}
74
- </select>
75
- {:else}
76
- <input
77
- id="{namePrefix}_{i}_{sub.key}"
78
- name="{namePrefix}[{i}][{sub.key}]"
79
- type={fieldToInputType(sub)}
80
- value={item[sub.key] ?? ''}
81
- class="lite-input"
82
- placeholder={fieldToPlaceholder(sub)}
83
- />
84
- {/if}
85
- </div>
86
- {/each}
87
- </div>
88
- {/each}
89
-
90
- {#if values.length === 0}
91
- <p style="text-align:center;padding:16px;color:#94a3b8;font-size:14px;">No items added yet.</p>
92
- {/if}
93
-
94
- <button type="button" class="lite-btn" style="margin-top:4px;" onclick={(e) => {
95
- const target = e.currentTarget as HTMLElement;
96
- const template = target.previousElementSibling?.previousElementSibling?.cloneNode(true);
97
- if (template && target.parentElement) target.parentElement.insertBefore(template, target.previousElementSibling);
98
- }}>
99
- + Add Item
100
- </button>
101
- </fieldset>
102
-
103
- <style>
104
- .lite-array-field {
105
- border: 2px dashed #e2e8f0;
106
- border-radius: 8px;
107
- padding: 16px;
108
- margin-bottom: 16px;
109
- }
110
- .lite-array-field legend {
111
- padding: 0 8px;
112
- }
113
- </style>
@@ -1,104 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteAuditLog — SSR-compatible audit log viewer.
4
- * Pure HTML table with pagination links. No client-side JS required.
5
- */
6
-
7
- interface AuditEntry {
8
- id: string | number;
9
- userName?: string;
10
- action: string;
11
- resource?: string;
12
- createdAt: string;
13
- ipAddress?: string;
14
- details?: string;
15
- }
16
-
17
- interface Props {
18
- logs: AuditEntry[];
19
- total?: number;
20
- page?: number;
21
- pageSize?: number;
22
- basePath?: string;
23
- }
24
-
25
- let {
26
- logs,
27
- total = 0,
28
- page = 1,
29
- pageSize = 20,
30
- basePath = '?',
31
- }: Props = $props();
32
-
33
- const totalPages = $derived(Math.ceil(total / pageSize) || 1);
34
-
35
- function formatDate(d: string) {
36
- return new Date(d).toLocaleString();
37
- }
38
-
39
- function actionClass(action: string): string {
40
- const a = action.toLowerCase();
41
- if (a.includes('delete') || a.includes('remove')) return 'lite-badge-danger';
42
- if (a.includes('create') || a.includes('add')) return 'lite-badge-success';
43
- if (a.includes('update') || a.includes('edit')) return 'lite-badge-warning';
44
- return 'lite-badge-default';
45
- }
46
- </script>
47
-
48
- <div class="lite-audit-log">
49
- <h2 style="margin-bottom:16px;">Audit Logs</h2>
50
-
51
- <table class="lite-table">
52
- <thead>
53
- <tr>
54
- <th style="width:160px;">Time</th>
55
- <th>User</th>
56
- <th>Action</th>
57
- <th>Resource</th>
58
- <th style="width:120px;">IP Address</th>
59
- </tr>
60
- </thead>
61
- <tbody>
62
- {#each logs as log}
63
- <tr>
64
- <td style="font-family:monospace;font-size:12px;color:#64748b;">{formatDate(log.createdAt)}</td>
65
- <td>{log.userName ?? '—'}</td>
66
- <td><span class="lite-badge {actionClass(log.action)}">{log.action}</span></td>
67
- <td style="color:#64748b;">{log.resource ?? '—'}</td>
68
- <td style="font-family:monospace;font-size:12px;color:#64748b;">{log.ipAddress ?? '—'}</td>
69
- </tr>
70
- {:else}
71
- <tr>
72
- <td colspan="5" style="text-align:center;padding:24px;color:#94a3b8;">No audit logs found.</td>
73
- </tr>
74
- {/each}
75
- </tbody>
76
- </table>
77
-
78
- <!-- Pagination -->
79
- {#if totalPages > 1}
80
- <div class="lite-pagination" style="margin-top:12px;">
81
- {#if page > 1}
82
- <a href={`${basePath}page={page - 1}`} class="lite-btn lite-btn-sm">&laquo; Prev</a>
83
- {/if}
84
- <span style="padding:0 12px;font-size:14px;color:#64748b;">Page {page} / {totalPages} ({total} total)</span>
85
- {#if page < totalPages}
86
- <a href={`${basePath}page={page + 1}`} class="lite-btn lite-btn-sm">Next &raquo;</a>
87
- {/if}
88
- </div>
89
- {/if}
90
- </div>
91
-
92
- <style>
93
- .lite-badge {
94
- display: inline-block;
95
- padding: 2px 8px;
96
- border-radius: 4px;
97
- font-size: 12px;
98
- font-weight: 600;
99
- }
100
- .lite-badge-danger { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
101
- .lite-badge-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
102
- .lite-badge-warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
103
- .lite-badge-default { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
104
- </style>
@@ -1,39 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteBreadcrumbs — SSR-compatible breadcrumb navigation.
4
- * Pure HTML <nav> with <a> links. No client-side JS required.
5
- */
6
-
7
- interface BreadcrumbItem {
8
- label: string;
9
- href?: string;
10
- }
11
-
12
- interface Props {
13
- items: BreadcrumbItem[];
14
- /** Separator character between items */
15
- separator?: string;
16
- }
17
-
18
- let {
19
- items,
20
- separator = '/',
21
- }: Props = $props();
22
- </script>
23
-
24
- <nav class="lite-breadcrumbs" aria-label="Breadcrumb">
25
- <ol>
26
- {#each items as item, i}
27
- <li>
28
- {#if item.href && i < items.length - 1}
29
- <a href={item.href}>{item.label}</a>
30
- {:else}
31
- <span class="current">{item.label}</span>
32
- {/if}
33
- {#if i < items.length - 1}
34
- <span class="lite-breadcrumb-sep" aria-hidden="true">{separator}</span>
35
- {/if}
36
- </li>
37
- {/each}
38
- </ol>
39
- </nav>
@@ -1,101 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteChatDialog — SSR-compatible AI Chat Dialog fallback.
4
- * Relies on standard <form> POST without polling or WebSockets.
5
- * No client-side JS required.
6
- */
7
- import type { ChatMessage } from '@svadmin/core';
8
-
9
- interface Props {
10
- /** History of messages */
11
- messages: ChatMessage[];
12
- /** URL to handle the POST request for new messages */
13
- actionRoute: string;
14
- /** If true, shows a "Thinking..." indicator */
15
- isLoading?: boolean;
16
- title?: string;
17
- placeholder?: string;
18
- submitLabel?: string;
19
- }
20
-
21
- let {
22
- messages = [],
23
- actionRoute,
24
- isLoading = false,
25
- title = 'AI Assistant',
26
- placeholder = 'Type your message...',
27
- submitLabel = 'Send'
28
- }: Props = $props();
29
-
30
- function formatDate(ts: number) {
31
- if (!ts) return '';
32
- return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
33
- }
34
-
35
- // Auto-scroll anchor logic: pure HTML way to scroll to bottom after page load
36
- // User should include "#latest-msg" in the form action if possible
37
- </script>
38
-
39
- <div class="lite-chat-dialog lite-card" style="display:flex;flex-direction:column;height:560px;padding:0;overflow:hidden;max-width:400px;margin:0 auto;">
40
- <!-- Header -->
41
- <div style="background:#0f172a;color:#fff;padding:14px 16px;font-weight:600;flex-shrink:0;display:flex;align-items:center;border-top-left-radius:8px;border-top-right-radius:8px;">
42
- {title}
43
- </div>
44
-
45
- <!-- Message History -->
46
- <div class="lite-chat-history" style="flex:1;overflow-y:auto;padding:16px;background:#f8fafc;">
47
- {#if messages.length === 0}
48
- <div style="text-align:center;color:#64748b;margin-top:60px;font-size:14px;">
49
- How can I help you today?
50
- </div>
51
- {/if}
52
-
53
- {#each messages as msg, i}
54
- {#if msg.role === 'user' || msg.role === 'assistant'}
55
- <div
56
- id={i === messages.length - 1 ? 'latest-msg' : ''}
57
- style="margin-bottom:16px;display:flex;flex-direction:column;align-items:{msg.role === 'user' ? 'flex-end' : 'flex-start'};"
58
- >
59
- <div style="
60
- max-width:85%;
61
- padding:10px 14px;
62
- border-radius:12px;
63
- font-size:14px;
64
- line-height:1.5;
65
- {msg.role === 'user'
66
- ? 'background:#4f46e5;color:#fff;border-bottom-right-radius:2px;'
67
- : 'background:#e2e8f0;color:#0f172a;border-bottom-left-radius:2px;'}"
68
- >
69
- <div style="white-space:pre-wrap;word-break:break-word;">{msg.content}</div>
70
- </div>
71
- <div style="font-size:11px;color:#94a3b8;margin-top:4px;">
72
- {msg.role === 'user' ? 'You' : 'Assistant'} • {formatDate(msg.timestamp)}
73
- </div>
74
- </div>
75
- {/if}
76
- {/each}
77
-
78
- {#if isLoading}
79
- <div id="latest-msg" style="display:flex;flex-direction:column;align-items:flex-start;margin-bottom:16px;">
80
- <div style="background:#e2e8f0;color:#64748b;padding:10px 14px;border-radius:12px;border-bottom-left-radius:2px;font-size:13px;font-style:italic;">
81
- Thinking...
82
- </div>
83
- </div>
84
- {/if}
85
- </div>
86
-
87
- <!-- Input Form -->
88
- <form method="POST" action={actionRoute} style="display:flex;padding:12px;background:#fff;border-top:1px solid #e2e8f0;align-items:stretch;margin:0;flex-shrink:0;">
89
- <textarea
90
- name="message"
91
- class="lite-input"
92
- placeholder={placeholder}
93
- required
94
- rows="1"
95
- style="min-height:40px;resize:none;margin-right:8px;"
96
- ></textarea>
97
- <button type="submit" class="lite-btn lite-btn-primary" disabled={isLoading} style="display:flex;align-items:center;padding:0 16px;">
98
- {submitLabel}
99
- </button>
100
- </form>
101
- </div>
@@ -1,60 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteConfirmDialog — SSR-compatible confirmation dialog.
4
- * Utilizes pure HTML <details> and <summary> to show a popup.
5
- * No client-side JS required.
6
- */
7
-
8
- interface Props {
9
- title?: string;
10
- description?: string;
11
- confirmLabel?: string;
12
- cancelLabel?: string;
13
- /** The summary element (button) that triggers the dropdown */
14
- triggerLabel: string;
15
- triggerClass?: string;
16
- /** Form action to submit on confirm */
17
- action: string;
18
- /** Any hidden inputs to include in the form */
19
- hiddenInputs?: Record<string, string>;
20
- align?: 'left' | 'right';
21
- }
22
-
23
- let {
24
- title = 'Are you sure?',
25
- description = 'This action cannot be undone.',
26
- confirmLabel = 'Confirm',
27
- cancelLabel = 'Cancel',
28
- triggerLabel,
29
- triggerClass = 'lite-btn lite-btn-danger lite-btn-sm',
30
- action,
31
- hiddenInputs = {},
32
- align = 'right'
33
- }: Props = $props();
34
- </script>
35
-
36
- <details class="lite-confirm-details">
37
- <summary class={triggerClass}>{triggerLabel}</summary>
38
- <div class="lite-confirm-panel" style="text-align:left; {align === 'right' ? 'right:0;' : 'left:0;'}">
39
- <div style="font-weight:600;margin-bottom:8px;color:#0f172a;">{title}</div>
40
- {#if description}
41
- <div style="font-size:12px;color:#64748b;margin-bottom:16px;">{description}</div>
42
- {/if}
43
- <form method="POST" {action} style="display:flex;margin:0;justify-content:flex-end;">
44
- {#each Object.entries(hiddenInputs) as [key, val]}
45
- <input type="hidden" name={key} value={val} />
46
- {/each}
47
- <button
48
- type="button"
49
- class="lite-btn lite-btn-sm"
50
- style="margin-right:8px;"
51
- onclick={(e) => (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open')}
52
- >
53
- {cancelLabel}
54
- </button>
55
- <button type="submit" class="lite-btn lite-btn-primary lite-btn-sm lite-btn-danger">
56
- {confirmLabel}
57
- </button>
58
- </form>
59
- </div>
60
- </details>
@@ -1,39 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteEmptyState — SSR-compatible empty state indicator.
4
- * Renders a centered box with an icon, title, description, and link.
5
- * No client-side JS required.
6
- */
7
-
8
- interface Props {
9
- title: string;
10
- description?: string;
11
- /** Optional HTML entity or text icon */
12
- icon?: string;
13
- actionLabel?: string;
14
- actionUrl?: string;
15
- }
16
-
17
- let {
18
- title,
19
- description,
20
- icon,
21
- actionLabel,
22
- actionUrl,
23
- }: Props = $props();
24
- </script>
25
-
26
- <div class="lite-empty-state lite-card" style="text-align:center;padding:48px 24px;">
27
- {#if icon}
28
- <div style="font-size:32px;color:#94a3b8;margin-bottom:16px;">{icon}</div>
29
- {/if}
30
- <h3 style="font-size:16px;font-weight:600;color:#0f172a;margin:0 0 8px 0;">{title}</h3>
31
- {#if description}
32
- <p style="font-size:14px;color:#64748b;margin:0 0 24px 0;">{description}</p>
33
- {/if}
34
- {#if actionLabel && actionUrl}
35
- <a href={actionUrl} class="lite-btn lite-btn-primary">
36
- {actionLabel}
37
- </a>
38
- {/if}
39
- </div>
@@ -1,138 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LiteForm — Server-rendered form driven by FieldDefinitions.
4
- * Uses native <form> with method="POST" for IE11 compatibility.
5
- * Integrates with sveltekit-superforms for server-side validation.
6
- */
7
- import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';
8
- import { t } from '@svadmin/core/i18n';
9
- import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
10
-
11
- interface Props {
12
- fields: FieldDefinition[];
13
- /** Resource definition (used for primaryKey) */
14
- resource?: ResourceDefinition;
15
- /** Existing values (for edit mode) */
16
- values?: Record<string, unknown>;
17
- /** Validation errors from server */
18
- errors?: Record<string, string[]>;
19
- /** 'create' or 'edit' */
20
- mode?: 'create' | 'edit';
21
- /** Form action URL */
22
- action?: string;
23
- /** Submit button label */
24
- submitLabel?: string;
25
- /** Cancel URL */
26
- cancelUrl?: string;
27
- }
28
-
29
- let {
30
- fields,
31
- resource,
32
- values = {},
33
- errors = {},
34
- mode = 'create',
35
- action = '',
36
- submitLabel,
37
- cancelUrl,
38
- }: Props = $props();
39
-
40
- const formFields = $derived(
41
- fields.filter(f => {
42
- if (f.showInForm === false) return false;
43
- if (mode === 'create' && f.showInCreate === false) return false;
44
- if (mode === 'edit' && f.showInEdit === false) return false;
45
- return true;
46
- })
47
- );
48
- </script>
49
-
50
- <form method="POST" action={action || undefined} class="lite-card" enctype="multipart/form-data">
51
- {#if mode === 'edit'}
52
- {@const pk = resource?.primaryKey ?? 'id'}
53
- {#if values[pk] != null}
54
- <input type="hidden" name="_id" value={String(values[pk])} />
55
- {/if}
56
- {/if}
57
-
58
- {#each formFields as field}
59
- {@const inputType = fieldToInputType(field)}
60
- {@const placeholder = fieldToPlaceholder(field)}
61
- {@const value = values[field.key]}
62
- {@const fieldErrors = errors[field.key]}
63
- {@const hasError = fieldErrors && fieldErrors.length > 0}
64
-
65
- <div class="lite-form-group">
66
- {#if inputType === 'checkbox'}
67
- <div class="lite-checkbox-group">
68
- <input
69
- type="checkbox"
70
- name={field.key}
71
- id={field.key}
72
- checked={!!value}
73
- />
74
- <label for={field.key}>{field.label}</label>
75
- </div>
76
- {:else}
77
- <label for={field.key}>
78
- {field.label}
79
- {#if field.required}<span class="required">*</span>{/if}
80
- </label>
81
-
82
- {#if inputType === 'textarea'}
83
- <textarea
84
- name={field.key}
85
- id={field.key}
86
- class="lite-input {hasError ? 'lite-input-error' : ''}"
87
- placeholder={placeholder}
88
- >{value ?? ''}</textarea>
89
- {:else if inputType === 'select'}
90
- <select
91
- name={field.key}
92
- id={field.key}
93
- class="lite-select {hasError ? 'lite-input-error' : ''}"
94
- {...field.type === 'multiselect' ? { multiple: true } : {}}
95
- >
96
- {#if field.type !== 'multiselect'}
97
- <option value="">-- {t('common.select') || 'Select'} --</option>
98
- {/if}
99
- {#if field.options}
100
- {#each field.options as opt}
101
- <option
102
- value={String(opt.value)}
103
- selected={field.type === 'multiselect' ? (Array.isArray(value) && value.includes(opt.value)) : value === opt.value}
104
- >
105
- {opt.label}
106
- </option>
107
- {/each}
108
- {/if}
109
- </select>
110
- {:else}
111
- <input
112
- type={inputType}
113
- name={field.key}
114
- id={field.key}
115
- value={String(value ?? '')}
116
- class="lite-input {hasError ? 'lite-input-error' : ''}"
117
- placeholder={placeholder}
118
- {... field.required ? { required: true } : {}}
119
- {... field.type === 'images' ? { multiple: true } : {}}
120
- />
121
- {/if}
122
-
123
- {#if hasError}
124
- {#each fieldErrors as err}
125
- <div class="lite-error-text">{err}</div>
126
- {/each}
127
- {/if}
128
- {/if}
129
- </div>
130
- {/each}
131
-
132
- <div style="display:flex;align-items:center;margin-top:20px;">
133
- <button type="submit" class="lite-btn lite-btn-primary">{submitLabel || t('common.save') || 'Save'}</button>
134
- {#if cancelUrl}
135
- <a href={cancelUrl} class="lite-btn" style="margin-left:8px;">{t('common.cancel') || 'Cancel'}</a>
136
- {/if}
137
- </div>
138
- </form>