@svadmin/lite 0.6.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 +85 -9
- 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 +22 -0
- package/dist/components/LiteTransfer.svelte +185 -0
- package/dist/components/LiteTransfer.svelte.d.ts +18 -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/LiteListPage.svelte +14 -12
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/lite.css +205 -0
- package/dist/schema-generator.js +15 -4
- package/package.json +4 -3
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
|
|
@@ -29,6 +35,7 @@ The main `@svadmin/ui` package delivers a premium SPA experience using Svelte 5,
|
|
|
29
35
|
| **Capability fallbacks** | Canvas, WASM, realtime, directory upload, observers, storage, media, and other browser-only features have server-safe alternatives |
|
|
30
36
|
| **i18n** | Uses `@svadmin/core` `t()` translations |
|
|
31
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 |
|
|
32
39
|
| **Print** | `@media print` optimized styles |
|
|
33
40
|
|
|
34
41
|
## Quick Start
|
|
@@ -44,6 +51,20 @@ are required peers rather than optional dependencies.
|
|
|
44
51
|
|
|
45
52
|
### 2. Create a list page
|
|
46
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
|
+
|
|
47
68
|
```typescript
|
|
48
69
|
// src/routes/lite/posts/+page.server.ts
|
|
49
70
|
import { createListLoader, createCrudActions } from '@svadmin/lite';
|
|
@@ -223,18 +244,56 @@ If the application is behind a reverse proxy, the same rule can be implemented
|
|
|
223
244
|
there instead. The important property is that the SPA JavaScript is not sent to
|
|
224
245
|
IE11 before the decision is made.
|
|
225
246
|
|
|
226
|
-
## Components
|
|
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
|
|
252
|
+
|
|
253
|
+
| Component | Description |
|
|
254
|
+
|-----------|-------------|
|
|
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
|
|
227
294
|
|
|
228
295
|
| Component | Description |
|
|
229
296
|
|-----------|-------------|
|
|
230
|
-
| `LiteLayout` | Sidebar + main area layout (supports `menu` prop for multi-level menus) |
|
|
231
|
-
| `LiteTable` | HTML table with sort links and delete confirmation |
|
|
232
|
-
| `LiteSearch` | GET-based search form |
|
|
233
|
-
| `LitePagination` | Page number links |
|
|
234
|
-
| `LiteForm` | Auto-generated form from `FieldDefinition[]` |
|
|
235
|
-
| `LiteShow` | Record detail/view page |
|
|
236
|
-
| `LiteLogin` | Login form |
|
|
237
|
-
| `LiteAlert` | Success/error notification banner |
|
|
238
297
|
| `LiteCapabilityBoundary` | Documents and selects a server-safe fallback for an optional browser capability |
|
|
239
298
|
| `LiteVisualFallback` | Snapshot plus accessible table for Canvas, WebGL, map, chart, and flow UIs |
|
|
240
299
|
| `LiteDirectoryUpload` | Directory enhancement with multiple-file and ZIP upload fallbacks |
|
|
@@ -242,6 +301,23 @@ IE11 before the decision is made.
|
|
|
242
301
|
| `LiteComputeFallback` | Native POST action for WASM, Worker, and long-running compute fallback |
|
|
243
302
|
| `LiteOrderedList` | Up/down POST actions for drag-and-drop ordering fallback |
|
|
244
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.
|
|
245
321
|
|
|
246
322
|
## Server Utilities
|
|
247
323
|
|
|
@@ -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;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
|
|
5
|
+
export interface FilterRuleItem {
|
|
6
|
+
id: string;
|
|
7
|
+
field: string;
|
|
8
|
+
operator: CrudOperator;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
fields?: FieldDefinition[];
|
|
14
|
+
filters?: Filter[];
|
|
15
|
+
logicalOperator?: 'and' | 'or';
|
|
16
|
+
action?: string;
|
|
17
|
+
method?: 'GET' | 'POST';
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
onApply?: (filters: Filter[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
fields = [],
|
|
24
|
+
filters = [],
|
|
25
|
+
logicalOperator = 'and',
|
|
26
|
+
action = '',
|
|
27
|
+
disabled = false,
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const operatorOptions: { value: CrudOperator; label: string }[] = [
|
|
31
|
+
{ value: 'eq', label: '= 等于' },
|
|
32
|
+
{ value: 'ne', label: '!= 不等于' },
|
|
33
|
+
{ value: 'contains', label: '包含' },
|
|
34
|
+
{ value: 'ncontains', label: '不包含' },
|
|
35
|
+
{ value: 'gt', label: '> 大于' },
|
|
36
|
+
{ value: 'gte', label: '>= 大于等于' },
|
|
37
|
+
{ value: 'lt', label: '< 小于' },
|
|
38
|
+
{ value: 'lte', label: '<= 小于等于' },
|
|
39
|
+
{ value: 'null', label: '为空 (null)' },
|
|
40
|
+
{ value: 'nnull', label: '不为空' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Flatten initial filters or fallback to 1 empty rule
|
|
44
|
+
const initialRules = $derived.by(() => {
|
|
45
|
+
if (filters.length > 0) {
|
|
46
|
+
return filters.map((f, i) => ({
|
|
47
|
+
id: `rule_${i}`,
|
|
48
|
+
field: 'field' in f ? String(f.field) : (fields[0]?.key ?? ''),
|
|
49
|
+
operator: ('operator' in f ? f.operator : 'eq') as CrudOperator,
|
|
50
|
+
value: 'value' in f ? f.value : '',
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
id: 'rule_0',
|
|
56
|
+
field: fields[0]?.key ?? '',
|
|
57
|
+
operator: 'eq' as CrudOperator,
|
|
58
|
+
value: '',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div class="lite-filter-builder lite-form-group">
|
|
65
|
+
<div class="lite-filter-header">
|
|
66
|
+
<div class="lite-filter-title">
|
|
67
|
+
<strong>{t('common.filters') || '高级多条件筛选'}</strong>
|
|
68
|
+
<select
|
|
69
|
+
name="logical_op"
|
|
70
|
+
class="lite-select lite-select-sm"
|
|
71
|
+
style="width: 120px; display: inline-block; margin-left: 8px;"
|
|
72
|
+
value={logicalOperator}
|
|
73
|
+
{disabled}
|
|
74
|
+
>
|
|
75
|
+
<option value="and">符合全部 (AND)</option>
|
|
76
|
+
<option value="or">符合任一 (OR)</option>
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<button type="submit" name="_action" value="add_filter_rule" class="lite-btn lite-btn-sm" {disabled}>
|
|
81
|
+
+ 添加条件
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="lite-filter-rules">
|
|
87
|
+
{#each initialRules as rule, idx (rule.id)}
|
|
88
|
+
<div class="lite-filter-row">
|
|
89
|
+
<!-- Field select -->
|
|
90
|
+
<select
|
|
91
|
+
name="filter_field_{idx}"
|
|
92
|
+
class="lite-select lite-filter-field"
|
|
93
|
+
value={rule.field}
|
|
94
|
+
{disabled}
|
|
95
|
+
>
|
|
96
|
+
{#each fields as fld (fld.key)}
|
|
97
|
+
<option value={fld.key}>{fld.label || fld.key}</option>
|
|
98
|
+
{/each}
|
|
99
|
+
</select>
|
|
100
|
+
|
|
101
|
+
<!-- Operator select -->
|
|
102
|
+
<select
|
|
103
|
+
name="filter_op_{idx}"
|
|
104
|
+
class="lite-select lite-filter-op"
|
|
105
|
+
value={rule.operator}
|
|
106
|
+
{disabled}
|
|
107
|
+
>
|
|
108
|
+
{#each operatorOptions as op (op.value)}
|
|
109
|
+
<option value={op.value}>{op.label}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</select>
|
|
112
|
+
|
|
113
|
+
<!-- Value input -->
|
|
114
|
+
{#if rule.operator !== 'null' && rule.operator !== 'nnull'}
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
name="filter_val_{idx}"
|
|
118
|
+
class="lite-input lite-filter-val"
|
|
119
|
+
value={String(rule.value ?? '')}
|
|
120
|
+
placeholder="输入筛选值..."
|
|
121
|
+
{disabled}
|
|
122
|
+
/>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
<!-- Remove row button -->
|
|
126
|
+
<button
|
|
127
|
+
type="submit"
|
|
128
|
+
name="_action"
|
|
129
|
+
value="remove_filter_rule_{idx}"
|
|
130
|
+
class="lite-btn lite-btn-sm lite-btn-danger"
|
|
131
|
+
{disabled}
|
|
132
|
+
title="删除此行条件"
|
|
133
|
+
>
|
|
134
|
+
✕
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="lite-filter-footer">
|
|
141
|
+
<button type="submit" class="lite-btn lite-btn-primary">
|
|
142
|
+
应用筛选
|
|
143
|
+
</button>
|
|
144
|
+
<a href={action || '?'} class="lite-btn" style="margin-left: 8px;">
|
|
145
|
+
重置
|
|
146
|
+
</a>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<style>
|
|
151
|
+
.lite-filter-builder {
|
|
152
|
+
border: 1px solid #cbd5e1;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
padding: 14px;
|
|
155
|
+
background: #f8fafc;
|
|
156
|
+
margin-bottom: 16px;
|
|
157
|
+
}
|
|
158
|
+
.lite-filter-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
justify-content: space-between;
|
|
161
|
+
align-items: center;
|
|
162
|
+
margin-bottom: 12px;
|
|
163
|
+
}
|
|
164
|
+
.lite-filter-rules {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
margin-bottom: 12px;
|
|
168
|
+
}
|
|
169
|
+
.lite-filter-row {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
background: #ffffff;
|
|
173
|
+
padding: 8px;
|
|
174
|
+
border: 1px solid #e2e8f0;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
margin-bottom: 8px;
|
|
177
|
+
}
|
|
178
|
+
.lite-filter-row:last-child {
|
|
179
|
+
margin-bottom: 0;
|
|
180
|
+
}
|
|
181
|
+
.lite-filter-row > * {
|
|
182
|
+
margin-right: 8px;
|
|
183
|
+
}
|
|
184
|
+
.lite-filter-row > *:last-child {
|
|
185
|
+
margin-right: 0;
|
|
186
|
+
}
|
|
187
|
+
.lite-filter-field {
|
|
188
|
+
flex: 2;
|
|
189
|
+
min-width: 130px;
|
|
190
|
+
}
|
|
191
|
+
.lite-filter-op {
|
|
192
|
+
flex: 2;
|
|
193
|
+
min-width: 120px;
|
|
194
|
+
}
|
|
195
|
+
.lite-filter-val {
|
|
196
|
+
flex: 3;
|
|
197
|
+
min-width: 140px;
|
|
198
|
+
}
|
|
199
|
+
.lite-filter-footer {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
border-top: 1px solid #e2e8f0;
|
|
203
|
+
padding-top: 10px;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
|
|
2
|
+
export interface FilterRuleItem {
|
|
3
|
+
id: string;
|
|
4
|
+
field: string;
|
|
5
|
+
operator: CrudOperator;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
fields?: FieldDefinition[];
|
|
10
|
+
filters?: Filter[];
|
|
11
|
+
logicalOperator?: 'and' | 'or';
|
|
12
|
+
action?: string;
|
|
13
|
+
method?: 'GET' | 'POST';
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onApply?: (filters: Filter[]) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const LiteFilterBuilder: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type LiteFilterBuilder = ReturnType<typeof LiteFilterBuilder>;
|
|
19
|
+
export default LiteFilterBuilder;
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import { toSafeHref, toSafeText } from "../security";
|
|
8
8
|
import { isExplicitBooleanTrue, getStatusBadgeClass } from "../value-normalization";
|
|
9
9
|
import LiteMediaThumbnail from "./LiteMediaThumbnail.svelte";
|
|
10
|
+
import LiteAvatarField from "./fields/LiteAvatarField.svelte";
|
|
11
|
+
import LiteCurrencyField from "./fields/LiteCurrencyField.svelte";
|
|
12
|
+
import LitePhoneField from "./fields/LitePhoneField.svelte";
|
|
13
|
+
import LiteRatingField from "./fields/LiteRatingField.svelte";
|
|
14
|
+
import LiteCodeField from "./fields/LiteCodeField.svelte";
|
|
10
15
|
|
|
11
16
|
interface Props {
|
|
12
17
|
field: FieldDefinition;
|
|
@@ -42,6 +47,23 @@
|
|
|
42
47
|
{@const checked = isExplicitBooleanTrue(value)}
|
|
43
48
|
<span class="lite-bool {checked ? "lite-bool-true" : ""}"></span>
|
|
44
49
|
{checked ? "✓ Yes" : "✗ No"}
|
|
50
|
+
{:else if field.type === "avatar"}
|
|
51
|
+
<LiteAvatarField {field} value={value as unknown} mode="show" />
|
|
52
|
+
{:else if field.type === "rating"}
|
|
53
|
+
<LiteRatingField {field} value={value as string | number | null | undefined} mode="show" />
|
|
54
|
+
{:else if field.type === "code"}
|
|
55
|
+
<LiteCodeField {field} value={value as string | number | Record<string, unknown> | unknown[] | null | undefined} mode="show" />
|
|
56
|
+
{:else if field.type === "currency"}
|
|
57
|
+
<LiteCurrencyField {field} value={value as string | number | null | undefined} mode="show" />
|
|
58
|
+
{:else if field.type === "phone"}
|
|
59
|
+
<LitePhoneField {field} value={value as string | number | null | undefined} mode="show" />
|
|
60
|
+
{:else if field.type === "password"}
|
|
61
|
+
<span>••••••••</span>
|
|
62
|
+
{:else if field.type === "color" && value}
|
|
63
|
+
<span style="display:inline-flex;align-items:center;">
|
|
64
|
+
<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{toSafeText(value)};border:1px solid #cbd5e1;margin-right:6px;"></span>
|
|
65
|
+
<span class="lite-font-mono">{toSafeText(value)}</span>
|
|
66
|
+
</span>
|
|
45
67
|
{:else if field.type === "url" && value}
|
|
46
68
|
{@const href = toSafeHref(value)}
|
|
47
69
|
{#if href}
|