@svadmin/lite 0.3.6 → 0.3.10

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 (172) hide show
  1. package/README.md +19 -13
  2. package/dist/components/LiteAlert.svelte +19 -0
  3. package/dist/components/LiteAlert.svelte.d.ts +11 -0
  4. package/dist/components/LiteArrayField.svelte +60 -0
  5. package/dist/components/LiteArrayField.svelte.d.ts +17 -0
  6. package/dist/components/LiteArrayItem.svelte +146 -0
  7. package/dist/components/LiteArrayItem.svelte.d.ts +13 -0
  8. package/dist/components/LiteAuditLog.svelte +104 -0
  9. package/dist/components/LiteAuditLog.svelte.d.ts +23 -0
  10. package/dist/components/LiteBreadcrumbs.svelte +39 -0
  11. package/dist/components/LiteBreadcrumbs.svelte.d.ts +16 -0
  12. package/dist/components/LiteChatDialog.svelte +101 -0
  13. package/dist/components/LiteChatDialog.svelte.d.ts +20 -0
  14. package/dist/components/LiteConfirmDialog.svelte +60 -0
  15. package/dist/components/LiteConfirmDialog.svelte.d.ts +22 -0
  16. package/dist/components/LiteEmptyState.svelte +39 -0
  17. package/dist/components/LiteEmptyState.svelte.d.ts +16 -0
  18. package/dist/components/LiteForm.svelte +165 -0
  19. package/dist/components/LiteForm.svelte.d.ts +26 -0
  20. package/dist/components/LiteLayout.svelte +147 -0
  21. package/dist/components/LiteLayout.svelte.d.ts +19 -0
  22. package/dist/components/LiteLogin.svelte +41 -0
  23. package/dist/components/LiteLogin.svelte.d.ts +8 -0
  24. package/dist/components/LitePagination.svelte +73 -0
  25. package/dist/components/LitePagination.svelte.d.ts +11 -0
  26. package/dist/components/LitePermissionMatrix.svelte +147 -0
  27. package/dist/components/LitePermissionMatrix.svelte.d.ts +32 -0
  28. package/dist/components/LiteSearch.svelte +41 -0
  29. package/dist/components/LiteSearch.svelte.d.ts +9 -0
  30. package/dist/components/LiteShow.svelte +61 -0
  31. package/dist/components/LiteShow.svelte.d.ts +14 -0
  32. package/dist/components/LiteShowField.svelte +62 -0
  33. package/dist/components/LiteShowField.svelte.d.ts +12 -0
  34. package/dist/components/LiteStatsCard.svelte +70 -0
  35. package/dist/components/LiteStatsCard.svelte.d.ts +24 -0
  36. package/dist/components/LiteTable.svelte +132 -0
  37. package/dist/components/LiteTable.svelte.d.ts +19 -0
  38. package/dist/components/LiteTabs.svelte +57 -0
  39. package/dist/components/LiteTabs.svelte.d.ts +16 -0
  40. package/dist/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
  41. package/dist/components/advanced/LiteAutoSaveIndicator.svelte.d.ts +12 -0
  42. package/dist/components/advanced/LiteDraggableHeader.svelte +33 -0
  43. package/dist/components/advanced/LiteDraggableHeader.svelte.d.ts +15 -0
  44. package/dist/components/advanced/LiteDrawerForm.svelte +42 -0
  45. package/dist/components/advanced/LiteDrawerForm.svelte.d.ts +16 -0
  46. package/dist/components/advanced/LiteInlineEdit.svelte +32 -0
  47. package/dist/components/advanced/LiteInlineEdit.svelte.d.ts +15 -0
  48. package/dist/components/advanced/LiteModalForm.svelte +44 -0
  49. package/dist/components/advanced/LiteModalForm.svelte.d.ts +17 -0
  50. package/dist/components/advanced/LiteToast.svelte +34 -0
  51. package/dist/components/advanced/LiteToast.svelte.d.ts +14 -0
  52. package/dist/components/advanced/LiteUndoableNotification.svelte +25 -0
  53. package/dist/components/advanced/LiteUndoableNotification.svelte.d.ts +13 -0
  54. package/dist/components/advanced/LiteVirtualTable.svelte +44 -0
  55. package/dist/components/advanced/LiteVirtualTable.svelte.d.ts +20 -0
  56. package/dist/components/advanced/index.js +8 -0
  57. package/dist/components/buttons/LiteCloneButton.svelte +33 -0
  58. package/dist/components/buttons/LiteCloneButton.svelte.d.ts +11 -0
  59. package/dist/components/buttons/LiteCreateButton.svelte +31 -0
  60. package/dist/components/buttons/LiteCreateButton.svelte.d.ts +10 -0
  61. package/dist/components/buttons/LiteDeleteButton.svelte +57 -0
  62. package/dist/components/buttons/LiteDeleteButton.svelte.d.ts +12 -0
  63. package/dist/components/buttons/LiteEditButton.svelte +33 -0
  64. package/dist/components/buttons/LiteEditButton.svelte.d.ts +11 -0
  65. package/dist/components/buttons/LiteExportButton.svelte +31 -0
  66. package/dist/components/buttons/LiteExportButton.svelte.d.ts +10 -0
  67. package/dist/components/buttons/LiteImportButton.svelte +50 -0
  68. package/dist/components/buttons/LiteImportButton.svelte.d.ts +10 -0
  69. package/dist/components/buttons/LiteListButton.svelte +31 -0
  70. package/dist/components/buttons/LiteListButton.svelte.d.ts +10 -0
  71. package/dist/components/buttons/LiteRefreshButton.svelte +34 -0
  72. package/dist/components/buttons/LiteRefreshButton.svelte.d.ts +8 -0
  73. package/dist/components/buttons/LiteSaveButton.svelte +27 -0
  74. package/dist/components/buttons/LiteSaveButton.svelte.d.ts +8 -0
  75. package/dist/components/buttons/LiteShowButton.svelte +33 -0
  76. package/dist/components/buttons/LiteShowButton.svelte.d.ts +11 -0
  77. package/dist/components/buttons/index.js +10 -0
  78. package/dist/components/fields/LiteBooleanField.svelte +42 -0
  79. package/dist/components/fields/LiteBooleanField.svelte.d.ts +10 -0
  80. package/dist/components/fields/LiteDateField.svelte +51 -0
  81. package/dist/components/fields/LiteDateField.svelte.d.ts +10 -0
  82. package/dist/components/fields/LiteEmailField.svelte +41 -0
  83. package/dist/components/fields/LiteEmailField.svelte.d.ts +10 -0
  84. package/dist/components/fields/LiteFileField.svelte +59 -0
  85. package/dist/components/fields/LiteFileField.svelte.d.ts +10 -0
  86. package/dist/components/fields/LiteImageField.svelte +57 -0
  87. package/dist/components/fields/LiteImageField.svelte.d.ts +10 -0
  88. package/dist/components/fields/LiteJsonField.svelte +43 -0
  89. package/dist/components/fields/LiteJsonField.svelte.d.ts +10 -0
  90. package/dist/components/fields/LiteMarkdownField.svelte +33 -0
  91. package/dist/components/fields/LiteMarkdownField.svelte.d.ts +10 -0
  92. package/dist/components/fields/LiteMultiSelectField.svelte +57 -0
  93. package/dist/components/fields/LiteMultiSelectField.svelte.d.ts +10 -0
  94. package/dist/components/fields/LiteNumberField.svelte +35 -0
  95. package/dist/components/fields/LiteNumberField.svelte.d.ts +10 -0
  96. package/dist/components/fields/LiteRelationField.svelte +67 -0
  97. package/dist/components/fields/LiteRelationField.svelte.d.ts +10 -0
  98. package/dist/components/fields/LiteRichTextField.svelte +44 -0
  99. package/dist/components/fields/LiteRichTextField.svelte.d.ts +10 -0
  100. package/dist/components/fields/LiteSelectField.svelte +47 -0
  101. package/dist/components/fields/LiteSelectField.svelte.d.ts +10 -0
  102. package/dist/components/fields/LiteTagField.svelte +44 -0
  103. package/dist/components/fields/LiteTagField.svelte.d.ts +10 -0
  104. package/dist/components/fields/LiteTextField.svelte +35 -0
  105. package/dist/components/fields/LiteTextField.svelte.d.ts +10 -0
  106. package/dist/components/fields/LiteUrlField.svelte +52 -0
  107. package/dist/components/fields/LiteUrlField.svelte.d.ts +10 -0
  108. package/dist/components/fields/index.js +15 -0
  109. package/dist/components/layout/LiteAuthenticated.svelte +23 -0
  110. package/dist/components/layout/LiteAuthenticated.svelte.d.ts +10 -0
  111. package/dist/components/layout/LiteCanAccess.svelte +27 -0
  112. package/dist/components/layout/LiteCanAccess.svelte.d.ts +10 -0
  113. package/dist/components/layout/LiteCatchAllNavigate.svelte +20 -0
  114. package/dist/components/layout/LiteCatchAllNavigate.svelte.d.ts +7 -0
  115. package/dist/components/layout/LiteErrorBoundary.svelte +20 -0
  116. package/dist/components/layout/LiteErrorBoundary.svelte.d.ts +13 -0
  117. package/dist/components/layout/LiteErrorComponent.svelte +37 -0
  118. package/dist/components/layout/LiteErrorComponent.svelte.d.ts +9 -0
  119. package/dist/components/layout/LiteHeader.svelte +19 -0
  120. package/dist/components/layout/LiteHeader.svelte.d.ts +8 -0
  121. package/dist/components/layout/LiteNavigateToResource.svelte +25 -0
  122. package/dist/components/layout/LiteNavigateToResource.svelte.d.ts +7 -0
  123. package/dist/components/layout/LiteSidebar.svelte +93 -0
  124. package/dist/components/layout/LiteSidebar.svelte.d.ts +12 -0
  125. package/dist/components/layout/index.js +8 -0
  126. package/dist/components/pages/LiteCreatePage.svelte +39 -0
  127. package/dist/components/pages/LiteCreatePage.svelte.d.ts +10 -0
  128. package/dist/components/pages/LiteEditPage.svelte +54 -0
  129. package/dist/components/pages/LiteEditPage.svelte.d.ts +12 -0
  130. package/dist/components/pages/LiteForgotPasswordPage.svelte +60 -0
  131. package/dist/components/pages/LiteForgotPasswordPage.svelte.d.ts +11 -0
  132. package/dist/components/pages/LiteListPage.svelte +81 -0
  133. package/dist/components/pages/LiteListPage.svelte.d.ts +20 -0
  134. package/dist/components/pages/LiteProfilePage.svelte +61 -0
  135. package/dist/components/pages/LiteProfilePage.svelte.d.ts +14 -0
  136. package/dist/components/pages/LiteRegisterPage.svelte +63 -0
  137. package/dist/components/pages/LiteRegisterPage.svelte.d.ts +10 -0
  138. package/dist/components/pages/LiteShowPage.svelte +61 -0
  139. package/dist/components/pages/LiteShowPage.svelte.d.ts +11 -0
  140. package/dist/components/pages/LiteUpdatePasswordPage.svelte +51 -0
  141. package/dist/components/pages/LiteUpdatePasswordPage.svelte.d.ts +9 -0
  142. package/dist/components/pages/index.js +8 -0
  143. package/dist/components/widgets/LiteAnomalyBadge.svelte +40 -0
  144. package/dist/components/widgets/LiteAnomalyBadge.svelte.d.ts +15 -0
  145. package/dist/components/widgets/LiteBarChart.svelte +45 -0
  146. package/dist/components/widgets/LiteBarChart.svelte.d.ts +17 -0
  147. package/dist/components/widgets/LiteInsightCard.svelte +31 -0
  148. package/dist/components/widgets/LiteInsightCard.svelte.d.ts +13 -0
  149. package/dist/components/widgets/LiteLineChart.svelte +48 -0
  150. package/dist/components/widgets/LiteLineChart.svelte.d.ts +17 -0
  151. package/dist/components/widgets/LitePieChart.svelte +44 -0
  152. package/dist/components/widgets/LitePieChart.svelte.d.ts +17 -0
  153. package/dist/components/widgets/index.js +5 -0
  154. package/dist/enhance.js +112 -0
  155. package/dist/index.d.ts +27 -0
  156. package/{src/index.ts → dist/index.js} +3 -26
  157. package/dist/lite.css +817 -0
  158. package/dist/schema-generator.d.ts +29 -0
  159. package/dist/schema-generator.js +226 -0
  160. package/dist/security.d.ts +14 -0
  161. package/dist/security.js +59 -0
  162. package/dist/server-adapter.d.ts +111 -0
  163. package/dist/server-adapter.js +422 -0
  164. package/package.json +55 -18
  165. package/src/schema-generator.ts +0 -147
  166. package/src/server-adapter.ts +0 -307
  167. /package/{src/components/advanced/index.ts → dist/components/advanced/index.d.ts} +0 -0
  168. /package/{src/components/buttons/index.ts → dist/components/buttons/index.d.ts} +0 -0
  169. /package/{src/components/fields/index.ts → dist/components/fields/index.d.ts} +0 -0
  170. /package/{src/components/layout/index.ts → dist/components/layout/index.d.ts} +0 -0
  171. /package/{src/components/pages/index.ts → dist/components/pages/index.d.ts} +0 -0
  172. /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.6",
