@svadmin/lite 0.5.0 → 0.7.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 (66) hide show
  1. package/README.md +190 -9
  2. package/dist/compatibility.d.ts +23 -0
  3. package/dist/compatibility.js +95 -0
  4. package/dist/components/LiteDynamicFormList.svelte +169 -0
  5. package/dist/components/LiteDynamicFormList.svelte.d.ts +45 -0
  6. package/dist/components/LiteFilterBuilder.svelte +205 -0
  7. package/dist/components/LiteFilterBuilder.svelte.d.ts +19 -0
  8. package/dist/components/LiteShowField.svelte +52 -22
  9. package/dist/components/LiteShowField.svelte.d.ts +3 -2
  10. package/dist/components/LiteTable.svelte +89 -36
  11. package/dist/components/LiteTable.svelte.d.ts +3 -2
  12. package/dist/components/LiteTransfer.svelte +185 -0
  13. package/dist/components/LiteTransfer.svelte.d.ts +18 -0
  14. package/dist/components/compatibility/LiteCapabilityBoundary.svelte +51 -0
  15. package/dist/components/compatibility/LiteCapabilityBoundary.svelte.d.ts +13 -0
  16. package/dist/components/compatibility/LiteClipboardFallback.svelte +16 -0
  17. package/dist/components/compatibility/LiteClipboardFallback.svelte.d.ts +8 -0
  18. package/dist/components/compatibility/LiteComputeFallback.svelte +50 -0
  19. package/dist/components/compatibility/LiteComputeFallback.svelte.d.ts +14 -0
  20. package/dist/components/compatibility/LiteDirectoryUpload.svelte +44 -0
  21. package/dist/components/compatibility/LiteDirectoryUpload.svelte.d.ts +11 -0
  22. package/dist/components/compatibility/LiteOrderedList.svelte +47 -0
  23. package/dist/components/compatibility/LiteOrderedList.svelte.d.ts +14 -0
  24. package/dist/components/compatibility/LiteRealtimeStatus.svelte +43 -0
  25. package/dist/components/compatibility/LiteRealtimeStatus.svelte.d.ts +11 -0
  26. package/dist/components/compatibility/LiteVisualFallback.svelte +80 -0
  27. package/dist/components/compatibility/LiteVisualFallback.svelte.d.ts +18 -0
  28. package/dist/components/compatibility/index.d.ts +7 -0
  29. package/dist/components/compatibility/index.js +7 -0
  30. package/dist/components/fields/LiteAvatarField.svelte +127 -0
  31. package/dist/components/fields/LiteAvatarField.svelte.d.ts +21 -0
  32. package/dist/components/fields/LiteCascader.svelte +119 -0
  33. package/dist/components/fields/LiteCascader.svelte.d.ts +24 -0
  34. package/dist/components/fields/LiteCodeField.svelte +71 -0
  35. package/dist/components/fields/LiteCodeField.svelte.d.ts +16 -0
  36. package/dist/components/fields/LiteCopyField.svelte +74 -0
  37. package/dist/components/fields/LiteCopyField.svelte.d.ts +18 -0
  38. package/dist/components/fields/LiteCurrencyField.svelte +126 -0
  39. package/dist/components/fields/LiteCurrencyField.svelte.d.ts +21 -0
  40. package/dist/components/fields/LiteDateRangeField.svelte +147 -0
  41. package/dist/components/fields/LiteDateRangeField.svelte.d.ts +26 -0
  42. package/dist/components/fields/LitePercentField.svelte +124 -0
  43. package/dist/components/fields/LitePercentField.svelte.d.ts +17 -0
  44. package/dist/components/fields/LitePhoneField.svelte +81 -0
  45. package/dist/components/fields/LitePhoneField.svelte.d.ts +17 -0
  46. package/dist/components/fields/LiteRatingField.svelte +105 -0
  47. package/dist/components/fields/LiteRatingField.svelte.d.ts +16 -0
  48. package/dist/components/fields/LiteTreeSelect.svelte +143 -0
  49. package/dist/components/fields/LiteTreeSelect.svelte.d.ts +23 -0
  50. package/dist/components/fields/index.d.ts +12 -0
  51. package/dist/components/fields/index.js +10 -0
  52. package/dist/components/pages/LiteCreatePage.svelte +15 -6
  53. package/dist/components/pages/LiteCreatePage.svelte.d.ts +1 -1
  54. package/dist/components/pages/LiteEditPage.svelte +18 -9
  55. package/dist/components/pages/LiteEditPage.svelte.d.ts +1 -1
  56. package/dist/components/pages/LiteListPage.svelte +99 -27
  57. package/dist/components/pages/LiteListPage.svelte.d.ts +4 -2
  58. package/dist/components/pages/LiteShowPage.svelte +19 -10
  59. package/dist/components/pages/LiteShowPage.svelte.d.ts +1 -1
  60. package/dist/index.d.ts +10 -2
  61. package/dist/index.js +8 -1
  62. package/dist/lite.css +444 -0
  63. package/dist/schema-generator.js +15 -4
  64. package/dist/server-adapter.d.ts +18 -1
  65. package/dist/server-adapter.js +73 -16
  66. package/package.json +5 -4
