@svadmin/lite 0.3.4 → 0.3.6
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/package.json +9 -6
- package/src/server-adapter.ts +1 -1
- package/src/components/LiteAlert.svelte +0 -19
- package/src/components/LiteArrayField.svelte +0 -113
- package/src/components/LiteAuditLog.svelte +0 -104
- package/src/components/LiteBreadcrumbs.svelte +0 -39
- package/src/components/LiteChatDialog.svelte +0 -101
- package/src/components/LiteConfirmDialog.svelte +0 -60
- package/src/components/LiteEmptyState.svelte +0 -39
- package/src/components/LiteForm.svelte +0 -138
- package/src/components/LiteLayout.svelte +0 -106
- package/src/components/LiteLogin.svelte +0 -41
- package/src/components/LitePagination.svelte +0 -73
- package/src/components/LitePermissionMatrix.svelte +0 -147
- package/src/components/LiteSearch.svelte +0 -41
- package/src/components/LiteShow.svelte +0 -61
- package/src/components/LiteShowField.svelte +0 -51
- package/src/components/LiteStatsCard.svelte +0 -70
- package/src/components/LiteTable.svelte +0 -132
- package/src/components/LiteTabs.svelte +0 -57
- package/src/components/advanced/LiteAutoSaveIndicator.svelte +0 -26
- package/src/components/advanced/LiteDraggableHeader.svelte +0 -33
- package/src/components/advanced/LiteDrawerForm.svelte +0 -42
- package/src/components/advanced/LiteInlineEdit.svelte +0 -32
- package/src/components/advanced/LiteModalForm.svelte +0 -44
- package/src/components/advanced/LiteToast.svelte +0 -34
- package/src/components/advanced/LiteUndoableNotification.svelte +0 -25
- package/src/components/advanced/LiteVirtualTable.svelte +0 -44
- package/src/components/buttons/LiteCloneButton.svelte +0 -33
- package/src/components/buttons/LiteCreateButton.svelte +0 -31
- package/src/components/buttons/LiteDeleteButton.svelte +0 -57
- package/src/components/buttons/LiteEditButton.svelte +0 -33
- package/src/components/buttons/LiteExportButton.svelte +0 -31
- package/src/components/buttons/LiteImportButton.svelte +0 -50
- package/src/components/buttons/LiteListButton.svelte +0 -31
- package/src/components/buttons/LiteRefreshButton.svelte +0 -27
- package/src/components/buttons/LiteSaveButton.svelte +0 -27
- package/src/components/buttons/LiteShowButton.svelte +0 -33
- package/src/components/fields/LiteBooleanField.svelte +0 -41
- package/src/components/fields/LiteDateField.svelte +0 -51
- package/src/components/fields/LiteEmailField.svelte +0 -40
- package/src/components/fields/LiteFileField.svelte +0 -53
- package/src/components/fields/LiteImageField.svelte +0 -57
- package/src/components/fields/LiteJsonField.svelte +0 -43
- package/src/components/fields/LiteMarkdownField.svelte +0 -33
- package/src/components/fields/LiteMultiSelectField.svelte +0 -57
- package/src/components/fields/LiteNumberField.svelte +0 -34
- package/src/components/fields/LiteRelationField.svelte +0 -66
- package/src/components/fields/LiteRichTextField.svelte +0 -45
- package/src/components/fields/LiteSelectField.svelte +0 -47
- package/src/components/fields/LiteTagField.svelte +0 -44
- package/src/components/fields/LiteTextField.svelte +0 -34
- package/src/components/fields/LiteUrlField.svelte +0 -40
- package/src/components/layout/LiteAuthenticated.svelte +0 -23
- package/src/components/layout/LiteCanAccess.svelte +0 -27
- package/src/components/layout/LiteCatchAllNavigate.svelte +0 -20
- package/src/components/layout/LiteErrorBoundary.svelte +0 -20
- package/src/components/layout/LiteErrorComponent.svelte +0 -37
- package/src/components/layout/LiteHeader.svelte +0 -19
- package/src/components/layout/LiteNavigateToResource.svelte +0 -25
- package/src/components/layout/LiteSidebar.svelte +0 -93
- package/src/components/pages/LiteCreatePage.svelte +0 -39
- package/src/components/pages/LiteEditPage.svelte +0 -54
- package/src/components/pages/LiteForgotPasswordPage.svelte +0 -60
- package/src/components/pages/LiteListPage.svelte +0 -81
- package/src/components/pages/LiteProfilePage.svelte +0 -61
- package/src/components/pages/LiteRegisterPage.svelte +0 -64
- package/src/components/pages/LiteShowPage.svelte +0 -61
- package/src/components/pages/LiteUpdatePasswordPage.svelte +0 -51
- package/src/components/widgets/LiteAnomalyBadge.svelte +0 -40
- package/src/components/widgets/LiteBarChart.svelte +0 -45
- package/src/components/widgets/LiteInsightCard.svelte +0 -28
- package/src/components/widgets/LiteLineChart.svelte +0 -48
- package/src/components/widgets/LitePieChart.svelte +0 -44
- package/src/lite.css +0 -708
- package/static/enhance.js +0 -56
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteLayout — Minimal server-rendered sidebar + main layout.
|
|
4
|
-
* No client-side JS required. Uses only IE11-safe CSS classes.
|
|
5
|
-
* Supports multi-level menus via <details>/<summary> (pure HTML).
|
|
6
|
-
*/
|
|
7
|
-
import type { ResourceDefinition, MenuItem } from '@svadmin/core';
|
|
8
|
-
import { t } from '@svadmin/core/i18n';
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
resources: ResourceDefinition[];
|
|
12
|
-
currentResource?: string;
|
|
13
|
-
brandName?: string;
|
|
14
|
-
userName?: string;
|
|
15
|
-
basePath?: string;
|
|
16
|
-
/** Optional multi-level menu configuration */
|
|
17
|
-
menu?: MenuItem[];
|
|
18
|
-
children: any;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let {
|
|
22
|
-
resources,
|
|
23
|
-
currentResource = '',
|
|
24
|
-
brandName = 'Admin',
|
|
25
|
-
userName = '',
|
|
26
|
-
basePath = '/lite',
|
|
27
|
-
menu,
|
|
28
|
-
children,
|
|
29
|
-
}: Props = $props();
|
|
30
|
-
|
|
31
|
-
const menuResources = $derived(
|
|
32
|
-
resources.filter((r: ResourceDefinition) => r.showInMenu !== false)
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
function isActive(href: string | undefined): boolean {
|
|
36
|
-
if (!href) return false;
|
|
37
|
-
return currentResource === href.replace(basePath + '/', '');
|
|
38
|
-
}
|
|
39
|
-
</script>
|
|
40
|
-
|
|
41
|
-
<div class="lite-wrapper">
|
|
42
|
-
<!-- Sidebar -->
|
|
43
|
-
<nav class="lite-sidebar">
|
|
44
|
-
<div class="lite-sidebar-brand">{brandName}</div>
|
|
45
|
-
{#if menu && menu.length > 0}
|
|
46
|
-
{#each menu as item}
|
|
47
|
-
{#if item.children && item.children.length > 0}
|
|
48
|
-
<details class="lite-menu-group">
|
|
49
|
-
<summary class="lite-menu-parent">{item.label ?? item.name}</summary>
|
|
50
|
-
{#each item.children as child}
|
|
51
|
-
{#if child.children && child.children.length > 0}
|
|
52
|
-
<details class="lite-menu-group" style="margin-left:12px">
|
|
53
|
-
<summary class="lite-menu-parent">{child.label ?? child.name}</summary>
|
|
54
|
-
{#each child.children as grandchild}
|
|
55
|
-
<a
|
|
56
|
-
href={grandchild.href ?? `${basePath}/${grandchild.name}`}
|
|
57
|
-
class={isActive(grandchild.href ?? `${basePath}/${grandchild.name}`) ? 'active' : ''}
|
|
58
|
-
target={grandchild.target === '_blank' ? '_blank' : undefined}
|
|
59
|
-
style="padding-left:40px"
|
|
60
|
-
>{grandchild.label ?? grandchild.name}</a>
|
|
61
|
-
{/each}
|
|
62
|
-
</details>
|
|
63
|
-
{:else}
|
|
64
|
-
<a
|
|
65
|
-
href={child.href ?? `${basePath}/${child.name}`}
|
|
66
|
-
class={isActive(child.href ?? `${basePath}/${child.name}`) ? 'active' : ''}
|
|
67
|
-
target={child.target === '_blank' ? '_blank' : undefined}
|
|
68
|
-
style="padding-left:28px"
|
|
69
|
-
>{child.label ?? child.name}</a>
|
|
70
|
-
{/if}
|
|
71
|
-
{/each}
|
|
72
|
-
</details>
|
|
73
|
-
{:else}
|
|
74
|
-
<a
|
|
75
|
-
href={item.href ?? `${basePath}/${item.name}`}
|
|
76
|
-
class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
|
|
77
|
-
target={item.target === '_blank' ? '_blank' : undefined}
|
|
78
|
-
>{item.label ?? item.name}</a>
|
|
79
|
-
{/if}
|
|
80
|
-
{/each}
|
|
81
|
-
{:else}
|
|
82
|
-
{#each menuResources as res}
|
|
83
|
-
<a
|
|
84
|
-
href={`${basePath}/${res.name}`}
|
|
85
|
-
class={res.name === currentResource ? 'active' : ''}
|
|
86
|
-
>
|
|
87
|
-
{res.label}
|
|
88
|
-
</a>
|
|
89
|
-
{/each}
|
|
90
|
-
{/if}
|
|
91
|
-
{#if userName}
|
|
92
|
-
<div style="position:absolute;bottom:0;left:0;right:0;padding:12px 16px;border-top:1px solid #334155;font-size:12px;color:#94a3b8;">
|
|
93
|
-
{userName}
|
|
94
|
-
<form method="POST" action={`${basePath}/login?/logout`} style="display:inline;margin-left:8px;">
|
|
95
|
-
<button type="submit" class="lite-btn lite-btn-sm" style="color:#94a3b8;border-color:#475569;background:transparent;">Logout</button>
|
|
96
|
-
</form>
|
|
97
|
-
</div>
|
|
98
|
-
{/if}
|
|
99
|
-
</nav>
|
|
100
|
-
|
|
101
|
-
<!-- Main Content -->
|
|
102
|
-
<main class="lite-main">
|
|
103
|
-
{@render children()}
|
|
104
|
-
</main>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteLogin — Simple login form for SSR.
|
|
4
|
-
* Posts to SvelteKit form action, no JS needed.
|
|
5
|
-
*/
|
|
6
|
-
import { t } from '@svadmin/core/i18n';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
error?: string;
|
|
10
|
-
brandName?: string;
|
|
11
|
-
action?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let { error, brandName = 'Admin', action = '?/login' }: Props = $props();
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<div class="lite-login-wrapper">
|
|
18
|
-
<div class="lite-login-card">
|
|
19
|
-
<div class="lite-card">
|
|
20
|
-
<h2 style="text-align:center;margin:0 0 20px;font-size:20px;font-weight:600;">{brandName}</h2>
|
|
21
|
-
|
|
22
|
-
{#if error}
|
|
23
|
-
<div class="lite-alert lite-alert-error">{error}</div>
|
|
24
|
-
{/if}
|
|
25
|
-
|
|
26
|
-
<form method="POST" action={action}>
|
|
27
|
-
<div class="lite-form-group">
|
|
28
|
-
<label for="email">{t('auth.email') || 'Email'}</label>
|
|
29
|
-
<input type="text" name="email" id="email" class="lite-input" placeholder="user@example.com" required />
|
|
30
|
-
</div>
|
|
31
|
-
<div class="lite-form-group">
|
|
32
|
-
<label for="password">{t('auth.password') || 'Password'}</label>
|
|
33
|
-
<input type="password" name="password" id="password" class="lite-input" required />
|
|
34
|
-
</div>
|
|
35
|
-
<button type="submit" class="lite-btn lite-btn-primary" style="width:100%;padding:10px;">
|
|
36
|
-
{t('auth.signIn') || 'Sign In'}
|
|
37
|
-
</button>
|
|
38
|
-
</form>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LitePagination — Pure <a> tag pagination for SSR.
|
|
4
|
-
* No JavaScript. All page transitions are full page navigations.
|
|
5
|
-
*/
|
|
6
|
-
import { t } from '@svadmin/core/i18n';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
page: number;
|
|
10
|
-
totalPages: number;
|
|
11
|
-
/** Base URL that page numbers will be appended to */
|
|
12
|
-
baseUrl?: string;
|
|
13
|
-
/** Additional parameters to preserve (like sort, order, q) */
|
|
14
|
-
preserveParams?: Record<string, string>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let { page, totalPages, baseUrl = '', preserveParams = {} }: Props = $props();
|
|
18
|
-
|
|
19
|
-
// Show max 7 page links
|
|
20
|
-
const visiblePages = $derived.by(() => {
|
|
21
|
-
const pages: (number | '...')[] = [];
|
|
22
|
-
if (totalPages <= 7) {
|
|
23
|
-
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
|
24
|
-
} else {
|
|
25
|
-
pages.push(1);
|
|
26
|
-
if (page > 3) pages.push('...');
|
|
27
|
-
for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) {
|
|
28
|
-
pages.push(i);
|
|
29
|
-
}
|
|
30
|
-
if (page < totalPages - 2) pages.push('...');
|
|
31
|
-
pages.push(totalPages);
|
|
32
|
-
}
|
|
33
|
-
return pages;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
function pageUrl(p: number): string {
|
|
37
|
-
const params = new URLSearchParams(preserveParams);
|
|
38
|
-
params.set('page', String(p));
|
|
39
|
-
const qs = params.toString();
|
|
40
|
-
const sep = baseUrl.includes('?') ? '&' : '?';
|
|
41
|
-
return `${baseUrl}${baseUrl && !baseUrl.includes('?') ? '?' : sep}${qs}`;
|
|
42
|
-
}
|
|
43
|
-
</script>
|
|
44
|
-
|
|
45
|
-
{#if totalPages > 1}
|
|
46
|
-
<nav class="lite-pagination">
|
|
47
|
-
{#if page > 1}
|
|
48
|
-
<a href={pageUrl(page - 1)}>← {t('common.prev') || 'Prev'}</a>
|
|
49
|
-
{:else}
|
|
50
|
-
<span class="disabled">← {t('common.prev') || 'Prev'}</span>
|
|
51
|
-
{/if}
|
|
52
|
-
|
|
53
|
-
{#each visiblePages as p}
|
|
54
|
-
{#if p === '...'}
|
|
55
|
-
<span class="disabled">…</span>
|
|
56
|
-
{:else if p === page}
|
|
57
|
-
<span class="active">{p}</span>
|
|
58
|
-
{:else}
|
|
59
|
-
<a href={pageUrl(p)}>{p}</a>
|
|
60
|
-
{/if}
|
|
61
|
-
{/each}
|
|
62
|
-
|
|
63
|
-
{#if page < totalPages}
|
|
64
|
-
<a href={pageUrl(page + 1)}>{t('common.next') || 'Next'} →</a>
|
|
65
|
-
{:else}
|
|
66
|
-
<span class="disabled">{t('common.next') || 'Next'} →</span>
|
|
67
|
-
{/if}
|
|
68
|
-
|
|
69
|
-
<span class="lite-pagination-info">
|
|
70
|
-
{t('common.pageOf', { page, total: totalPages }) || `Page ${page} of ${totalPages}`}
|
|
71
|
-
</span>
|
|
72
|
-
</nav>
|
|
73
|
-
{/if}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LitePermissionMatrix — SSR-compatible permission matrix.
|
|
4
|
-
* Pure HTML table with checkbox inputs. No client-side JS required.
|
|
5
|
-
* Works with form POST actions for state changes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
interface LiteRole {
|
|
9
|
-
code: string;
|
|
10
|
-
name: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface LiteResource {
|
|
14
|
-
code: string;
|
|
15
|
-
name: string;
|
|
16
|
-
section?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface LiteAction {
|
|
20
|
-
code: string;
|
|
21
|
-
name: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface Props {
|
|
25
|
-
roles: LiteRole[];
|
|
26
|
-
resources: LiteResource[];
|
|
27
|
-
actions: LiteAction[];
|
|
28
|
-
/** Current permissions map: { "resource:action": true } */
|
|
29
|
-
permissions: Record<string, boolean>;
|
|
30
|
-
selectedRole?: string;
|
|
31
|
-
/** Form POST action URL */
|
|
32
|
-
actionUrl?: string;
|
|
33
|
-
disabled?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let {
|
|
37
|
-
roles,
|
|
38
|
-
resources,
|
|
39
|
-
actions,
|
|
40
|
-
permissions,
|
|
41
|
-
selectedRole = roles[0]?.code || '',
|
|
42
|
-
actionUrl = '?/updatePermissions',
|
|
43
|
-
disabled = false,
|
|
44
|
-
}: Props = $props();
|
|
45
|
-
|
|
46
|
-
function isGranted(resource: string, action: string): boolean {
|
|
47
|
-
return permissions[`${resource}:${action}`] === true;
|
|
48
|
-
}
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<div class="lite-permission-matrix">
|
|
52
|
-
<!-- Role Tabs -->
|
|
53
|
-
<div class="lite-role-tabs">
|
|
54
|
-
{#each roles as role}
|
|
55
|
-
<a
|
|
56
|
-
href="?role={role.code}"
|
|
57
|
-
class="lite-role-tab {selectedRole === role.code ? 'active' : ''}"
|
|
58
|
-
>
|
|
59
|
-
{role.name}
|
|
60
|
-
</a>
|
|
61
|
-
{/each}
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<!-- Matrix Table -->
|
|
65
|
-
<form method="POST" action={actionUrl}>
|
|
66
|
-
<input type="hidden" name="role" value={selectedRole} />
|
|
67
|
-
|
|
68
|
-
<table class="lite-table" style="margin-top:0;">
|
|
69
|
-
<thead>
|
|
70
|
-
<tr>
|
|
71
|
-
<th style="width:200px;">Resource</th>
|
|
72
|
-
{#each actions as action}
|
|
73
|
-
<th style="text-align:center;">{action.name}</th>
|
|
74
|
-
{/each}
|
|
75
|
-
</tr>
|
|
76
|
-
</thead>
|
|
77
|
-
<tbody>
|
|
78
|
-
{#each resources as resource, i}
|
|
79
|
-
{#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
|
|
80
|
-
<tr>
|
|
81
|
-
<td colspan={actions.length + 1} style="background:#f1f5f9;font-weight:700;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:#475569;">
|
|
82
|
-
{resource.section}
|
|
83
|
-
</td>
|
|
84
|
-
</tr>
|
|
85
|
-
{/if}
|
|
86
|
-
<tr>
|
|
87
|
-
<td><strong>{resource.name}</strong><br/><small style="color:#94a3b8;">{resource.code}</small></td>
|
|
88
|
-
{#each actions as action}
|
|
89
|
-
<td style="text-align:center;">
|
|
90
|
-
<input
|
|
91
|
-
type="checkbox"
|
|
92
|
-
name="perm_{resource.code}_{action.code}"
|
|
93
|
-
checked={isGranted(resource.code, action.code)}
|
|
94
|
-
{disabled}
|
|
95
|
-
style="width:18px;height:18px;cursor:{disabled ? 'not-allowed' : 'pointer'};"
|
|
96
|
-
/>
|
|
97
|
-
</td>
|
|
98
|
-
{/each}
|
|
99
|
-
</tr>
|
|
100
|
-
{/each}
|
|
101
|
-
{#if resources.length === 0}
|
|
102
|
-
<tr>
|
|
103
|
-
<td colspan={actions.length + 1} style="text-align:center;padding:24px;color:#94a3b8;">
|
|
104
|
-
No resources configured.
|
|
105
|
-
</td>
|
|
106
|
-
</tr>
|
|
107
|
-
{/if}
|
|
108
|
-
</tbody>
|
|
109
|
-
</table>
|
|
110
|
-
|
|
111
|
-
{#if !disabled}
|
|
112
|
-
<div style="margin-top:12px;text-align:right;">
|
|
113
|
-
<button type="submit" class="lite-btn lite-btn-primary">Save Permissions</button>
|
|
114
|
-
</div>
|
|
115
|
-
{/if}
|
|
116
|
-
</form>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<style>
|
|
120
|
-
.lite-role-tabs {
|
|
121
|
-
display: flex;
|
|
122
|
-
flex-wrap: wrap;
|
|
123
|
-
margin-bottom: 16px;
|
|
124
|
-
border-bottom: 2px solid #e2e8f0;
|
|
125
|
-
padding-bottom: 0;
|
|
126
|
-
}
|
|
127
|
-
.lite-role-tab {
|
|
128
|
-
padding: 8px 16px;
|
|
129
|
-
margin-right: 4px;
|
|
130
|
-
text-decoration: none;
|
|
131
|
-
color: #64748b;
|
|
132
|
-
font-size: 14px;
|
|
133
|
-
font-weight: 500;
|
|
134
|
-
border-bottom: 2px solid transparent;
|
|
135
|
-
margin-bottom: -2px;
|
|
136
|
-
transition: color 0.15s ease, border-color 0.15s ease;
|
|
137
|
-
}
|
|
138
|
-
.lite-role-tab:hover {
|
|
139
|
-
color: #0f172a;
|
|
140
|
-
text-decoration: none;
|
|
141
|
-
}
|
|
142
|
-
.lite-role-tab.active {
|
|
143
|
-
color: #4f46e5;
|
|
144
|
-
border-bottom-color: #4f46e5;
|
|
145
|
-
font-weight: 600;
|
|
146
|
-
}
|
|
147
|
-
</style>
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteSearch — Search bar using native <form> GET submission.
|
|
4
|
-
* No JS needed — submits as ?q=searchterm via page navigation.
|
|
5
|
-
*/
|
|
6
|
-
import { t } from '@svadmin/core/i18n';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
value?: string;
|
|
10
|
-
placeholder?: string;
|
|
11
|
-
/** Additional hidden fields to preserve (e.g. sort, order) */
|
|
12
|
-
preserveParams?: Record<string, string>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let {
|
|
16
|
-
value = '',
|
|
17
|
-
placeholder,
|
|
18
|
-
preserveParams = {},
|
|
19
|
-
}: Props = $props();
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<form method="GET" class="lite-search">
|
|
23
|
-
{#each Object.entries(preserveParams) as [key, val]}
|
|
24
|
-
<input type="hidden" name={key} value={val} />
|
|
25
|
-
{/each}
|
|
26
|
-
<input
|
|
27
|
-
type="text"
|
|
28
|
-
name="q"
|
|
29
|
-
value={value}
|
|
30
|
-
class="lite-input"
|
|
31
|
-
placeholder={placeholder || t('common.search') || 'Search...'}
|
|
32
|
-
/>
|
|
33
|
-
<button type="submit" class="lite-btn lite-btn-primary">
|
|
34
|
-
{t('common.search') || 'Search'}
|
|
35
|
-
</button>
|
|
36
|
-
{#if value}
|
|
37
|
-
<a href="?" class="lite-btn" style="margin-left:4px;">
|
|
38
|
-
{t('common.clear') || 'Clear'}
|
|
39
|
-
</a>
|
|
40
|
-
{/if}
|
|
41
|
-
</form>
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteShow — Detail/view page for a single record.
|
|
4
|
-
* Renders field labels and values in a key-value layout.
|
|
5
|
-
*/
|
|
6
|
-
import type { ResourceDefinition } from '@svadmin/core';
|
|
7
|
-
import { t } from '@svadmin/core/i18n';
|
|
8
|
-
import LiteShowField from './LiteShowField.svelte';
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
record: Record<string, unknown>;
|
|
12
|
-
resource: ResourceDefinition;
|
|
13
|
-
basePath?: string;
|
|
14
|
-
canEdit?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let {
|
|
18
|
-
record,
|
|
19
|
-
resource,
|
|
20
|
-
basePath = '/lite',
|
|
21
|
-
canEdit = true,
|
|
22
|
-
}: Props = $props();
|
|
23
|
-
|
|
24
|
-
let pk = $derived(resource.primaryKey ?? 'id');
|
|
25
|
-
let id = $derived(record[pk]);
|
|
26
|
-
const showFields = $derived(
|
|
27
|
-
resource.fields.filter(f => f.showInShow !== false)
|
|
28
|
-
);
|
|
29
|
-
</script>
|
|
30
|
-
|
|
31
|
-
<div class="lite-card">
|
|
32
|
-
<div class="lite-header">
|
|
33
|
-
<h1>{resource.label} #{id}</h1>
|
|
34
|
-
<div>
|
|
35
|
-
{#if canEdit}
|
|
36
|
-
<a href={`${basePath}/${resource.name}/edit/${id}`} class="lite-btn lite-btn-primary">
|
|
37
|
-
{t('common.edit') || 'Edit'}
|
|
38
|
-
</a>
|
|
39
|
-
{/if}
|
|
40
|
-
<a href={`${basePath}/${resource.name}`} class="lite-btn" style="margin-left:8px;">
|
|
41
|
-
{t('common.backToList') || 'Back to List'}
|
|
42
|
-
</a>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<table class="lite-table">
|
|
47
|
-
<tbody>
|
|
48
|
-
{#each showFields as field}
|
|
49
|
-
{@const value = record[field.key]}
|
|
50
|
-
<tr>
|
|
51
|
-
<td style="width:200px;font-weight:600;color:#64748b;vertical-align:top;">
|
|
52
|
-
{field.label}
|
|
53
|
-
</td>
|
|
54
|
-
<td>
|
|
55
|
-
<LiteShowField {field} {value} />
|
|
56
|
-
</td>
|
|
57
|
-
</tr>
|
|
58
|
-
{/each}
|
|
59
|
-
</tbody>
|
|
60
|
-
</table>
|
|
61
|
-
</div>
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteShowField — SSR-compatible field renderer for detail views.
|
|
4
|
-
* Renders a single field value based on its type definition.
|
|
5
|
-
*/
|
|
6
|
-
import type { FieldDefinition } from '@svadmin/core';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
field: FieldDefinition;
|
|
10
|
-
value: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let { field, value }: Props = $props();
|
|
14
|
-
|
|
15
|
-
function formatValue(v: unknown, f: FieldDefinition): string {
|
|
16
|
-
if (v == null) return '—';
|
|
17
|
-
if (f.type === 'date') {
|
|
18
|
-
try { return new Date(v as string).toLocaleString(); } catch { return String(v); }
|
|
19
|
-
}
|
|
20
|
-
if (f.type === 'select' && f.options) {
|
|
21
|
-
const opt = f.options.find(o => o.value === v);
|
|
22
|
-
return opt?.label ?? String(v);
|
|
23
|
-
}
|
|
24
|
-
if (f.type === 'url') return String(v);
|
|
25
|
-
if (f.type === 'email') return String(v);
|
|
26
|
-
if (Array.isArray(v)) return v.join(', ');
|
|
27
|
-
if (typeof v === 'object') {
|
|
28
|
-
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
|
|
29
|
-
}
|
|
30
|
-
return String(v);
|
|
31
|
-
}
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
{#if field.type === 'boolean'}
|
|
35
|
-
<span class="lite-bool {value ? 'lite-bool-true' : ''}"></span>
|
|
36
|
-
{value ? '✓ Yes' : '✗ No'}
|
|
37
|
-
{:else if field.type === 'url' && value}
|
|
38
|
-
<a href={String(value)} target="_blank" rel="noopener">{value}</a>
|
|
39
|
-
{:else if field.type === 'email' && value}
|
|
40
|
-
<a href="mailto:{value}">{value}</a>
|
|
41
|
-
{:else if field.type === 'image' && value}
|
|
42
|
-
<img src={String(value)} alt={field.label} style="max-width:300px;max-height:200px;border-radius:6px;border:1px solid #e2e8f0;" />
|
|
43
|
-
{:else if field.type === 'tags' && Array.isArray(value)}
|
|
44
|
-
{#each value as tag}
|
|
45
|
-
<span class="lite-badge">{tag}</span>
|
|
46
|
-
{/each}
|
|
47
|
-
{:else if field.type === 'json' && value}
|
|
48
|
-
<pre style="margin:0;font-size:12px;background:#f8fafc;padding:12px;border-radius:6px;border:1px solid #e2e8f0;overflow-x:auto;">{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}</pre>
|
|
49
|
-
{:else}
|
|
50
|
-
{formatValue(value, field)}
|
|
51
|
-
{/if}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteStatsCard — SSR-compatible KPI statistics card.
|
|
4
|
-
* Pure HTML card with number, label, and optional trend indicator.
|
|
5
|
-
* No client-side JS required.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
/** The main number/value to display */
|
|
10
|
-
value: string | number;
|
|
11
|
-
/** Label/title for the stat */
|
|
12
|
-
label: string;
|
|
13
|
-
/** Optional description text below the label */
|
|
14
|
-
description?: string;
|
|
15
|
-
/** Trend direction: 'up' shows green arrow, 'down' shows red arrow */
|
|
16
|
-
trend?: 'up' | 'down' | 'neutral';
|
|
17
|
-
/** Trend value text (e.g. "+12%", "-3%") */
|
|
18
|
-
trendValue?: string;
|
|
19
|
-
/** Optional icon character or HTML entity to display */
|
|
20
|
-
icon?: string;
|
|
21
|
-
/** Link to detail page */
|
|
22
|
-
href?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let {
|
|
26
|
-
value,
|
|
27
|
-
label,
|
|
28
|
-
description,
|
|
29
|
-
trend,
|
|
30
|
-
trendValue,
|
|
31
|
-
icon,
|
|
32
|
-
href,
|
|
33
|
-
}: Props = $props();
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
{#if href}
|
|
37
|
-
<a href={href} class="lite-stats-card lite-card" style="text-decoration:none;display:block;">
|
|
38
|
-
{#if icon}
|
|
39
|
-
<div class="lite-stats-icon">{icon}</div>
|
|
40
|
-
{/if}
|
|
41
|
-
<div class="lite-stats-value">{value}</div>
|
|
42
|
-
<div class="lite-stats-label">{label}</div>
|
|
43
|
-
{#if trendValue}
|
|
44
|
-
<div class="lite-stats-trend lite-stats-trend-{trend || 'neutral'}">
|
|
45
|
-
{#if trend === 'up'}▲{:else if trend === 'down'}▼{:else}▬{/if}
|
|
46
|
-
{trendValue}
|
|
47
|
-
</div>
|
|
48
|
-
{/if}
|
|
49
|
-
{#if description}
|
|
50
|
-
<div class="lite-stats-desc">{description}</div>
|
|
51
|
-
{/if}
|
|
52
|
-
</a>
|
|
53
|
-
{:else}
|
|
54
|
-
<div class="lite-stats-card lite-card">
|
|
55
|
-
{#if icon}
|
|
56
|
-
<div class="lite-stats-icon">{icon}</div>
|
|
57
|
-
{/if}
|
|
58
|
-
<div class="lite-stats-value">{value}</div>
|
|
59
|
-
<div class="lite-stats-label">{label}</div>
|
|
60
|
-
{#if trendValue}
|
|
61
|
-
<div class="lite-stats-trend lite-stats-trend-{trend || 'neutral'}">
|
|
62
|
-
{#if trend === 'up'}▲{:else if trend === 'down'}▼{:else}▬{/if}
|
|
63
|
-
{trendValue}
|
|
64
|
-
</div>
|
|
65
|
-
{/if}
|
|
66
|
-
{#if description}
|
|
67
|
-
<div class="lite-stats-desc">{description}</div>
|
|
68
|
-
{/if}
|
|
69
|
-
</div>
|
|
70
|
-
{/if}
|