4
- "description": "SSR-compatible lightweight admin UI for @svadmin zero client-side JS, works in IE11",
3
+ "version": "0.3.10",
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
- "src/**/*.ts",
8
- "!src/**/*.test.*",
9
- "!src/**/*.spec.*",
10
- "!src/**/vitest.config.*",
11
- "!src/**/setupTest.*"
11
+ "dist",
12
+ "README.md",
13
+ "!dist/**/*.test.*",
14
+ "!dist/**/*.test-*",
15
+ "!dist/**/*.spec.*",
16
+ "!dist/**/*.spec-*"
12
17
  ],
13
- "main": "src/index.ts",
14
- "types": "src/index.ts",
15
- "svelte": "./src/index.ts",
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "svelte": "./dist/index.js",
16
21
  "exports": {
17
22
  ".": {
18
- "types": "./src/index.ts",
19
- "svelte": "./src/index.ts",
20
- "default": "./src/index.ts"
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
- "./enhance.js": "./static/enhance.js",
23
- "./src/lite.css": "./src/lite.css"
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
51
  "svelte": "^5.0.0",
27
- "@svadmin/core": "^0.27.1",
52
+ "@svadmin/core": "^0.32.2",
28
53
  "@sveltejs/kit": "^2.0.0"
29
54
  },
30
55
  "dependencies": {
31
- "@lucide/svelte": "^1.17.0",
32
- "sveltekit-superforms": "^2.30.1",
56
+ "@lucide/svelte": "^1.25.0",
57
+ "sveltekit-superforms": "^2.30.2",
33
58
  "zod": "^4.4.3"
34
59
  },
60
+ "devDependencies": {
61
+ "@happy-dom/global-registrator": "^20.11.0",
62
+ "@sveltejs/vite-plugin-svelte": "^7.2.0",
63
+ "@testing-library/dom": "^10.4.1",
64
+ "@testing-library/svelte": "^5.4.2",
65
+ "happy-dom": "^20.11.0",
66
+ "vitest": "^4.1.10"
67
+ },
68
+ "scripts": {
69
+ "build": "svelte-package -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",
@@ -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
- }