package/README.md CHANGED
@@ -10,6 +10,12 @@ The bundled CSS follows an IE11-oriented baseline, but Svelte 5, SvelteKit, Tail
10
10
 
11
11
  The main `@svadmin/ui` package delivers a premium SPA experience using Svelte 5, Tailwind CSS v4, and TanStack. Those tools target modern browsers. Some enterprise/government environments instead prefer server-driven navigation, native forms, and a smaller client-side runtime.
12
12
 
13
+ **What Lite is:**
14
+ - A **Zero-JS SSR-first admin component suite** designed with an IE11-safe CSS baseline.
15
+ - 100% interoperable with `@svadmin/core` — sharing the same `DataProvider`, `AuthProvider`, `Resource`, and `FieldDefinition` contracts.
16
+ - An architectural companion to `@svadmin/ui` achieving **100% component parity (72/72 components)** through 1:1 server-rendered matches or deliberate semantic fallbacks.
17
+ - **Not** a generic Tailwind-to-IE11 compiler or a full client-side port of shadcn-svelte (which depends on modern browser JS runtimes and CSS custom properties).
18
+
13
19
  `@svadmin/lite` provides a server-rendered fallback that **shares the same DataProvider, AuthProvider, and Resource definitions** — only the rendering layer is different.
14
20
 
15
21
  ## Features
@@ -25,8 +31,11 @@ The main `@svadmin/ui` package delivers a premium SPA experience using Svelte 5,
25
31
  | **Authentication pages** | Login/logout plus optional provider-delegating account actions |
26
32
  | **Auth Guard** | Server hook redirects unauthenticated users |
27
33
  | **UA Detection** | Optional legacy-browser detection hook redirects users to `/lite/` routes |
34
+ | **SPA + IE11 split** | Modern browsers keep the existing SPA; IE11 is redirected before the SPA route is rendered |
35
+ | **Capability fallbacks** | Canvas, WASM, realtime, directory upload, observers, storage, media, and other browser-only features have server-safe alternatives |
28
36
  | **i18n** | Uses `@svadmin/core` `t()` translations |
29
37
  | **Multi-level Menu** | Always-expanded, config-driven nested links via `MenuItem[]` |
38
+ | **Parity Tracking** | Built-in CLI matrix, CI parity verification gate, and live interactive visual showroom |
30
39
  | **Print** | `@media print` optimized styles |
31
40
 
32
41
  ## Quick Start
@@ -42,6 +51,20 @@ are required peers rather than optional dependencies.
42
51
 
43
52
  ### 2. Create a list page
44
53
 
54
+ For an existing SvelteKit project, the route tree can be generated once instead
55
+ of being handwritten for every resource:
56
+
57
+ ```bash
58
+ bunx @svadmin/create lite init .
59
+ bunx @svadmin/create lite init . --write
60
+ ```
61
+
62
+ The generator adds a shared `$lib/svadmin-lite.ts` adapter and dynamic
63
+ `[resource]` list/create/show/edit routes. `$lib/admin` only needs to export
64
+ `resources` and `dataProvider`; the existing SPA files are not changed. The
65
+ generated Lite subtree is `ssr = true` and `csr = false`, and existing files are
66
+ preserved on repeat runs.
67
+
45
68
  ```typescript
46
69
  // src/routes/lite/posts/+page.server.ts
47
70
  import { createListLoader, createCrudActions } from '@svadmin/lite';
@@ -195,18 +218,106 @@ export const handle = createLegacyRedirectHook('/lite');
195
218
  </head>
196
219
  ```
197
220
 
198
- ## Components
221
+ ### 5. Keep the SPA untouched while routing IE11 to Lite
222
+
223
+ The redirect belongs to the server hook or reverse proxy. It does not add a
224
+ compatibility branch, polyfill, or user-agent check to the SPA bundle:
225
+
226
+ ```typescript
227
+ // src/hooks.server.ts
228
+ import { createLegacyRedirectHook } from '@svadmin/lite';
229
+
230
+ export const handle = createLegacyRedirectHook({
231
+ litePrefix: '/lite',
232
+ spaPrefix: '/admin',
233
+ // Static assets, health checks, and API routes stay outside document routing.
234
+ exclude: ['/api', '/_app', '/health'],
235
+ });
236
+ ```
237
+
238
+ For a request such as `/admin/orders/show/7`, an IE11 user receives a 302 to
239
+ `/lite/orders/show/7`. Modern browsers continue to the existing SPA route.
240
+ The Lite subtree must remain `ssr = true` and `csr = false`; the browser never
241
+ executes Svelte 5 there.
242
+
243
+ If the application is behind a reverse proxy, the same rule can be implemented
244
+ there instead. The important property is that the SPA JavaScript is not sent to
245
+ IE11 before the decision is made.
246
+
247
+ ## Components & Parity (100% Coverage)
248
+
249
+ `@svadmin/lite` provides **100% component parity** (72/72 components) corresponding to `@svadmin/ui`. See [PARITY.md](./PARITY.md) for the complete matrix.
250
+
251
+ ### Core Pages & Layout
199
252
 
