@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.
- package/README.md +190 -9
- package/dist/compatibility.d.ts +23 -0
- package/dist/compatibility.js +95 -0
- package/dist/components/LiteDynamicFormList.svelte +169 -0
- package/dist/components/LiteDynamicFormList.svelte.d.ts +45 -0
- package/dist/components/LiteFilterBuilder.svelte +205 -0
- package/dist/components/LiteFilterBuilder.svelte.d.ts +19 -0
- package/dist/components/LiteShowField.svelte +52 -22
- package/dist/components/LiteShowField.svelte.d.ts +3 -2
- package/dist/components/LiteTable.svelte +89 -36
- package/dist/components/LiteTable.svelte.d.ts +3 -2
- package/dist/components/LiteTransfer.svelte +185 -0
- package/dist/components/LiteTransfer.svelte.d.ts +18 -0
- package/dist/components/compatibility/LiteCapabilityBoundary.svelte +51 -0
- package/dist/components/compatibility/LiteCapabilityBoundary.svelte.d.ts +13 -0
- package/dist/components/compatibility/LiteClipboardFallback.svelte +16 -0
- package/dist/components/compatibility/LiteClipboardFallback.svelte.d.ts +8 -0
- package/dist/components/compatibility/LiteComputeFallback.svelte +50 -0
- package/dist/components/compatibility/LiteComputeFallback.svelte.d.ts +14 -0
- package/dist/components/compatibility/LiteDirectoryUpload.svelte +44 -0
- package/dist/components/compatibility/LiteDirectoryUpload.svelte.d.ts +11 -0
- package/dist/components/compatibility/LiteOrderedList.svelte +47 -0
- package/dist/components/compatibility/LiteOrderedList.svelte.d.ts +14 -0
- package/dist/components/compatibility/LiteRealtimeStatus.svelte +43 -0
- package/dist/components/compatibility/LiteRealtimeStatus.svelte.d.ts +11 -0
- package/dist/components/compatibility/LiteVisualFallback.svelte +80 -0
- package/dist/components/compatibility/LiteVisualFallback.svelte.d.ts +18 -0
- package/dist/components/compatibility/index.d.ts +7 -0
- package/dist/components/compatibility/index.js +7 -0
- package/dist/components/fields/LiteAvatarField.svelte +127 -0
- package/dist/components/fields/LiteAvatarField.svelte.d.ts +21 -0
- package/dist/components/fields/LiteCascader.svelte +119 -0
- package/dist/components/fields/LiteCascader.svelte.d.ts +24 -0
- package/dist/components/fields/LiteCodeField.svelte +71 -0
- package/dist/components/fields/LiteCodeField.svelte.d.ts +16 -0
- package/dist/components/fields/LiteCopyField.svelte +74 -0
- package/dist/components/fields/LiteCopyField.svelte.d.ts +18 -0
- package/dist/components/fields/LiteCurrencyField.svelte +126 -0
- package/dist/components/fields/LiteCurrencyField.svelte.d.ts +21 -0
- package/dist/components/fields/LiteDateRangeField.svelte +147 -0
- package/dist/components/fields/LiteDateRangeField.svelte.d.ts +26 -0
- package/dist/components/fields/LitePercentField.svelte +124 -0
- package/dist/components/fields/LitePercentField.svelte.d.ts +17 -0
- package/dist/components/fields/LitePhoneField.svelte +81 -0
- package/dist/components/fields/LitePhoneField.svelte.d.ts +17 -0
- package/dist/components/fields/LiteRatingField.svelte +105 -0
- package/dist/components/fields/LiteRatingField.svelte.d.ts +16 -0
- package/dist/components/fields/LiteTreeSelect.svelte +143 -0
- package/dist/components/fields/LiteTreeSelect.svelte.d.ts +23 -0
- package/dist/components/fields/index.d.ts +12 -0
- package/dist/components/fields/index.js +10 -0
- package/dist/components/pages/LiteCreatePage.svelte +15 -6
- package/dist/components/pages/LiteCreatePage.svelte.d.ts +1 -1
- package/dist/components/pages/LiteEditPage.svelte +18 -9
- package/dist/components/pages/LiteEditPage.svelte.d.ts +1 -1
- package/dist/components/pages/LiteListPage.svelte +99 -27
- package/dist/components/pages/LiteListPage.svelte.d.ts +4 -2
- package/dist/components/pages/LiteShowPage.svelte +19 -10
- package/dist/components/pages/LiteShowPage.svelte.d.ts +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -1
- package/dist/lite.css +444 -0
- package/dist/schema-generator.js +15 -4
- package/dist/server-adapter.d.ts +18 -1
- package/dist/server-adapter.js +73 -16
- 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
|
-
|
|
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` |
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
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;
|