@svadmin/lite 0.3.7 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -13
- package/dist/components/LiteAlert.svelte +19 -0
- package/dist/components/LiteAlert.svelte.d.ts +11 -0
- package/dist/components/LiteArrayField.svelte +60 -0
- package/dist/components/LiteArrayField.svelte.d.ts +17 -0
- package/dist/components/LiteArrayItem.svelte +146 -0
- package/dist/components/LiteArrayItem.svelte.d.ts +13 -0
- package/dist/components/LiteAuditLog.svelte +104 -0
- package/dist/components/LiteAuditLog.svelte.d.ts +23 -0
- package/dist/components/LiteBreadcrumbs.svelte +39 -0
- package/dist/components/LiteBreadcrumbs.svelte.d.ts +16 -0
- package/dist/components/LiteChatDialog.svelte +101 -0
- package/dist/components/LiteChatDialog.svelte.d.ts +20 -0
- package/dist/components/LiteConfirmDialog.svelte +60 -0
- package/dist/components/LiteConfirmDialog.svelte.d.ts +22 -0
- package/dist/components/LiteEmptyState.svelte +39 -0
- package/dist/components/LiteEmptyState.svelte.d.ts +16 -0
- package/dist/components/LiteForm.svelte +165 -0
- package/dist/components/LiteForm.svelte.d.ts +26 -0
- package/dist/components/LiteLayout.svelte +147 -0
- package/dist/components/LiteLayout.svelte.d.ts +19 -0
- package/dist/components/LiteLogin.svelte +41 -0
- package/dist/components/LiteLogin.svelte.d.ts +8 -0
- package/dist/components/LitePagination.svelte +73 -0
- package/dist/components/LitePagination.svelte.d.ts +11 -0
- package/dist/components/LitePermissionMatrix.svelte +147 -0
- package/dist/components/LitePermissionMatrix.svelte.d.ts +32 -0
- package/dist/components/LiteSearch.svelte +41 -0
- package/dist/components/LiteSearch.svelte.d.ts +9 -0
- package/dist/components/LiteShow.svelte +61 -0
- package/dist/components/LiteShow.svelte.d.ts +14 -0
- package/dist/components/LiteShowField.svelte +62 -0
- package/dist/components/LiteShowField.svelte.d.ts +12 -0
- package/dist/components/LiteStatsCard.svelte +70 -0
- package/dist/components/LiteStatsCard.svelte.d.ts +24 -0
- package/dist/components/LiteTable.svelte +132 -0
- package/dist/components/LiteTable.svelte.d.ts +19 -0
- package/dist/components/LiteTabs.svelte +57 -0
- package/dist/components/LiteTabs.svelte.d.ts +16 -0
- package/dist/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
- package/dist/components/advanced/LiteAutoSaveIndicator.svelte.d.ts +12 -0
- package/dist/components/advanced/LiteDraggableHeader.svelte +33 -0
- package/dist/components/advanced/LiteDraggableHeader.svelte.d.ts +15 -0
- package/dist/components/advanced/LiteDrawerForm.svelte +42 -0
- package/dist/components/advanced/LiteDrawerForm.svelte.d.ts +16 -0
- package/dist/components/advanced/LiteInlineEdit.svelte +32 -0
- package/dist/components/advanced/LiteInlineEdit.svelte.d.ts +15 -0
- package/dist/components/advanced/LiteModalForm.svelte +44 -0
- package/dist/components/advanced/LiteModalForm.svelte.d.ts +17 -0
- package/dist/components/advanced/LiteToast.svelte +34 -0
- package/dist/components/advanced/LiteToast.svelte.d.ts +14 -0
- package/dist/components/advanced/LiteUndoableNotification.svelte +25 -0
- package/dist/components/advanced/LiteUndoableNotification.svelte.d.ts +13 -0
- package/dist/components/advanced/LiteVirtualTable.svelte +44 -0
- package/dist/components/advanced/LiteVirtualTable.svelte.d.ts +20 -0
- package/dist/components/advanced/index.js +8 -0
- package/dist/components/buttons/LiteCloneButton.svelte +33 -0
- package/dist/components/buttons/LiteCloneButton.svelte.d.ts +11 -0
- package/dist/components/buttons/LiteCreateButton.svelte +31 -0
- package/dist/components/buttons/LiteCreateButton.svelte.d.ts +10 -0
- package/dist/components/buttons/LiteDeleteButton.svelte +57 -0
- package/dist/components/buttons/LiteDeleteButton.svelte.d.ts +12 -0
- package/dist/components/buttons/LiteEditButton.svelte +33 -0
- package/dist/components/buttons/LiteEditButton.svelte.d.ts +11 -0
- package/dist/components/buttons/LiteExportButton.svelte +31 -0
- package/dist/components/buttons/LiteExportButton.svelte.d.ts +10 -0
- package/dist/components/buttons/LiteImportButton.svelte +50 -0
- package/dist/components/buttons/LiteImportButton.svelte.d.ts +10 -0
- package/dist/components/buttons/LiteListButton.svelte +31 -0
- package/dist/components/buttons/LiteListButton.svelte.d.ts +10 -0
- package/dist/components/buttons/LiteRefreshButton.svelte +34 -0
- package/dist/components/buttons/LiteRefreshButton.svelte.d.ts +8 -0
- package/dist/components/buttons/LiteSaveButton.svelte +27 -0
- package/dist/components/buttons/LiteSaveButton.svelte.d.ts +8 -0
- package/dist/components/buttons/LiteShowButton.svelte +33 -0
- package/dist/components/buttons/LiteShowButton.svelte.d.ts +11 -0
- package/dist/components/buttons/index.js +10 -0
- package/dist/components/fields/LiteBooleanField.svelte +42 -0
- package/dist/components/fields/LiteBooleanField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteDateField.svelte +51 -0
- package/dist/components/fields/LiteDateField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteEmailField.svelte +41 -0
- package/dist/components/fields/LiteEmailField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteFileField.svelte +59 -0
- package/dist/components/fields/LiteFileField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteImageField.svelte +57 -0
- package/dist/components/fields/LiteImageField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteJsonField.svelte +43 -0
- package/dist/components/fields/LiteJsonField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteMarkdownField.svelte +33 -0
- package/dist/components/fields/LiteMarkdownField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteMultiSelectField.svelte +57 -0
- package/dist/components/fields/LiteMultiSelectField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteNumberField.svelte +35 -0
- package/dist/components/fields/LiteNumberField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteRelationField.svelte +67 -0
- package/dist/components/fields/LiteRelationField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteRichTextField.svelte +44 -0
- package/dist/components/fields/LiteRichTextField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteSelectField.svelte +47 -0
- package/dist/components/fields/LiteSelectField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteTagField.svelte +44 -0
- package/dist/components/fields/LiteTagField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteTextField.svelte +35 -0
- package/dist/components/fields/LiteTextField.svelte.d.ts +10 -0
- package/dist/components/fields/LiteUrlField.svelte +52 -0
- package/dist/components/fields/LiteUrlField.svelte.d.ts +10 -0
- package/dist/components/fields/index.js +15 -0
- package/dist/components/layout/LiteAuthenticated.svelte +23 -0
- package/dist/components/layout/LiteAuthenticated.svelte.d.ts +10 -0
- package/dist/components/layout/LiteCanAccess.svelte +27 -0
- package/dist/components/layout/LiteCanAccess.svelte.d.ts +10 -0
- package/dist/components/layout/LiteCatchAllNavigate.svelte +20 -0
- package/dist/components/layout/LiteCatchAllNavigate.svelte.d.ts +7 -0
- package/dist/components/layout/LiteErrorBoundary.svelte +20 -0
- package/dist/components/layout/LiteErrorBoundary.svelte.d.ts +13 -0
- package/dist/components/layout/LiteErrorComponent.svelte +37 -0
- package/dist/components/layout/LiteErrorComponent.svelte.d.ts +9 -0
- package/dist/components/layout/LiteHeader.svelte +19 -0
- package/dist/components/layout/LiteHeader.svelte.d.ts +8 -0
- package/dist/components/layout/LiteNavigateToResource.svelte +25 -0
- package/dist/components/layout/LiteNavigateToResource.svelte.d.ts +7 -0
- package/dist/components/layout/LiteSidebar.svelte +93 -0
- package/dist/components/layout/LiteSidebar.svelte.d.ts +12 -0
- package/dist/components/layout/index.js +8 -0
- package/dist/components/pages/LiteCreatePage.svelte +39 -0
- package/dist/components/pages/LiteCreatePage.svelte.d.ts +10 -0
- package/dist/components/pages/LiteEditPage.svelte +54 -0
- package/dist/components/pages/LiteEditPage.svelte.d.ts +12 -0
- package/dist/components/pages/LiteForgotPasswordPage.svelte +60 -0
- package/dist/components/pages/LiteForgotPasswordPage.svelte.d.ts +11 -0
- package/dist/components/pages/LiteListPage.svelte +81 -0
- package/dist/components/pages/LiteListPage.svelte.d.ts +20 -0
- package/dist/components/pages/LiteProfilePage.svelte +61 -0
- package/dist/components/pages/LiteProfilePage.svelte.d.ts +14 -0
- package/dist/components/pages/LiteRegisterPage.svelte +63 -0
- package/dist/components/pages/LiteRegisterPage.svelte.d.ts +10 -0
- package/dist/components/pages/LiteShowPage.svelte +61 -0
- package/dist/components/pages/LiteShowPage.svelte.d.ts +11 -0
- package/dist/components/pages/LiteUpdatePasswordPage.svelte +51 -0
- package/dist/components/pages/LiteUpdatePasswordPage.svelte.d.ts +9 -0
- package/dist/components/pages/index.js +8 -0
- package/dist/components/widgets/LiteAnomalyBadge.svelte +40 -0
- package/dist/components/widgets/LiteAnomalyBadge.svelte.d.ts +15 -0
- package/dist/components/widgets/LiteBarChart.svelte +45 -0
- package/dist/components/widgets/LiteBarChart.svelte.d.ts +17 -0
- package/dist/components/widgets/LiteInsightCard.svelte +31 -0
- package/dist/components/widgets/LiteInsightCard.svelte.d.ts +13 -0
- package/dist/components/widgets/LiteLineChart.svelte +48 -0
- package/dist/components/widgets/LiteLineChart.svelte.d.ts +17 -0
- package/dist/components/widgets/LitePieChart.svelte +44 -0
- package/dist/components/widgets/LitePieChart.svelte.d.ts +17 -0
- package/dist/components/widgets/index.js +5 -0
- package/dist/enhance.js +112 -0
- package/dist/index.d.ts +27 -0
- package/{src/index.ts → dist/index.js} +3 -26
- package/dist/lite.css +817 -0
- package/dist/schema-generator.d.ts +29 -0
- package/dist/schema-generator.js +226 -0
- package/dist/security.d.ts +14 -0
- package/dist/security.js +59 -0
- package/dist/server-adapter.d.ts +111 -0
- package/dist/server-adapter.js +422 -0
- package/package.json +57 -20
- package/src/schema-generator.ts +0 -147
- package/src/server-adapter.ts +0 -307
- /package/{src/components/advanced/index.ts → dist/components/advanced/index.d.ts} +0 -0
- /package/{src/components/buttons/index.ts → dist/components/buttons/index.d.ts} +0 -0
- /package/{src/components/fields/index.ts → dist/components/fields/index.d.ts} +0 -0
- /package/{src/components/layout/index.ts → dist/components/layout/index.d.ts} +0 -0
- /package/{src/components/pages/index.ts → dist/components/pages/index.d.ts} +0 -0
- /package/{src/components/widgets/index.ts → dist/components/widgets/index.d.ts} +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { redirect, isRedirect } from '@sveltejs/kit';
|
|
2
|
+
import { resourceToZodSchema } from './schema-generator';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a SvelteKit `load` function that fetches a resource list
|
|
5
|
+
* via the DataProvider. All state is driven by URL search params.
|
|
6
|
+
*/
|
|
7
|
+
export function createListLoader(dp, resource) {
|
|
8
|
+
return async ({ url }) => {
|
|
9
|
+
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
|
|
10
|
+
const pageSize = resource.pageSize ?? 20;
|
|
11
|
+
const sort = url.searchParams.get('sort') ?? undefined;
|
|
12
|
+
const order = url.searchParams.get('order') ?? 'asc';
|
|
13
|
+
const search = url.searchParams.get('q') ?? undefined;
|
|
14
|
+
const sorters = sort ? [{ field: sort, order }] :
|
|
15
|
+
resource.defaultSort ? [resource.defaultSort] : [];
|
|
16
|
+
const filters = [];
|
|
17
|
+
if (search) {
|
|
18
|
+
const searchable = resource.fields.find((f) => f.searchable);
|
|
19
|
+
if (searchable) {
|
|
20
|
+
filters.push({ field: searchable.key, operator: 'contains', value: search });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const result = await dp.getList({
|
|
24
|
+
resource: resource.name,
|
|
25
|
+
pagination: { current: page, pageSize },
|
|
26
|
+
sorters,
|
|
27
|
+
filters,
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
records: result.data,
|
|
31
|
+
total: result.total,
|
|
32
|
+
page,
|
|
33
|
+
pageSize,
|
|
34
|
+
totalPages: Math.ceil(result.total / pageSize),
|
|
35
|
+
sort,
|
|
36
|
+
order,
|
|
37
|
+
search,
|
|
38
|
+
resource,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ─── Detail Loader ────────────────────────────────────────────
|
|
43
|
+
export function createDetailLoader(dp, resource) {
|
|
44
|
+
return async ({ params }) => {
|
|
45
|
+
const result = await dp.getOne({
|
|
46
|
+
resource: resource.name,
|
|
47
|
+
id: params.id,
|
|
48
|
+
});
|
|
49
|
+
return { record: result.data, resource };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ─── CRUD Form Actions ────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Creates SvelteKit form actions for create / update / delete.
|
|
55
|
+
* Works with standard `<form method="POST">` submissions — no JS needed.
|
|
56
|
+
*/
|
|
57
|
+
export function createCrudActions(dp, resource) {
|
|
58
|
+
const pk = resource.primaryKey ?? 'id';
|
|
59
|
+
return {
|
|
60
|
+
create: async ({ request }) => {
|
|
61
|
+
const formData = await request.formData();
|
|
62
|
+
const submittedValues = formDataToObject(formData, resource.fields);
|
|
63
|
+
const validation = validateFormVariables(resource, 'create', submittedValues);
|
|
64
|
+
if (!validation.success)
|
|
65
|
+
return validation.failure;
|
|
66
|
+
const variables = validation.data;
|
|
67
|
+
try {
|
|
68
|
+
const result = await dp.create({ resource: resource.name, variables });
|
|
69
|
+
return { success: true, id: result.data[pk] };
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (isRedirect(e))
|
|
73
|
+
throw e;
|
|
74
|
+
return { success: false, error: e.message, values: variables };
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
update: async ({ request }) => {
|
|
78
|
+
const formData = await request.formData();
|
|
79
|
+
const id = formData.get('_id');
|
|
80
|
+
formData.delete('_id');
|
|
81
|
+
const submittedValues = formDataToObject(formData, resource.fields);
|
|
82
|
+
const validation = validateFormVariables(resource, 'edit', submittedValues);
|
|
83
|
+
if (!validation.success)
|
|
84
|
+
return validation.failure;
|
|
85
|
+
const variables = validation.data;
|
|
86
|
+
try {
|
|
87
|
+
await dp.update({ resource: resource.name, id, variables });
|
|
88
|
+
return { success: true };
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
if (isRedirect(e))
|
|
92
|
+
throw e;
|
|
93
|
+
return { success: false, error: e.message, values: variables };
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
delete: async ({ request }) => {
|
|
97
|
+
const formData = await request.formData();
|
|
98
|
+
const id = formData.get('id');
|
|
99
|
+
const redirectTo = formData.get('redirect');
|
|
100
|
+
try {
|
|
101
|
+
await dp.deleteOne({ resource: resource.name, id });
|
|
102
|
+
if (redirectTo)
|
|
103
|
+
throw redirect(303, redirectTo);
|
|
104
|
+
return { success: true };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (isRedirect(e))
|
|
108
|
+
throw e;
|
|
109
|
+
return { success: false, error: e.message };
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ─── Auth Helpers ─────────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Creates a SvelteKit server hook that checks auth via AuthProvider
|
|
117
|
+
* and redirects unauthenticated users to a login page.
|
|
118
|
+
*/
|
|
119
|
+
export function createAuthGuard(authProvider, loginPath = '/lite/login') {
|
|
120
|
+
return async ({ event, resolve }) => {
|
|
121
|
+
// Skip auth check for the login page itself
|
|
122
|
+
if (event.url.pathname === loginPath) {
|
|
123
|
+
return resolve(event);
|
|
124
|
+
}
|
|
125
|
+
// Local Lite Session Verify
|
|
126
|
+
const session = event.cookies.get('svadmin-session');
|
|
127
|
+
if (!session) {
|
|
128
|
+
return new Response(null, {
|
|
129
|
+
status: 302,
|
|
130
|
+
headers: { Location: loginPath },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const check = await authProvider.check();
|
|
135
|
+
if (!check.authenticated) {
|
|
136
|
+
event.cookies.delete('svadmin-session', { path: '/' });
|
|
137
|
+
return new Response(null, {
|
|
138
|
+
status: 302,
|
|
139
|
+
headers: { Location: loginPath },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
event.cookies.delete('svadmin-session', { path: '/' });
|
|
145
|
+
return new Response(null, {
|
|
146
|
+
status: 302,
|
|
147
|
+
headers: { Location: loginPath },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return resolve(event);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Creates login/logout form actions using AuthProvider.
|
|
155
|
+
*/
|
|
156
|
+
export function createAuthActions(authProvider) {
|
|
157
|
+
return {
|
|
158
|
+
login: async ({ request, cookies }) => {
|
|
159
|
+
const formData = await request.formData();
|
|
160
|
+
const params = Object.fromEntries(formData);
|
|
161
|
+
try {
|
|
162
|
+
const result = await authProvider.login(params);
|
|
163
|
+
if (result.success) {
|
|
164
|
+
// Store a session indicator in a cookie
|
|
165
|
+
cookies.set('svadmin-session', 'active', {
|
|
166
|
+
path: '/',
|
|
167
|
+
httpOnly: true,
|
|
168
|
+
sameSite: 'lax',
|
|
169
|
+
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
170
|
+
});
|
|
171
|
+
throw redirect(303, result.redirectTo ?? '/lite');
|
|
172
|
+
}
|
|
173
|
+
return { success: false, error: result.error?.message ?? 'Login failed' };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (isRedirect(e))
|
|
177
|
+
throw e;
|
|
178
|
+
return { success: false, error: e.message };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
logout: async ({ cookies }) => {
|
|
182
|
+
try {
|
|
183
|
+
await authProvider.logout();
|
|
184
|
+
}
|
|
185
|
+
catch { /* ignore */ }
|
|
186
|
+
cookies.delete('svadmin-session', { path: '/' });
|
|
187
|
+
return { success: true };
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ─── UA Detection ─────────────────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Detects IE11 user agents for applications that maintain a dedicated fallback.
|
|
194
|
+
* This helper does not imply that Svelte 5 or the consuming app supports IE11.
|
|
195
|
+
*/
|
|
196
|
+
export function isLegacyBrowser(userAgent) {
|
|
197
|
+
return /MSIE|Trident|rv:11/.test(userAgent);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Creates an opt-in SvelteKit hook that redirects detected IE11 user agents.
|
|
201
|
+
* Consumers remain responsible for their own transpilation and browser support.
|
|
202
|
+
*/
|
|
203
|
+
export function createLegacyRedirectHook(litePrefix = '/lite') {
|
|
204
|
+
return async ({ event, resolve }) => {
|
|
205
|
+
const ua = event.request.headers.get('user-agent') ?? '';
|
|
206
|
+
if (isLegacyBrowser(ua) && !event.url.pathname.startsWith(litePrefix)) {
|
|
207
|
+
return new Response(null, {
|
|
208
|
+
status: 302,
|
|
209
|
+
headers: { Location: `${litePrefix}${event.url.pathname}` },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return resolve(event);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function formatValidationErrors(issues) {
|
|
216
|
+
const errors = {};
|
|
217
|
+
for (const issue of issues) {
|
|
218
|
+
const root = issue.path[0];
|
|
219
|
+
const field = typeof root === 'string' || typeof root === 'number' ? String(root) : '_form';
|
|
220
|
+
const messages = errors[field] ?? [];
|
|
221
|
+
if (!messages.includes(issue.message))
|
|
222
|
+
messages.push(issue.message);
|
|
223
|
+
errors[field] = messages;
|
|
224
|
+
}
|
|
225
|
+
return errors;
|
|
226
|
+
}
|
|
227
|
+
function validateFormVariables(resource, mode, values) {
|
|
228
|
+
const result = resourceToZodSchema(resource, mode).safeParse(values);
|
|
229
|
+
if (result.success)
|
|
230
|
+
return { success: true, data: result.data };
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
failure: {
|
|
234
|
+
success: false,
|
|
235
|
+
error: 'Validation failed',
|
|
236
|
+
values,
|
|
237
|
+
errors: formatValidationErrors(result.error.issues),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function isNativeFile(value) {
|
|
242
|
+
return typeof File !== 'undefined' && value instanceof File;
|
|
243
|
+
}
|
|
244
|
+
function isNonEmptyNativeFile(value) {
|
|
245
|
+
return isNativeFile(value) && value.size > 0 && value.name !== '';
|
|
246
|
+
}
|
|
247
|
+
function formDataToObject(formData, fields) {
|
|
248
|
+
const obj = {};
|
|
249
|
+
for (const field of fields) {
|
|
250
|
+
if (field.showInForm === false)
|
|
251
|
+
continue;
|
|
252
|
+
if (field.type === 'array') {
|
|
253
|
+
obj[field.key] = parseArrayField(formData, field);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (field.type === 'multiselect') {
|
|
257
|
+
const values = formData.getAll(field.key);
|
|
258
|
+
if (values.length > 0) {
|
|
259
|
+
obj[field.key] = values.map(v => String(v));
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (field.type === 'images') {
|
|
264
|
+
const values = formData
|
|
265
|
+
.getAll(field.key)
|
|
266
|
+
.filter((value) => !isNativeFile(value) || isNonEmptyNativeFile(value));
|
|
267
|
+
if (values.length > 0)
|
|
268
|
+
obj[field.key] = values;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const raw = formData.get(field.key);
|
|
272
|
+
if (raw === null) {
|
|
273
|
+
if (field.type === 'boolean')
|
|
274
|
+
obj[field.key] = false;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// Don't coerce File objects, but ignore empty native file inputs
|
|
278
|
+
if (isNativeFile(raw)) {
|
|
279
|
+
if (isNonEmptyNativeFile(raw)) {
|
|
280
|
+
obj[field.key] = raw;
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const strRaw = String(raw);
|
|
285
|
+
switch (field.type) {
|
|
286
|
+
case 'number':
|
|
287
|
+
if (strRaw.trim() !== '') {
|
|
288
|
+
const numberValue = Number(strRaw);
|
|
289
|
+
obj[field.key] = Number.isNaN(numberValue) ? strRaw : numberValue;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 'boolean':
|
|
293
|
+
obj[field.key] = strRaw === 'on' || strRaw === 'true' || strRaw === '1';
|
|
294
|
+
break;
|
|
295
|
+
case 'tags':
|
|
296
|
+
obj[field.key] = strRaw ? strRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
297
|
+
break;
|
|
298
|
+
case 'json':
|
|
299
|
+
try {
|
|
300
|
+
obj[field.key] = strRaw ? JSON.parse(strRaw) : null;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
obj[field.key] = strRaw; // Let Zod handle validation errors
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
default:
|
|
307
|
+
obj[field.key] = strRaw;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return obj;
|
|
311
|
+
}
|
|
312
|
+
function parseArrayField(formData, field) {
|
|
313
|
+
const escapedKey = field.key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
314
|
+
const entryPattern = new RegExp(`^${escapedKey}\\[(\\d+)\\]\\[([^\\]]+)\\]$`);
|
|
315
|
+
const rows = new Map();
|
|
316
|
+
for (const [name, value] of formData.entries()) {
|
|
317
|
+
const match = entryPattern.exec(name);
|
|
318
|
+
if (!match)
|
|
319
|
+
continue;
|
|
320
|
+
const index = Number(match[1]);
|
|
321
|
+
const key = match[2];
|
|
322
|
+
if (!Number.isSafeInteger(index) || index < 0 || !key)
|
|
323
|
+
continue;
|
|
324
|
+
const row = rows.get(index) ?? new Map();
|
|
325
|
+
const values = row.get(key) ?? [];
|
|
326
|
+
values.push(value);
|
|
327
|
+
row.set(key, values);
|
|
328
|
+
rows.set(index, row);
|
|
329
|
+
}
|
|
330
|
+
const result = [];
|
|
331
|
+
for (const [, entries] of [...rows.entries()].sort(([a], [b]) => a - b)) {
|
|
332
|
+
if (isChecked(entries.get('_delete')))
|
|
333
|
+
continue;
|
|
334
|
+
const item = {};
|
|
335
|
+
let hasMeaningfulValue = false;
|
|
336
|
+
for (const subField of field.subFields ?? []) {
|
|
337
|
+
const rawValues = entries.get(subField.key) ?? [];
|
|
338
|
+
if (subField.type === 'boolean') {
|
|
339
|
+
const value = isChecked(rawValues);
|
|
340
|
+
item[subField.key] = value;
|
|
341
|
+
hasMeaningfulValue ||= value;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (rawValues.length === 0)
|
|
345
|
+
continue;
|
|
346
|
+
const value = coerceFieldValues(subField, rawValues);
|
|
347
|
+
if (value === undefined)
|
|
348
|
+
continue;
|
|
349
|
+
item[subField.key] = value;
|
|
350
|
+
hasMeaningfulValue ||= isMeaningfulValue(value);
|
|
351
|
+
}
|
|
352
|
+
if (hasMeaningfulValue)
|
|
353
|
+
result.push(item);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
function isChecked(values) {
|
|
358
|
+
return (values ?? []).some((value) => {
|
|
359
|
+
if (typeof value !== 'string')
|
|
360
|
+
return false;
|
|
361
|
+
return value === 'on' || value === 'true' || value === '1';
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function isMeaningfulValue(value) {
|
|
365
|
+
if (value == null || value === '')
|
|
366
|
+
return false;
|
|
367
|
+
if (Array.isArray(value))
|
|
368
|
+
return value.length > 0;
|
|
369
|
+
if (isNativeFile(value))
|
|
370
|
+
return isNonEmptyNativeFile(value);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
function coerceFieldValues(field, rawValues) {
|
|
374
|
+
if (field.type === 'multiselect') {
|
|
375
|
+
return rawValues.filter((value) => typeof value === 'string');
|
|
376
|
+
}
|
|
377
|
+
if (field.type === 'images') {
|
|
378
|
+
const uploadedFiles = rawValues.filter(isNonEmptyNativeFile);
|
|
379
|
+
if (uploadedFiles.length > 0)
|
|
380
|
+
return uploadedFiles;
|
|
381
|
+
const retainedReferences = rawValues.filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
382
|
+
return retainedReferences.length > 0 ? retainedReferences : undefined;
|
|
383
|
+
}
|
|
384
|
+
if (field.type === 'file' || field.type === 'image') {
|
|
385
|
+
for (let index = rawValues.length - 1; index >= 0; index -= 1) {
|
|
386
|
+
const value = rawValues[index];
|
|
387
|
+
if (isNonEmptyNativeFile(value))
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
for (let index = rawValues.length - 1; index >= 0; index -= 1) {
|
|
391
|
+
const value = rawValues[index];
|
|
392
|
+
if (typeof value === 'string' && value.trim().length > 0)
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
const raw = rawValues.at(-1);
|
|
398
|
+
if (raw === undefined)
|
|
399
|
+
return undefined;
|
|
400
|
+
if (isNativeFile(raw)) {
|
|
401
|
+
return isNonEmptyNativeFile(raw) ? raw : undefined;
|
|
402
|
+
}
|
|
403
|
+
switch (field.type) {
|
|
404
|
+
case 'number':
|
|
405
|
+
if (raw.trim() === '')
|
|
406
|
+
return undefined;
|
|
407
|
+
return Number.isNaN(Number(raw)) ? raw : Number(raw);
|
|
408
|
+
case 'boolean':
|
|
409
|
+
return raw === 'on' || raw === 'true' || raw === '1';
|
|
410
|
+
case 'tags':
|
|
411
|
+
return raw ? raw.split(',').map((value) => value.trim()).filter(Boolean) : [];
|
|
412
|
+
case 'json':
|
|
413
|
+
try {
|
|
414
|
+
return raw ? JSON.parse(raw) : null;
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return raw;
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
return raw;
|
|
421
|
+
}
|
|
422
|
+
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "SSR-
|
|
3
|
+
"version": "0.3.12",
|
|
4
|
+
"description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./dist/lite.css",
|
|
8
|
+
"./dist/enhance.js"
|
|
9
|
+
],
|
|
6
10
|
"files": [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"!
|
|
10
|
-
"!
|
|
11
|
-
"!
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"!dist/**/*.test.*",
|
|
14
|
+
"!dist/**/*.test-*",
|
|
15
|
+
"!dist/**/*.spec.*",
|
|
16
|
+
"!dist/**/*.spec-*"
|
|
12
17
|
],
|
|
13
|
-
"main": "
|
|
14
|
-
"types": "
|
|
15
|
-
"svelte": "./
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"svelte": "./dist/index.js",
|
|
16
21
|
"exports": {
|
|
17
22
|
".": {
|
|
18
|
-
"types": "./
|
|
19
|
-
"svelte": "./
|
|
20
|
-
"default": "./
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"svelte": "./dist/index.js",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./components/*.svelte": {
|
|
28
|
+
"types": "./dist/components/*.svelte.d.ts",
|
|
29
|
+
"svelte": "./dist/components/*.svelte",
|
|
30
|
+
"default": "./dist/components/*.svelte"
|
|
31
|
+
},
|
|
32
|
+
"./components/*": "./dist/components/*",
|
|
33
|
+
"./enhance.js": {
|
|
34
|
+
"import": "./dist/enhance.js",
|
|
35
|
+
"default": "./dist/enhance.js"
|
|
36
|
+
},
|
|
37
|
+
"./lite.css": {
|
|
38
|
+
"svelte": "./dist/lite.css",
|
|
39
|
+
"style": "./dist/lite.css",
|
|
40
|
+
"import": "./dist/lite.css",
|
|
41
|
+
"default": "./dist/lite.css"
|
|
21
42
|
},
|
|
22
|
-
"./
|
|
23
|
-
|
|
43
|
+
"./src/lite.css": {
|
|
44
|
+
"svelte": "./dist/lite.css",
|
|
45
|
+
"style": "./dist/lite.css",
|
|
46
|
+
"import": "./dist/lite.css",
|
|
47
|
+
"default": "./dist/lite.css"
|
|
48
|
+
}
|
|
24
49
|
},
|
|
25
50
|
"peerDependencies": {
|
|
26
|
-
"svelte": "^5.
|
|
27
|
-
"@svadmin/core": "^0.
|
|
28
|
-
"@sveltejs/kit": "^2.
|
|
51
|
+
"svelte": "^5.56.8",
|
|
52
|
+
"@svadmin/core": "^0.34.2",
|
|
53
|
+
"@sveltejs/kit": "^2.70.2"
|
|
29
54
|
},
|
|
30
55
|
"dependencies": {
|
|
31
|
-
"@lucide/svelte": "^1.
|
|
32
|
-
"sveltekit-superforms": "^2.30.
|
|
56
|
+
"@lucide/svelte": "^1.30.0",
|
|
57
|
+
"sveltekit-superforms": "^2.30.2",
|
|
33
58
|
"zod": "^4.4.3"
|
|
34
59
|
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@happy-dom/global-registrator": "^20.11.2",
|
|
62
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
63
|
+
"@testing-library/dom": "^10.4.1",
|
|
64
|
+
"@testing-library/svelte": "^5.4.2",
|
|
65
|
+
"happy-dom": "^20.11.2",
|
|
66
|
+
"vitest": "^4.1.10"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist && bun ../../scripts/copy-package-assets.ts static/enhance.js dist/enhance.js",
|
|
70
|
+
"test": "vitest run --config ./vitest.config.ts"
|
|
71
|
+
},
|
|
35
72
|
"license": "MIT",
|
|
36
73
|
"publishConfig": {
|
|
37
74
|
"access": "public",
|
package/src/schema-generator.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @svadmin/lite — Schema Generator
|
|
3
|
-
*
|
|
4
|
-
* Auto-generates Zod schemas from @svadmin/core FieldDefinitions.
|
|
5
|
-
* Used with sveltekit-superforms for server-side form validation.
|
|
6
|
-
*/
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Convert a single FieldDefinition to its corresponding Zod type.
|
|
12
|
-
*/
|
|
13
|
-
function fieldToZod(field: FieldDefinition): z.ZodTypeAny {
|
|
14
|
-
let schema: z.ZodTypeAny;
|
|
15
|
-
|
|
16
|
-
switch (field.type) {
|
|
17
|
-
case 'number':
|
|
18
|
-
schema = z.coerce.number({ message: `${field.label} must be a number` });
|
|
19
|
-
break;
|
|
20
|
-
case 'boolean':
|
|
21
|
-
schema = z.coerce.boolean();
|
|
22
|
-
break;
|
|
23
|
-
case 'email':
|
|
24
|
-
schema = z.string().email(`${field.label} must be a valid email`);
|
|
25
|
-
break;
|
|
26
|
-
case 'url':
|
|
27
|
-
schema = z.string().url(`${field.label} must be a valid URL`);
|
|
28
|
-
break;
|
|
29
|
-
case 'date':
|
|
30
|
-
schema = z.string().refine(
|
|
31
|
-
(v: string | undefined) => !v || !isNaN(Date.parse(v)),
|
|
32
|
-
{ message: `${field.label} must be a valid date` },
|
|
33
|
-
);
|
|
34
|
-
break;
|
|
35
|
-
case 'select':
|
|
36
|
-
if (field.options?.length) {
|
|
37
|
-
schema = z.enum(
|
|
38
|
-
field.options.map((o: { value: string | number }) => String(o.value)) as [string, ...string[]],
|
|
39
|
-
{ message: `${field.label} must be one of the options` },
|
|
40
|
-
);
|
|
41
|
-
} else {
|
|
42
|
-
schema = z.string();
|
|
43
|
-
}
|
|
44
|
-
break;
|
|
45
|
-
case 'multiselect':
|
|
46
|
-
schema = z.array(z.string()).default([]);
|
|
47
|
-
break;
|
|
48
|
-
case 'tags':
|
|
49
|
-
schema = z.string().transform((v: string) => v ? v.split(',').map((s: string) => s.trim()) : []);
|
|
50
|
-
break;
|
|
51
|
-
case 'textarea':
|
|
52
|
-
case 'richtext':
|
|
53
|
-
schema = z.string().max(50000, `${field.label} is too long`);
|
|
54
|
-
break;
|
|
55
|
-
case 'json':
|
|
56
|
-
schema = z.string().refine(
|
|
57
|
-
(v: string) => { try { JSON.parse(v); return true; } catch { return false; } },
|
|
58
|
-
{ message: `${field.label} must be valid JSON` },
|
|
59
|
-
);
|
|
60
|
-
break;
|
|
61
|
-
case 'phone':
|
|
62
|
-
schema = z.string().regex(/^[+\d\s()-]*$/, `${field.label} must be a valid phone number`);
|
|
63
|
-
break;
|
|
64
|
-
default:
|
|
65
|
-
schema = z.string();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Wrap with optional/required
|
|
69
|
-
if (!field.required) {
|
|
70
|
-
schema = schema.optional().or(z.literal(''));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return schema;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Generate a Zod object schema from a ResourceDefinition's fields.
|
|
78
|
-
* Only includes fields that are relevant for form rendering.
|
|
79
|
-
*
|
|
80
|
-
* @param mode - 'create' | 'edit' to filter fields by showInCreate / showInEdit
|
|
81
|
-
*/
|
|
82
|
-
export function fieldsToZodSchema(
|
|
83
|
-
fields: FieldDefinition[],
|
|
84
|
-
mode: 'create' | 'edit' = 'create',
|
|
85
|
-
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
86
|
-
const shape: Record<string, z.ZodTypeAny> = {};
|
|
87
|
-
|
|
88
|
-
for (const field of fields) {
|
|
89
|
-
// Skip fields not shown in forms
|
|
90
|
-
if (field.showInForm === false) continue;
|
|
91
|
-
if (mode === 'create' && field.showInCreate === false) continue;
|
|
92
|
-
if (mode === 'edit' && field.showInEdit === false) continue;
|
|
93
|
-
|
|
94
|
-
shape[field.key] = fieldToZod(field);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return z.object(shape);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Generate a Zod schema from a ResourceDefinition.
|
|
102
|
-
* Convenience wrapper around fieldsToZodSchema.
|
|
103
|
-
*/
|
|
104
|
-
export function resourceToZodSchema(
|
|
105
|
-
resource: ResourceDefinition,
|
|
106
|
-
mode: 'create' | 'edit' = 'create',
|
|
107
|
-
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
108
|
-
return fieldsToZodSchema(resource.fields, mode);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Determine the appropriate HTML input type for an IE11-friendly `<input>`.
|
|
113
|
-
* Falls back to 'text' for unsupported HTML5 types.
|
|
114
|
-
*/
|
|
115
|
-
export function fieldToInputType(field: FieldDefinition): string {
|
|
116
|
-
switch (field.type) {
|
|
117
|
-
case 'number': return 'text'; // IE11 <input type="number"> has bugs
|
|
118
|
-
case 'email': return 'text'; // IE11 email validation is broken
|
|
119
|
-
case 'url': return 'text';
|
|
120
|
-
case 'phone': return 'tel';
|
|
121
|
-
case 'boolean': return 'checkbox';
|
|
122
|
-
case 'date': return 'text'; // IE11 doesn't support type="date"
|
|
123
|
-
case 'textarea':
|
|
124
|
-
case 'richtext': return 'textarea';
|
|
125
|
-
case 'select':
|
|
126
|
-
case 'multiselect': return 'select';
|
|
127
|
-
case 'image':
|
|
128
|
-
case 'images':
|
|
129
|
-
case 'file': return 'file';
|
|
130
|
-
default: return 'text';
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Generate an input placeholder with format hint for IE11
|
|
136
|
-
* (which cannot show native date pickers, etc.)
|
|
137
|
-
*/
|
|
138
|
-
export function fieldToPlaceholder(field: FieldDefinition): string {
|
|
139
|
-
switch (field.type) {
|
|
140
|
-
case 'date': return 'YYYY-MM-DD';
|
|
141
|
-
case 'email': return 'user@example.com';
|
|
142
|
-
case 'url': return 'https://example.com';
|
|
143
|
-
case 'phone': return '+1 (555) 000-0000';
|
|
144
|
-
case 'number': return '0';
|
|
145
|
-
default: return '';
|
|
146
|
-
}
|
|
147
|
-
}
|