200
253
  | Component | Description |
201
254
  |-----------|-------------|
202
- | `LiteLayout` | Sidebar + main area layout (supports `menu` prop for multi-level menus) |
203
- | `LiteTable` | HTML table with sort links and delete confirmation |
204
- | `LiteSearch` | GET-based search form |
205
- | `LitePagination` | Page number links |
206
- | `LiteForm` | Auto-generated form from `FieldDefinition[]` |
207
- | `LiteShow` | Record detail/view page |
208
- | `LiteLogin` | Login form |
209
- | `LiteAlert` | Success/error notification banner |
255
+ | `LiteLayout` | Responsive sidebar + header + main shell (supports nested `menu` links) |
256
+ | `LiteSidebar` / `LiteHeader` | Discrete layout primitives |
257
+ | `LiteListPage` | Full-featured list view with search, table, bulk actions, and pagination |
258
+ | `LiteCreatePage` / `LiteEditPage` | Auto-generated CRUD form pages |
259
+ | `LiteShowPage` | Key-value record view with typed field renderers |
260
+ | `LiteProfilePage` | User profile management page with avatar and info |
261
+ | `LiteRegisterPage` | User registration page with server-side validation |
262
+ | `LiteForgotPasswordPage` / `LiteUpdatePasswordPage` | Password recovery and credential rotation flows |
263
+ | `LiteTable` | HTML table with sort links, selectable rows, and delete confirmation |
264
+ | `LiteForm` | Schema-driven form renderer supporting all field definitions |
265
+ | `LiteShow` | Detailed field-by-field record viewer |
266
+ | `LitePagination` / `LiteSearch` | URL-driven pagination and GET search controls |
267
+ | `LiteAlert` | Inline notification banner for success/error/warning states |
268
+ | `LiteBreadcrumbs` / `LiteTabs` / `LiteEmptyState` | Semantic navigation and placeholder primitives |
269
+
270
+ ### Field Components (23 Fields)
271
+
272
+ | Category | Components |
273
+ |----------|------------|
274
+ | **Text & Numeric** | `LiteTextField`, `LiteNumberField`, `LiteCurrencyField`, `LitePercentField`, `LitePhoneField`, `LiteEmailField`, `LiteUrlField`, `LiteRatingField`, `LiteCopyField` |
275
+ | **Date & Choice** | `LiteDateField`, `LiteDateRangeField`, `LiteBooleanField`, `LiteSelectField`, `LiteMultiSelectField`, `LiteTreeSelect`, `LiteCascader`, `LiteTagField`, `LiteRelationField` |
276
+ | **Media & Rich & Array** | `LiteAvatarField`, `LiteImageField`, `LiteFileField`, `LiteCodeField`, `LiteMarkdownField`, `LiteRichTextField`, `LiteJsonField`, `LiteArrayField`, `LiteDynamicFormList`, `LiteTransfer` |
277
+
278
+ ### Action Buttons (10 Buttons)
279
+
280
+ | Component | Description |
281
+ |-----------|-------------|
282
+ | `LiteListButton`, `LiteCreateButton`, `LiteEditButton`, `LiteShowButton`, `LiteCloneButton` | Server-rendered `<a>` navigation buttons |
283
+ | `LiteDeleteButton` | Fragment-target modal confirmation with native POST form action |
284
+ | `LiteSaveButton`, `LiteRefreshButton`, `LiteExportButton`, `LiteImportButton` | Form submission, page refresh, and file action buttons |
285
+
286
+ ### Widgets & Charts (SSR-Safe)
287
+
288
+ | Component | Description |
289
+ |-----------|-------------|
290
+ | `LiteStatsCard`, `LiteInsightCard`, `LiteAnomalyBadge` | Metric KPI cards and status badges |
291
+ | `LiteBarChart`, `LiteLineChart`, `LitePieChart` | Server-rendered SVG/HTML charts with fallback data tables |
292
+
293
+ ### Compatibility & Degradation Primitives
294
+
295
+ | Component | Description |
296
+ |-----------|-------------|
297
+ | `LiteCapabilityBoundary` | Documents and selects a server-safe fallback for an optional browser capability |
298
+ | `LiteVisualFallback` | Snapshot plus accessible table for Canvas, WebGL, map, chart, and flow UIs |
299
+ | `LiteDirectoryUpload` | Directory enhancement with multiple-file and ZIP upload fallbacks |
300
+ | `LiteRealtimeStatus` | Snapshot timestamp and native refresh/meta-refresh fallback for live data |
301
+ | `LiteComputeFallback` | Native POST action for WASM, Worker, and long-running compute fallback |
302
+ | `LiteOrderedList` | Up/down POST actions for drag-and-drop ordering fallback |
303
+ | `LiteClipboardFallback` | Selectable textarea when Clipboard API is unavailable |
304
+ | `LiteAutoSaveIndicator` | Server persistence timestamp indicator |
305
+ | `LiteInlineEdit` | Non-JS inline edit modal fallback |
306
+ | `LiteModalForm` / `LiteDrawerForm` | Fragment-target `#modal` forms for zero-JS popups |
307
+ | `LiteVirtualTable` | Graceful fallback to server-paginated table |
308
+ | `LiteDraggableHeader` | Static column header with URL sorter links |
309
+ | `LiteToast` / `LiteUndoableNotification` | Server-driven flash messages and undo actions |
310
+ | `LiteFilterBuilder` | Native multi-rule CRUD filter builder with AND/OR logical operators |
311
+
312
+ ## Parity Tracking & Visualization
313
+
314
+ To monitor and maintain 100% component parity between `@svadmin/ui` and `@svadmin/lite`:
315
+
316
+ - **CLI Dashboard**: Run `bun run check:parity` to inspect the live coverage matrix in terminal.
317
+ - **Parity Matrix Document**: [PARITY.md](./PARITY.md) is auto-generated with exhaustive module-by-module tables.
318
+ - **Machine-Readable JSON**: `packages/lite/parity.json` provides structured data for automation.
319
+ - **Interactive Showroom**: Visit `/lite/parity` in the example app to interactively preview all 72 components and live metrics.
320
+ - **CI Contract Test**: `scripts/parity-contract.test.ts` runs in CI to prevent regression and ensure every new UI component has a documented Lite counterpart.
210
321
 
