@svadmin/lite 0.3.27 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +106 -1
  2. package/dist/compatibility.d.ts +23 -0
  3. package/dist/compatibility.js +95 -0
  4. package/dist/components/LiteForm.svelte +1 -1
  5. package/dist/components/LiteForm.svelte.d.ts +1 -1
  6. package/dist/components/LiteShowField.svelte +32 -22
  7. package/dist/components/LiteShowField.svelte.d.ts +3 -2
  8. package/dist/components/LiteTable.svelte +90 -37
  9. package/dist/components/LiteTable.svelte.d.ts +3 -2
  10. package/dist/components/compatibility/LiteCapabilityBoundary.svelte +51 -0
  11. package/dist/components/compatibility/LiteCapabilityBoundary.svelte.d.ts +13 -0
  12. package/dist/components/compatibility/LiteClipboardFallback.svelte +16 -0
  13. package/dist/components/compatibility/LiteClipboardFallback.svelte.d.ts +8 -0
  14. package/dist/components/compatibility/LiteComputeFallback.svelte +50 -0
  15. package/dist/components/compatibility/LiteComputeFallback.svelte.d.ts +14 -0
  16. package/dist/components/compatibility/LiteDirectoryUpload.svelte +44 -0
  17. package/dist/components/compatibility/LiteDirectoryUpload.svelte.d.ts +11 -0
  18. package/dist/components/compatibility/LiteOrderedList.svelte +47 -0
  19. package/dist/components/compatibility/LiteOrderedList.svelte.d.ts +14 -0
  20. package/dist/components/compatibility/LiteRealtimeStatus.svelte +43 -0
  21. package/dist/components/compatibility/LiteRealtimeStatus.svelte.d.ts +11 -0
  22. package/dist/components/compatibility/LiteVisualFallback.svelte +80 -0
  23. package/dist/components/compatibility/LiteVisualFallback.svelte.d.ts +18 -0
  24. package/dist/components/compatibility/index.d.ts +7 -0
  25. package/dist/components/compatibility/index.js +7 -0
  26. package/dist/components/pages/LiteCreatePage.svelte +15 -6
  27. package/dist/components/pages/LiteCreatePage.svelte.d.ts +1 -1
  28. package/dist/components/pages/LiteEditPage.svelte +18 -9
  29. package/dist/components/pages/LiteEditPage.svelte.d.ts +1 -1
  30. package/dist/components/pages/LiteListPage.svelte +86 -16
  31. package/dist/components/pages/LiteListPage.svelte.d.ts +4 -2
  32. package/dist/components/pages/LiteShowPage.svelte +19 -10
  33. package/dist/components/pages/LiteShowPage.svelte.d.ts +1 -1
  34. package/dist/index.d.ts +7 -3
  35. package/dist/index.js +8 -3
  36. package/dist/lite.css +269 -0
  37. package/dist/schema-generator.d.ts +49 -13
  38. package/dist/schema-generator.js +381 -199
  39. package/dist/server-adapter.d.ts +25 -1
  40. package/dist/server-adapter.js +132 -33
  41. package/dist/value-normalization.d.ts +1 -0
  42. package/dist/value-normalization.js +18 -0
  43. package/package.json +10 -10
@@ -24,6 +24,7 @@ export interface ListLoaderResult {
24
24
  currentSort?: string;
25
25
  currentOrder?: 'asc' | 'desc';
26
26
  currentSearch?: string;
27
+ currentFilters?: Record<string, string>;
27
28
  resource: ResourceDefinition;
28
29
  }
29
30
  /**
@@ -92,6 +93,13 @@ export declare function createCrudActions(dp: DataProvider, resource: ResourceDe
92
93
  success: boolean;
93
94
  error?: undefined;
94
95
  }>;
96
+ batchDelete: ({ request }: RequestEvent) => Promise<{
97
+ success: boolean;
98
+ error: string;
99
+ } | {
100
+ success: boolean;
101
+ error?: undefined;
102
+ }>;
95
103
  };
96
104
  /**
97
105
  * Creates a SvelteKit server hook that checks auth via AuthProvider
@@ -150,11 +158,27 @@ export declare function createAuthActions(authProvider: AuthProvider): {
150
158
  * This helper does not imply that Svelte 5 or the consuming app supports IE11.
151
159
  */
