@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.
Files changed (36) hide show
  1. package/README.md +85 -9
  2. package/dist/components/LiteDynamicFormList.svelte +169 -0
  3. package/dist/components/LiteDynamicFormList.svelte.d.ts +45 -0
  4. package/dist/components/LiteFilterBuilder.svelte +205 -0
  5. package/dist/components/LiteFilterBuilder.svelte.d.ts +19 -0
  6. package/dist/components/LiteShowField.svelte +22 -0
  7. package/dist/components/LiteTransfer.svelte +185 -0
  8. package/dist/components/LiteTransfer.svelte.d.ts +18 -0
  9. package/dist/components/fields/LiteAvatarField.svelte +127 -0
  10. package/dist/components/fields/LiteAvatarField.svelte.d.ts +21 -0
  11. package/dist/components/fields/LiteCascader.svelte +119 -0
  12. package/dist/components/fields/LiteCascader.svelte.d.ts +24 -0
  13. package/dist/components/fields/LiteCodeField.svelte +71 -0
  14. package/dist/components/fields/LiteCodeField.svelte.d.ts +16 -0
  15. package/dist/components/fields/LiteCopyField.svelte +74 -0
  16. package/dist/components/fields/LiteCopyField.svelte.d.ts +18 -0
  17. package/dist/components/fields/LiteCurrencyField.svelte +126 -0
  18. package/dist/components/fields/LiteCurrencyField.svelte.d.ts +21 -0
  19. package/dist/components/fields/LiteDateRangeField.svelte +147 -0
  20. package/dist/components/fields/LiteDateRangeField.svelte.d.ts +26 -0
  21. package/dist/components/fields/LitePercentField.svelte +124 -0
  22. package/dist/components/fields/LitePercentField.svelte.d.ts +17 -0
  23. package/dist/components/fields/LitePhoneField.svelte +81 -0
  24. package/dist/components/fields/LitePhoneField.svelte.d.ts +17 -0
  25. package/dist/components/fields/LiteRatingField.svelte +105 -0
  26. package/dist/components/fields/LiteRatingField.svelte.d.ts +16 -0
  27. package/dist/components/fields/LiteTreeSelect.svelte +143 -0
  28. package/dist/components/fields/LiteTreeSelect.svelte.d.ts +23 -0
  29. package/dist/components/fields/index.d.ts +12 -0
  30. package/dist/components/fields/index.js +10 -0
  31. package/dist/components/pages/LiteListPage.svelte +14 -12
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.js +3 -0
  34. package/dist/lite.css +205 -0
  35. package/dist/schema-generator.js +15 -4
  36. 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}