211
322
  ## Server Utilities
212
323
 
@@ -243,6 +354,68 @@ Copy the exported `@svadmin/lite/enhance.js` asset to your application's static
243
354
 
244
355
  The asset is optional for core server-rendered navigation and form submission; the conveniences listed above require it.
245
356
 
357
+ ## Capability compatibility
358
+
359
+ The compatibility catalog is exported as `LITE_COMPATIBILITY_CATALOG`, with
360
+ `detectLiteCapabilities()` and `resolveLiteCompatibility()` for explicit client
361
+ enhancement entries. The detector accepts an injected environment, so SSR tests
362
+ do not need browser globals:
363
+
364
+ ```typescript
365
+ import {
366
+ detectLiteCapabilities,
367
+ resolveLiteCompatibility,
368
+ } from '@svadmin/lite';
369
+
370
+ const support = detectLiteCapabilities(globalThis);
371
+ const realtime = resolveLiteCompatibility('websocket', support);
372
+ ```
373
+
374
+ Do not import this detector into the modern SPA just to make IE11 work. The SPA
375
+ stays modern. Use it only in an optional Lite enhancement entry or a modern-only
376
+ Lite page. The normal Lite route requires no detector and no hydration.
377
+
378
+ Recommended fallback contract:
379
+
380
+ | Modern capability | Required Lite fallback |
381
+ |---|---|
382
+ | Canvas/WebGL/flow/map/chart | Static image or SVG plus a structured data table and download |
383
+ | WASM/Worker | Server action or background job with status and downloadable result |
384
+ | WebSocket/SSE | Snapshot timestamp, refresh link, optional polling or meta refresh |
385
+ | Directory/File System Access | Multiple files, relative paths where available, or ZIP upload |
386
+ | Virtual scrolling/IntersectionObserver | Server pagination or eager server rendering |
387
+ | Drag and drop | Ordered list with up/down POST actions |
388
+ | Clipboard API | Selectable text area and normal browser copy command |
389
+ | IndexedDB/localStorage | Server persistence for authoritative data; local cache only for preferences |
390
+ | Notifications/Media Capture/WebRTC | In-page status, file upload, or server-managed workflow |
391
+
392
+ These fallbacks preserve the business operation, submitted data, read access,
393
+ download, and auditability. They do not attempt to reproduce every modern
394
+ interaction pixel-for-pixel.
395
+
396
+ ### Third-party library boundaries
397
+
398
+ Do not patch these libraries into the IE11 document. Keep them in the modern SPA
399
+ and render the matching Lite fallback from shared records or server projections:
400
+
401
+ | Modern library family | Examples | Lite boundary |
402
+ |---|---|---|
403
+ | Flow/canvas/3D | `@xyflow/svelte`, Three.js, Fabric.js | `LiteVisualFallback` with nodes, edges, properties, snapshot, and export |
404
+ | Charts | ECharts, Chart.js, Vega | Lite chart components or `LiteVisualFallback` with a data table |
405
+ | Maps | MapLibre, Leaflet | Address/coordinate table, static map image, and external navigation link |
406
+ | Editors | Monaco, CodeMirror, TipTap | `textarea`, Markdown, source download, and server validation |
407
+ | Realtime clients | native WebSocket/EventSource, Socket.IO | `LiteRealtimeStatus`, refresh, polling endpoint, or server status page |
408
+ | File-system helpers | `browser-fs-access` | `LiteDirectoryUpload` and normal download links |
409
+ | Browser storage | `idb`, `idb-keyval`, localForage | Server persistence; browser cache only for drafts and preferences |
410
+ | Virtual table/drag libraries | TanStack Virtual, SortableJS | Server pagination and `LiteOrderedList` actions |
411
+ | PDF/media terminals | PDF.js, MediaRecorder, WebRTC, xterm.js | Download/upload, transcript/log view, and server workflow |
412
+
413
+ Dependency patch systems such as Bun `patchedDependencies` or `patch-package`
414
+ should only repair a reproducible package defect, such as an eager `window`
415
+ reference or incorrect package export. They must not be used to pretend that an
416
+ unavailable browser capability exists. Every retained patch needs an exact
417
+ package version, a regression test, and a documented removal condition.
418
+
246
419
  ## Compatibility notes