152
160
  export declare function isLegacyBrowser(userAgent: string): boolean;
161
+ export interface LegacyRedirectOptions {
162
+ /** Route prefix that contains the standalone SSR Lite application. */
163
+ litePrefix?: string;
164
+ /** Optional route prefix occupied by the modern SPA, for example `/admin`. */
165
+ spaPrefix?: string;
166
+ /** Paths that must remain outside automatic legacy routing. */
167
+ exclude?: string[];
168
+ /** Override the SPA-to-Lite path mapping without changing SPA code. */
169
+ mapPath?: (pathname: string) => string;
170
+ status?: 302 | 307;
171
+ }
172
+ /**
173
+ * Computes a legacy redirect without importing or modifying the modern SPA.
174
+ * This pure helper can be used from non-SvelteKit server middleware.
175
+ */
176
+ export declare function getLegacyRedirectLocation(request: Pick<Request, 'method' | 'headers'>, url: URL, options?: string | LegacyRedirectOptions): string | undefined;
153
177
  /**
154
178
  * Creates an opt-in SvelteKit hook that redirects detected IE11 user agents.
155
179
  * Consumers remain responsible for their own transpilation and browser support.
156
180
  */
157
- export declare function createLegacyRedirectHook(litePrefix?: string): ({ event, resolve }: {
181
+ export declare function createLegacyRedirectHook(options?: string | LegacyRedirectOptions): ({ event, resolve }: {
158
182
  event: RequestEvent;
159
183
  resolve: (event: RequestEvent) => Promise<Response>;
160
184
  }) => Promise<Response>;
@@ -1,5 +1,5 @@
1
1
  import { redirect, isRedirect } from '@sveltejs/kit';
2
- import { resourceToZodSchema } from './schema-generator';
2
+ import { resourceToTypeBoxSchema } from './schema-generator';
3
3
  import { parseExplicitBoolean } from './value-normalization';
4
4
  function listRequestState(url, resource) {
5
5
  const requestedPage = Number(url.searchParams.get('page'));
@@ -19,19 +19,45 @@ function listRequestState(url, resource) {
19
19
  : resource.defaultSort?.order ?? 'asc';
20
20
  return { page, pageSize, sort, order, search: url.searchParams.get('q') ?? undefined };
21
21
  }
22
- function listSearchFilters(resource, search) {
23
- if (!search)
24
- return [];
25
- const fieldFilters = resource.fields
26
- .filter((field) => field.searchable)
27
- .map((field) => ({
28
- field: field.key,
29
- operator: 'contains',
30
- value: search,
31
- }));
32
- if (fieldFilters.length === 1)
33
- return fieldFilters;
34
- return fieldFilters.length > 1 ? [{ operator: 'or', value: fieldFilters }] : [];
22
+ function listRequestFilterValues(resource, url) {
23
+ const values = {};
24
+ for (const field of resource.fields) {
25
+ const raw = url.searchParams.get(`filter_${field.key}`) ?? (field.key !== "q" && field.key !== "sort" && field.key !== "order" && field.key !== "page" ? url.searchParams.get(field.key) : null);
26
+ if (raw != null && raw.trim() !== "") {
27
+ values[field.key] = raw.trim();
28
+ }
29
+ }
30
+ return values;
31
+ }
32
+ function listRequestFilters(resource, url) {
33
+ const filters = [];
34
+ const search = url.searchParams.get('q') ?? undefined;
35
+ if (search) {
36
+ const searchFilters = resource.fields
37
+ .filter((field) => field.searchable)
38
+ .map((field) => ({
39
+ field: field.key,
40
+ operator: 'contains',
41
+ value: search,
42
+ }));
43
+ if (searchFilters.length === 1) {
44
+ filters.push(searchFilters[0]);
45
+ }
46
+ else if (searchFilters.length > 1) {
47
+ filters.push({ operator: 'or', value: searchFilters });
48
+ }
49
+ }
50
+ for (const field of resource.fields) {
51
+ const rawVal = url.searchParams.get(`filter_${field.key}`) ?? (field.key !== 'q' && field.key !== 'sort' && field.key !== 'order' && field.key !== 'page' ? url.searchParams.get(field.key) : null);
52
+ if (rawVal != null && rawVal.trim() !== '') {
53
+ filters.push({
54
+ field: field.key,
55
+ operator: 'eq',
56
+ value: field.type === 'number' && !Number.isNaN(Number(rawVal)) ? Number(rawVal) : rawVal,
57
+ });
58
+ }
59
+ }
60
+ return filters;
35
61
  }
36
62
  /**
37
63
  * Creates a SvelteKit `load` function that fetches a resource list
@@ -45,7 +71,7 @@ export function createListLoader(dp, resource) {
45
71
  resource: resource.name,
46
72
  pagination: { current: page, pageSize },
47
73
  sorters,
48
- filters: listSearchFilters(resource, search),
74
+ filters: listRequestFilters(resource, url),
49
75
  });
50
76
  return {
51
77
  records: listResponse.data,
@@ -60,6 +86,7 @@ export function createListLoader(dp, resource) {
60
86
  currentSort: sort,
61
87
  currentOrder: order,
62
88
  currentSearch: search,
89
+ currentFilters: listRequestFilterValues(resource, url),
63
90
  resource,
64
91
  };
65
92
  };
@@ -155,6 +182,32 @@ export function createCrudActions(dp, resource) {
155
182
  return { success: false, error: 'Delete failed' };
156
183
  }
157
184
  },
185
+ batchDelete: async ({ request }) => {
186
+ if (resource.canDelete === false) {
187
+ return { success: false, error: 'Delete is disabled for this resource' };
188
+ }
189
+ const formData = await request.formData();
190
+ const ids = formData.getAll('ids').map(v => String(v).trim()).filter(Boolean);
191
+ if (ids.length === 0)
192
+ return { success: false, error: 'No records selected' };
193
+ const redirectTo = toSafeLocalRedirect(formData.get('redirect'));
194
+ try {
195
+ if (dp.deleteMany) {
196
+ await dp.deleteMany({ resource: resource.name, ids });
197
+ }
198
+ else {
199
+ await Promise.all(ids.map(id => dp.deleteOne({ resource: resource.name, id })));
200
+ }
201
+ if (redirectTo)
202
+ throw redirect(303, redirectTo);
203
+ return { success: true };
204
+ }
205
+ catch (caughtError) {
206
+ if (isRedirect(caughtError))
207
+ throw caughtError;
208
+ return { success: false, error: 'Batch delete failed' };
209
+ }
210
+ },
158
211
  };
159
212
  }
160
213
  // ─── Auth Helpers ─────────────────────────────────────────────
@@ -301,28 +354,74 @@ export function createAuthActions(authProvider) {
301
354
  export function isLegacyBrowser(userAgent) {
302
355
  return /MSIE|Trident|rv:11/.test(userAgent);
303
356
  }
357
+ function normalizeRoutePrefix(prefix) {
358
+ const segments = prefix.split('/').filter(Boolean);
359
+ return segments.length > 0 ? `/${segments.join('/')}` : '/';
360
+ }
361
+ function pathWithinPrefix(pathname, prefix) {
362
+ return prefix === '/' || pathname === prefix || pathname.startsWith(`${prefix}/`);
363
+ }
364
+ function redirectOptions(options) {
365
+ const configured = typeof options === 'string' ? { litePrefix: options } : options;
366
+ return {
367
+ litePrefix: normalizeRoutePrefix(configured.litePrefix ?? '/lite'),
368
+ spaPrefix: normalizeRoutePrefix(configured.spaPrefix ?? '/'),
369
+ exclude: (configured.exclude ?? []).map(normalizeRoutePrefix),
370
+ mapPath: configured.mapPath,
371
+ status: configured.status ?? 302,
372
+ };
373
+ }
374
+ /**
375
+ * Computes a legacy redirect without importing or modifying the modern SPA.
376
+ * This pure helper can be used from non-SvelteKit server middleware.
377
+ */
378
+ export function getLegacyRedirectLocation(request, url, options = '/lite') {
379
+ const configured = redirectOptions(options);
380
+ const userAgent = request.headers.get('user-agent') ?? '';
381
+ const acceptsHtml = request.headers.get('accept')?.includes('text/html') === true;
382
+ const isDocumentRequest = (request.method === 'GET' || request.method === 'HEAD') && acceptsHtml;
383
+ if (!isLegacyBrowser(userAgent) || !isDocumentRequest)
384
+ return undefined;
385
+ if (pathWithinPrefix(url.pathname, configured.litePrefix))
386
+ return undefined;
387
+ if (configured.exclude.some((prefix) => pathWithinPrefix(url.pathname, prefix)))
388
+ return undefined;
389
+ if (!configured.mapPath
390
+ && configured.spaPrefix !== '/'
391
+ && !pathWithinPrefix(url.pathname, configured.spaPrefix))
392
+ return undefined;
393
+ let mappedPath;
394
+ if (configured.mapPath) {
395
+ mappedPath = configured.mapPath(url.pathname);
396
+ }
397
+ else if (configured.spaPrefix !== '/' && pathWithinPrefix(url.pathname, configured.spaPrefix)) {
398
+ mappedPath = url.pathname.slice(configured.spaPrefix.length) || '/';
399
+ }
400
+ else {
401
+ mappedPath = url.pathname;
402
+ }
403
+ const normalizedMappedPath = mappedPath.startsWith('/') ? mappedPath : `/${mappedPath}`;
404
+ const location = configured.litePrefix === '/'
405
+ ? normalizedMappedPath
406
+ : `${configured.litePrefix}${normalizedMappedPath === '/' ? '' : normalizedMappedPath}`;
407
+ return `${location}${url.search}`;
408
+ }
304
409
  /**
305
410
  * Creates an opt-in SvelteKit hook that redirects detected IE11 user agents.
306
411
  * Consumers remain responsible for their own transpilation and browser support.
307
412
  */
308
- export function createLegacyRedirectHook(litePrefix = '/lite') {
309
- const prefixSegments = litePrefix.split('/').filter(Boolean);
310
- const normalizedPrefix = prefixSegments.length > 0 ? `/${prefixSegments.join('/')}` : '/';
413
+ export function createLegacyRedirectHook(options = '/lite') {
414
+ const configured = redirectOptions(options);
311
415
  return async ({ event, resolve }) => {
312
- const ua = event.request.headers.get('user-agent') ?? '';
313
- const acceptsHtml = event.request.headers.get('accept')?.includes('text/html') === true;
314
- const isDocumentRequest = (event.request.method === 'GET' || event.request.method === 'HEAD')
315
- && acceptsHtml;
316
- const isWithinLite = normalizedPrefix === '/'
317
- || event.url.pathname === normalizedPrefix
318
- || event.url.pathname.startsWith(`${normalizedPrefix}/`);
319
- if (isLegacyBrowser(ua) && isDocumentRequest && !isWithinLite) {
320
- const targetPath = normalizedPrefix === '/'
321
- ? event.url.pathname
322
- : `${normalizedPrefix}${event.url.pathname}`;
416
+ const location = getLegacyRedirectLocation(event.request, event.url, configured);
417
+ if (location) {
323
418
  return new Response(null, {
324
- status: 302,
325
- headers: { Location: `${targetPath}${event.url.search}` },
419
+ status: configured.status,
420
+ headers: {
421
+ Location: location,
422
+ 'Cache-Control': 'private, no-store',
423
+ Vary: 'User-Agent',
424
+ },
326
425
  });
327
426
  }
328
427
  return resolve(event);
@@ -374,7 +473,7 @@ function formatValidationErrors(issues) {
374
473
  return errors;
375
474
  }
376
475
  function validateFormVariables(resource, mode, values) {
377
- const result = resourceToZodSchema(resource, mode).safeParse(values);
476
+ const result = resourceToTypeBoxSchema(resource, mode).safeParse(values);
378
477
  if (result.success)
379
478
  return { success: true, data: result.data };
380
479
  return {
@@ -471,7 +570,7 @@ function formDataToObject(formData, fields) {
471
570
  obj[field.key] = strRaw ? JSON.parse(strRaw) : null;
472
571
  }
473
572
  catch {
474
- obj[field.key] = strRaw; // Let Zod handle validation errors
573
+ obj[field.key] = strRaw; // Let TypeBox handle validation errors
475
574
  }
476
575
  break;
477
576
  default:
@@ -1,2 +1,3 @@
1
1
  export declare function parseExplicitBoolean(rawBoolean: unknown): boolean | undefined;
2
2
  export declare function isExplicitBooleanTrue(rawBoolean: unknown): boolean;
3
+ export declare function getStatusBadgeClass(value: unknown): string;
@@ -19,3 +19,21 @@ export function parseExplicitBoolean(rawBoolean) {
19
19
  export function isExplicitBooleanTrue(rawBoolean) {
20
20
  return parseExplicitBoolean(rawBoolean) === true;
21
21
  }
22
+ export function getStatusBadgeClass(value) {
23
+ if (typeof value !== "string")
24
+ return "lite-badge";
25
+ const v = value.toLowerCase().trim();
26
+ if (["active", "completed", "published", "delivered", "paid", "approved", "success", "in_stock", "healthy"].includes(v)) {
27
+ return "lite-badge lite-badge-success";
28
+ }
29
+ if (["draft", "pending", "processing", "in_transit", "review", "waiting", "warning", "low_stock"].includes(v)) {
30
+ return "lite-badge lite-badge-warning";
31
+ }
32
+ if (["cancelled", "rejected", "failed", "suspended", "inactive", "banned", "error", "out_of_stock", "closed"].includes(v)) {
33
+ return "lite-badge lite-badge-danger";
34
+ }
35
+ if (["shipped", "submitted", "open", "new", "admin", "verified"].includes(v)) {
36
+ return "lite-badge lite-badge-info";
37
+ }
38
+ return "lite-badge";
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/lite",
3
- "version": "0.3.27",
3
+ "version": "0.6.0",
4
4
  "description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -48,28 +48,28 @@
48
48
  }
49
49
  },
50
50
  "peerDependencies": {
51
- "svelte": "^5.56.8",
52
- "@svadmin/core": ">=0.34.2 <0.43.0",
53
- "@sveltejs/kit": "^2.70.2"
51
+ "svelte": "^5.56.10",
52
+ "@svadmin/core": ">=0.34.2 <0.46.0",
53
+ "@sveltejs/kit": "^2.70.3"
54
54
  },
55
55
  "dependencies": {
56
- "@lucide/svelte": "^1.31.0",
57
- "zod": "^4.4.3"
56
+ "@lucide/svelte": "^1.35.0",
57
+ "@sinclair/typebox": "^0.34.52"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.4.0",
61
- "@happy-dom/global-registrator": "^20.11.2",
61
+ "@happy-dom/global-registrator": "^20.11.12",
62
62
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
63
63
  "@testing-library/dom": "^10.4.1",
64
64
  "@testing-library/svelte": "^5.4.2",
65
- "happy-dom": "^20.11.2",
66
- "vitest": "^4.1.10"
65
+ "happy-dom": "^20.11.12",
66
+ "vitest": "^4.1.11"
67
67
  },
68
68
  "scripts": {
69
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
70
  "test": "bun run test:components && bun run test:server && bun run test:security",
71
71
  "test:components": "vitest run --config ./vitest.config.ts",
72
- "test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts ./src/fragment-id.test.ts ./src/ie11-ssr-contract.test.ts",
72
+ "test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts ./src/fragment-id.test.ts ./src/compatibility.test.ts ./src/ie11-ssr-contract.test.ts",
73
73
  "test:security": "vitest run --config ./src/security.test.config.ts"
74
74
  },
75
75
  "license": "MIT",