247
420
 
248
421
  - Registration, recovery, password, and profile actions delegate the submitted form
@@ -261,6 +434,14 @@ The asset is optional for core server-rendered navigation and form submission; t
261
434
  native links/forms and the IE11-safe Lite CSS contract. Keep the entire Lite route
262
435
  subtree on `ssr = true` and `csr = false`; Svelte 5 hydration/runtime execution in
263
436
  IE11 is not supported or promised by this package.
437
+ - The SPA is not an IE11 target. The supported architecture is `modern SPA +
438
+ server-routed Lite`, where server middleware decides which document bundle is
439
+ returned before the browser executes application JavaScript.
440
+ - Third-party polyfills are optional host concerns. `@vitejs/plugin-legacy`,
441
+ `core-js`, Fetch/EventSource polyfills, and `browser-fs-access` can improve a
442
+ modern-only enhancement entry, but they are not Lite core dependencies and do
443
+ not replace a server fallback for Canvas, WebSocket protocol support, WASM, or
444
+ File System Access.
264
445
 
265
446
  ## License
266
447
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Browser capability metadata for optional Lite enhancements.
3
+ *
4
+ * The SSR baseline never reads browser globals. Consumers may call
5
+ * `detectLiteCapabilities(globalThis)` from an explicitly client-only entry.
6
+ */
7
+ export type LiteCapability = 'canvas-2d' | 'webgl' | 'wasm' | 'wasm-streaming' | 'websocket' | 'event-source' | 'worker' | 'directory-upload' | 'file-system-access' | 'clipboard' | 'broadcast-channel' | 'intersection-observer' | 'resize-observer' | 'service-worker' | 'indexed-db' | 'notifications' | 'media-capture' | 'web-rtc' | 'geolocation' | 'web-streams';
8
+ export type LiteFallbackKind = 'structured-data' | 'static-snapshot' | 'server-action' | 'server-refresh' | 'server-pagination' | 'standard-upload' | 'manual-copy' | 'server-storage' | 'in-page-status' | 'download';
9
+ export interface LiteCompatibilityDescriptor {
10
+ capability: LiteCapability;
11
+ fallbackKind: LiteFallbackKind;
12
+ fallback: string;
13
+ enhancement: string;
14
+ }
15
+ export type LiteCapabilitySupport = Record<LiteCapability, boolean>;
16
+ export interface LiteCompatibilityResolution extends LiteCompatibilityDescriptor {
17
+ supported: boolean;
18
+ mode: 'enhanced' | 'fallback';
19
+ }
20
+ export declare const LITE_COMPATIBILITY_CATALOG: readonly LiteCompatibilityDescriptor[];
21
+ /** Detect optional browser APIs without accessing globals during SSR. */
22
+ export declare function detectLiteCapabilities(environment?: unknown): LiteCapabilitySupport;
23
+ export declare function resolveLiteCompatibility(capability: LiteCapability, support?: LiteCapabilitySupport): LiteCompatibilityResolution;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Browser capability metadata for optional Lite enhancements.
3
+ *
4
+ * The SSR baseline never reads browser globals. Consumers may call
5
+ * `detectLiteCapabilities(globalThis)` from an explicitly client-only entry.
6
+ */
7
+ function asRecord(value) {
8
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
9
+ ? value
10
+ : undefined;
11
+ }
12
+ function hasFunction(record, key) {
13
+ return typeof record?.[key] === 'function';
14
+ }
15
+ function supportsCanvas(documentValue, context) {
16
+ const documentRecord = asRecord(documentValue);
17
+ if (!documentRecord || typeof documentRecord.createElement !== 'function')
18
+ return false;
19
+ const canvas = documentRecord.createElement('canvas');
20
+ const canvasRecord = asRecord(canvas);
21
+ if (!canvasRecord || typeof canvasRecord.getContext !== 'function')
22
+ return false;
23
+ return Boolean(canvasRecord.getContext(context));
24
+ }
25
+ function supportsDirectoryUpload(environment) {
26
+ const inputConstructor = asRecord(environment.HTMLInputElement);
27
+ const prototype = asRecord(inputConstructor?.prototype);
28
+ return prototype !== undefined && ('webkitdirectory' in prototype || 'directory' in prototype);
29
+ }
30
+ export const LITE_COMPATIBILITY_CATALOG = [
31
+ { capability: 'canvas-2d', fallbackKind: 'structured-data', fallback: 'Render a data table or static image.', enhancement: 'Load the Canvas UI in a modern client.' },
32
+ { capability: 'webgl', fallbackKind: 'static-snapshot', fallback: 'Render a server-generated image and structured values.', enhancement: 'Load the WebGL view in a modern client.' },
33
+ { capability: 'wasm', fallbackKind: 'server-action', fallback: 'Submit the task to a server action or background job.', enhancement: 'Run the WebAssembly module in a modern client.' },
34
+ { capability: 'wasm-streaming', fallbackKind: 'server-action', fallback: 'Use a server computation or downloadable result.', enhancement: 'Instantiate WebAssembly from a streamed response.' },
35
+ { capability: 'websocket', fallbackKind: 'server-refresh', fallback: 'Show the last snapshot with a native refresh link.', enhancement: 'Subscribe through WebSocket.' },
36
+ { capability: 'event-source', fallbackKind: 'server-refresh', fallback: 'Use refresh, polling, or a server status page.', enhancement: 'Subscribe through server-sent events.' },
37
+ { capability: 'worker', fallbackKind: 'server-action', fallback: 'Run work synchronously or as a server job.', enhancement: 'Move client computation into a Worker.' },
38
+ { capability: 'directory-upload', fallbackKind: 'standard-upload', fallback: 'Upload multiple files or a ZIP archive.', enhancement: 'Select a directory and preserve relative paths.' },
39
+ { capability: 'file-system-access', fallbackKind: 'standard-upload', fallback: 'Use standard file inputs and download links.', enhancement: 'Use the File System Access API.' },
40
+ { capability: 'clipboard', fallbackKind: 'manual-copy', fallback: 'Expose selectable text for manual copy and paste.', enhancement: 'Use the Clipboard API.' },
41
+ { capability: 'broadcast-channel', fallbackKind: 'server-refresh', fallback: 'Reload server-owned session state.', enhancement: 'Synchronize non-authoritative UI state between tabs.' },
42
+ { capability: 'intersection-observer', fallbackKind: 'server-pagination', fallback: 'Render eagerly or use server pagination.', enhancement: 'Lazy-load or virtualize visible content.' },
43
+ { capability: 'resize-observer', fallbackKind: 'structured-data', fallback: 'Use stable responsive dimensions and normal document flow.', enhancement: 'React to element-size changes.' },
44
+ { capability: 'service-worker', fallbackKind: 'download', fallback: 'Offer static snapshots and downloadable exports.', enhancement: 'Cache explicitly selected offline assets.' },
45
+ { capability: 'indexed-db', fallbackKind: 'server-storage', fallback: 'Persist authoritative state on the server.', enhancement: 'Cache drafts and non-authoritative preferences locally.' },
46
+ { capability: 'notifications', fallbackKind: 'in-page-status', fallback: 'Render an in-page alert and audit log.', enhancement: 'Show a browser notification after permission is granted.' },
47
+ { capability: 'media-capture', fallbackKind: 'standard-upload', fallback: 'Upload an existing image, audio, or video file.', enhancement: 'Capture media from the browser.' },
48
+ { capability: 'web-rtc', fallbackKind: 'standard-upload', fallback: 'Upload a recording or use a server-managed workflow.', enhancement: 'Enable a real-time peer media session.' },
49
+ { capability: 'geolocation', fallbackKind: 'structured-data', fallback: 'Enter an address or coordinates manually.', enhancement: 'Read the current location with user permission.' },
50
+ { capability: 'web-streams', fallbackKind: 'download', fallback: 'Use a normal request, response, upload, or download.', enhancement: 'Process streamed data in the modern client.' },
51
+ ];
52
+ const descriptorByCapability = new Map(LITE_COMPATIBILITY_CATALOG.map((descriptor) => [descriptor.capability, descriptor]));
53
+ /** Detect optional browser APIs without accessing globals during SSR. */
54
+ export function detectLiteCapabilities(environment) {
55
+ const explicitEnvironment = arguments.length > 0;
56
+ const env = asRecord(explicitEnvironment
57
+ ? environment
58
+ : typeof globalThis === 'undefined' ? undefined : globalThis) ?? {};
59
+ const navigator = asRecord(env.navigator);
60
+ const clipboard = asRecord(navigator?.clipboard);
61
+ const webAssembly = asRecord(env.WebAssembly);
62
+ const readableStream = asRecord(env.ReadableStream);
63
+ return {
64
+ 'canvas-2d': supportsCanvas(env.document, '2d'),
65
+ webgl: supportsCanvas(env.document, 'webgl'),
66
+ wasm: hasFunction(webAssembly, 'instantiate'),
67
+ 'wasm-streaming': hasFunction(webAssembly, 'instantiateStreaming'),
68
+ websocket: typeof env.WebSocket === 'function',
69
+ 'event-source': typeof env.EventSource === 'function',
70
+ worker: typeof env.Worker === 'function',
71
+ 'directory-upload': supportsDirectoryUpload(env),
72
+ 'file-system-access': typeof env.showOpenFilePicker === 'function'
73
+ || typeof env.showDirectoryPicker === 'function',
74
+ clipboard: hasFunction(clipboard, 'writeText'),
75
+ 'broadcast-channel': typeof env.BroadcastChannel === 'function',
76
+ 'intersection-observer': typeof env.IntersectionObserver === 'function',
77
+ 'resize-observer': typeof env.ResizeObserver === 'function',
78
+ 'service-worker': asRecord(navigator?.serviceWorker) !== undefined,
79
+ 'indexed-db': asRecord(env.indexedDB) !== undefined,
80
+ notifications: typeof env.Notification === 'function',
81
+ 'media-capture': hasFunction(asRecord(navigator?.mediaDevices), 'getUserMedia')
82
+ || typeof env.MediaRecorder === 'function',
83
+ 'web-rtc': typeof env.RTCPeerConnection === 'function',
84
+ geolocation: asRecord(navigator?.geolocation) !== undefined,
85
+ 'web-streams': asRecord(env.ReadableStream) !== undefined
86
+ && (hasFunction(readableStream, 'from') || typeof env.WritableStream === 'function'),
87
+ };
88
+ }
89
+ export function resolveLiteCompatibility(capability, support = detectLiteCapabilities()) {
90
+ const descriptor = descriptorByCapability.get(capability);
91
+ if (!descriptor)
92
+ throw new Error(`Unknown Lite capability: ${capability}`);
93
+ const supported = support[capability];
94
+ return { ...descriptor, supported, mode: supported ? 'enhanced' : 'fallback' };
95
+ }
@@ -0,0 +1,169 @@
1
+ <script lang="ts" generics="T = Record<string, unknown>">
2
+ import type { Snippet } from 'svelte';
3
+ import { t } from '@svadmin/core/i18n';
4
+
5
+ interface Props {
6
+ items?: T[];
7
+ name?: string;
8
+ label?: string;
9
+ description?: string;
10
+ addButtonLabel?: string;
11
+ minItems?: number;
12
+ maxItems?: number;
13
+ defaultItem?: T | (() => T);
14
+ disabled?: boolean;
15
+ emptyText?: string;
16
+ children?: Snippet<[{
17
+ item: T;
18
+ index: number;
19
+ isFirst: boolean;
20
+ isLast: boolean;
21
+ }]>;
22
+ headerExtra?: Snippet<[{ count: number }]>;
23
+ }
24
+
25
+ let {
26
+ items = [],
27
+ name = 'items',
28
+ label,
29
+ description,
30
+ addButtonLabel = '+ Add Item',
31
+ minItems = 0,
32
+ maxItems = Infinity,
33
+ disabled = false,
34
+ emptyText = 'No items yet',
35
+ children,
36
+ headerExtra,
37
+ }: Props = $props();
38
+
39
+ const renderedItems = $derived(items.length > 0 ? items : (minItems > 0 ? Array(minItems).fill({}) as T[] : []));
40
+ </script>
41
+
42
+ <div class="lite-dynamic-form-list lite-form-group">
43
+ <div class="lite-dynamic-header">
44
+ <div>
45
+ {#if label}
46
+ <span class="lite-label">{label}</span>
47
+ {/if}
48
+ {#if description}
49
+ <p class="lite-help-text">{description}</p>
50
+ {/if}
51
+ </div>
52
+ <div class="lite-dynamic-actions">
53
+ {#if headerExtra}
54
+ {@render headerExtra({ count: renderedItems.length })}
55
+ {/if}
56
+ <span class="lite-badge">{renderedItems.length} {t('common.items') || 'items'}</span>
57
+ </div>
58
+ </div>
59
+
60
+ {#if renderedItems.length === 0}
61
+ <div class="lite-empty-box">
62
+ <p>{emptyText}</p>
63
+ </div>
64
+ {:else}
65
+ <div class="lite-dynamic-items">
66
+ {#each renderedItems as item, idx (idx)}
67
+ <div class="lite-dynamic-item-card">
68
+ <div class="lite-dynamic-item-header">
69
+ <span class="lite-badge lite-badge-indigo">#{idx + 1}</span>
70
+ <div class="lite-dynamic-item-tools">
71
+ {#if idx > 0}
72
+ <button type="submit" name="_action" value="move_up_{name}_{idx}" class="lite-btn lite-btn-sm" {disabled} title="Move Up">↑</button>
73
+ {/if}
74
+ {#if idx < renderedItems.length - 1}
75
+ <button type="submit" name="_action" value="move_down_{name}_{idx}" class="lite-btn lite-btn-sm" {disabled} title="Move Down">↓</button>
76
+ {/if}
77
+ {#if renderedItems.length > minItems}
78
+ <button type="submit" name="_action" value="remove_{name}_{idx}" class="lite-btn lite-btn-sm lite-btn-danger" {disabled} title="Remove">✕</button>
79
+ {/if}
80
+ </div>
81
+ </div>
82
+ <div class="lite-dynamic-item-body">
83
+ {#if children}
84
+ {@render children({
85
+ item,
86
+ index: idx,
87
+ isFirst: idx === 0,
88
+ isLast: idx === renderedItems.length - 1,
89
+ })}
90
+ {:else}
91
+ <div class="lite-form-row">
92
+ {#each Object.entries((item as Record<string, unknown>) ?? {}) as [k, v] (k)}
93
+ <div class="lite-form-group" style="flex: 1; min-width: 140px;">
94
+ <label class="lite-label" for="{name}_{idx}_{k}">{k}</label>
95
+ <input
96
+ type="text"
97
+ id="{name}_{idx}_{k}"
98
+ name="{name}[{idx}][{k}]"
99
+ value={String(v ?? '')}
100
+ class="lite-input"
101
+ {disabled}
102
+ />
103
+ </div>
104
+ {/each}
105
+ </div>
106
+ {/if}
107
+ </div>
108
+ </div>
109
+ {/each}
110
+ </div>
111
+ {/if}
112
+
113
+ {#if renderedItems.length < maxItems && !disabled}
114
+ <div style="margin-top: 10px;">
115
+ <button type="submit" name="_action" value="add_{name}" class="lite-btn">
116
+ {addButtonLabel}
117
+ </button>
118
+ </div>
119
+ {/if}
120
+ </div>
121
+
122
+ <style>
123
+ .lite-dynamic-form-list {
124
+ border: 1px solid #e2e8f0;
125
+ border-radius: 6px;
126
+ padding: 14px;
127
+ background-color: #fafafa;
128
+ margin-bottom: 16px;
129
+ }
130
+ .lite-dynamic-header {
131
+ display: flex;
132
+ justify-content: space-between;
133
+ align-items: center;
134
+ margin-bottom: 12px;
135
+ }
136
+ .lite-dynamic-items {
137
+ display: flex;
138
+ flex-direction: column;
139
+ }
140
+ .lite-dynamic-item-card {
141
+ background: #ffffff;
142
+ border: 1px solid #e2e8f0;
143
+ border-radius: 4px;
144
+ padding: 12px;
145
+ margin-bottom: 10px;
146
+ }
147
+ .lite-dynamic-item-header {
148
+ display: flex;
149
+ justify-content: space-between;
150
+ align-items: center;
151
+ margin-bottom: 8px;
152
+ border-bottom: 1px dashed #e2e8f0;
153
+ padding-bottom: 6px;
154
+ }
155
+ .lite-dynamic-item-tools {
156
+ display: flex;
157
+ }
158
+ .lite-dynamic-item-tools button {
159
+ margin-left: 4px;
160
+ }
161
+ .lite-empty-box {
162
+ text-align: center;
163
+ padding: 24px 12px;
164
+ color: #64748b;
165
+ background: #ffffff;
166
+ border: 1px dashed #cbd5e1;
167
+ border-radius: 4px;
168
+ }
169
+ </style>
@@ -0,0 +1,45 @@
1
+ import type { Snippet } from 'svelte';
2
+ declare function $$render<T = Record<string, unknown>>(): {
3
+ props: {
4
+ items?: T[];
5
+ name?: string;
6
+ label?: string;
7
+ description?: string;
8
+ addButtonLabel?: string;
9
+ minItems?: number;
10
+ maxItems?: number;
11
+ defaultItem?: T | (() => T);
12
+ disabled?: boolean;
13
+ emptyText?: string;
14
+ children?: Snippet<[{
15
+ item: T;
16
+ index: number;
17
+ isFirst: boolean;
18
+ isLast: boolean;
19
+ }]>;
20
+ headerExtra?: Snippet<[{
21
+ count: number;
22
+ }]>;
23
+ };
24
+ exports: {};
25
+ bindings: "";
26
+ slots: {};
27
+ events: {};
28
+ };
29
+ declare class __sveltets_Render<T = Record<string, unknown>> {
30
+ props(): ReturnType<typeof $$render<T>>['props'];
31
+ events(): ReturnType<typeof $$render<T>>['events'];
32
+ slots(): ReturnType<typeof $$render<T>>['slots'];
33
+ bindings(): "";
34
+ exports(): {};
35
+ }
36
+ interface $$IsomorphicComponent {
37
+ new <T = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
38
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
39
+ } & ReturnType<__sveltets_Render<T>['exports']>;
40
+ <T = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
41
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
42
+ }
43
+ declare const LiteDynamicFormList: $$IsomorphicComponent;
44
+ type LiteDynamicFormList<T = Record<string, unknown>> = InstanceType<typeof LiteDynamicFormList<T>>;
45
+ export default LiteDynamicFormList;