@svadmin/lite 0.3.15 → 0.3.18
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 +53 -9
- package/dist/components/LiteConfirmDialog.svelte +29 -19
- package/dist/components/LiteConfirmDialog.svelte.d.ts +1 -5
- package/dist/components/LiteLayout.svelte +21 -55
- package/dist/components/LiteTable.svelte +18 -7
- package/dist/components/advanced/LiteAutoSaveIndicator.svelte +1 -1
- package/dist/components/advanced/LiteInlineEdit.svelte +1 -1
- package/dist/components/buttons/LiteDeleteButton.svelte +18 -13
- package/dist/components/buttons/LiteImportButton.svelte +19 -14
- package/dist/components/fields/LiteFileField.svelte +1 -1
- package/dist/components/fields/LiteImageField.svelte +2 -2
- package/dist/components/fields/LiteMultiSelectField.svelte +1 -1
- package/dist/components/fields/LiteRichTextField.svelte +1 -1
- package/dist/components/fields/LiteTagField.svelte +1 -1
- package/dist/components/layout/LiteHeader.svelte +1 -1
- package/dist/components/layout/LiteMenuList.svelte +44 -0
- package/dist/components/layout/LiteMenuList.svelte.d.ts +11 -0
- package/dist/components/layout/LiteSidebar.svelte +2 -45
- package/dist/components/pages/LiteForgotPasswordPage.svelte +1 -2
- package/dist/components/pages/LiteProfilePage.svelte +1 -1
- package/dist/components/pages/LiteRegisterPage.svelte +1 -2
- package/dist/components/pages/LiteShowPage.svelte +1 -1
- package/dist/components/pages/LiteUpdatePasswordPage.svelte +1 -2
- package/dist/components/widgets/LiteBarChart.svelte +1 -1
- package/dist/components/widgets/LiteInsightCard.svelte +1 -1
- package/dist/components/widgets/LitePieChart.svelte +2 -2
- package/dist/enhance.js +17 -8
- package/dist/fragment-id.d.ts +1 -0
- package/dist/fragment-id.js +11 -0
- package/dist/lite.css +81 -33
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @svadmin/lite
|
|
2
2
|
|
|
3
|
-
**Lightweight, SSR-compatible admin UI for [@svadmin](https://github.com/
|
|
3
|
+
**Lightweight, SSR-compatible admin UI for [@svadmin](https://github.com/vibeunion/svadmin).**
|
|
4
4
|
|
|
5
5
|
Core server-driven CRUD flows can run without hydration. An optional `enhance.js` asset adds progressive client-side conveniences.
|
|
6
6
|
|
|
@@ -19,14 +19,14 @@ The main `@svadmin/ui` package delivers a premium SPA experience using Svelte 5,
|
|
|
19
19
|
| **List page** | Server-rendered `<table>` with `<a>` sort links |
|
|
20
20
|
| **Detail page** | Key-value layout with type-aware formatting |
|
|
21
21
|
| **Create/Edit** | Native `<form method="POST">` with server-side validation |
|
|
22
|
-
| **Delete** |
|
|
22
|
+
| **Delete** | Fragment-target confirmation + native POST form, no JS needed |
|
|
23
23
|
| **Search** | `<form method="GET">` with `?q=` parameter |
|
|
24
24
|
| **Pagination** | Pure `<a>` page links |
|
|
25
25
|
| **Authentication pages** | Login/logout plus optional provider-delegating account actions |
|
|
26
26
|
| **Auth Guard** | Server hook redirects unauthenticated users |
|
|
27
27
|
| **UA Detection** | Optional legacy-browser detection hook redirects users to `/lite/` routes |
|
|
28
28
|
| **i18n** | Uses `@svadmin/core` `t()` translations |
|
|
29
|
-
| **Multi-level Menu** |
|
|
29
|
+
| **Multi-level Menu** | Always-expanded, config-driven nested links via `MenuItem[]` |
|
|
30
30
|
| **Print** | `@media print` optimized styles |
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
@@ -54,10 +54,16 @@ export const actions = createCrudActions(dataProvider, postsResource);
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```typescript
|
|
57
|
-
// src/routes/lite
|
|
57
|
+
// src/routes/lite/+layout.ts
|
|
58
|
+
export const ssr = true;
|
|
58
59
|
export const csr = false;
|
|
59
60
|
```
|
|
60
61
|
|
|
62
|
+
Put these options on the `/lite` layout so every child route returns server-rendered
|
|
63
|
+
HTML without shipping or executing the Svelte runtime in IE11. See the runnable
|
|
64
|
+
[SSR example](https://github.com/vibeunion/svadmin/tree/main/packages/lite/example)
|
|
65
|
+
for the complete contract and its real-response check.
|
|
66
|
+
|
|
61
67
|
```svelte
|
|
62
68
|
<!-- src/routes/lite/posts/+page.svelte -->
|
|
63
69
|
<script lang="ts">
|
|
@@ -130,7 +136,44 @@ Pass a `menu` prop to `LiteLayout` to replace the auto-generated flat resource l
|
|
|
130
136
|
|
|
131
137
|
`meta.hidden` is honored recursively. For `meta.resource` / `meta.action`, pass a
|
|
132
138
|
server-computed synchronous `canAccess(resource, action)` callback; permission checks
|
|
133
|
-
that fail or throw are hidden.
|
|
139
|
+
that fail or throw are hidden. Nested groups stay expanded so their links remain usable
|
|
140
|
+
without JavaScript or native disclosure-element support. API and server actions must
|
|
141
|
+
still enforce authorization.
|
|
142
|
+
|
|
143
|
+
### 2c. Request-scoped providers and tenants
|
|
144
|
+
|
|
145
|
+
Lite's server utilities consume the same `DataProvider` contract as Core, but they do
|
|
146
|
+
not use client hooks. Build tenant-aware providers inside each SvelteKit request so SSR
|
|
147
|
+
requests never share tenant state through a module-level mutable variable:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// src/routes/lite/posts/+page.server.ts
|
|
151
|
+
import { withTenantDataProvider } from '@svadmin/core';
|
|
152
|
+
import { createListLoader, createCrudActions } from '@svadmin/lite';
|
|
153
|
+
import { dataProvider, postsResource } from '$lib/admin';
|
|
154
|
+
import type { Actions, PageServerLoad } from './$types';
|
|
155
|
+
|
|
156
|
+
function scopedProvider(tenantId: string) {
|
|
157
|
+
return withTenantDataProvider(dataProvider, { tenantId });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const load = ((event) =>
|
|
161
|
+
createListLoader(scopedProvider(event.locals.tenantId), postsResource)(event)
|
|
162
|
+
) satisfies PageServerLoad;
|
|
163
|
+
|
|
164
|
+
function scopedActions(tenantId: string) {
|
|
165
|
+
return createCrudActions(scopedProvider(tenantId), postsResource);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const actions = {
|
|
169
|
+
create: (event) => scopedActions(event.locals.tenantId).create(event),
|
|
170
|
+
update: (event) => scopedActions(event.locals.tenantId).update(event),
|
|
171
|
+
delete: (event) => scopedActions(event.locals.tenantId).delete(event),
|
|
172
|
+
} satisfies Actions;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Authorization and tenant isolation must still be enforced by the provider/backend; UI
|
|
176
|
+
visibility is not an authorization boundary.
|
|
134
177
|
|
|
135
178
|
### 3. Optionally route legacy browsers to Lite pages
|
|
136
179
|
|
|
@@ -180,7 +223,7 @@ export const handle = createLegacyRedirectHook('/lite');
|
|
|
180
223
|
## CSS
|
|
181
224
|
|
|
182
225
|
Import `@svadmin/lite/lite.css` in your layout. It's fully self-contained:
|
|
183
|
-
- IE11-oriented CSS baseline (standard flexbox, no CSS variables)
|
|
226
|
+
- IE11-oriented CSS baseline (standard flexbox, no CSS variables, Grid, or `gap`)
|
|
184
227
|
- Custom-styled checkboxes, radios, and selects (no `appearance: none` needed)
|
|
185
228
|
- Indigo/Slate color system aligned with `@svadmin/ui`
|
|
186
229
|
- Modern focus rings (`box-shadow` based)
|
|
@@ -194,7 +237,7 @@ This CSS baseline helps older browsers, but the generated Svelte/SvelteKit appli
|
|
|
194
237
|
## Optional: Progressive Enhancement
|
|
195
238
|
|
|
196
239
|
Copy the exported `@svadmin/lite/enhance.js` asset to your application's static folder. This ES5 script adds:
|
|
197
|
-
- Auto-close
|
|
240
|
+
- Auto-close fragment-target confirmations when clicking outside
|
|
198
241
|
- Auto-focus first form input
|
|
199
242
|
- Unsaved changes warning
|
|
200
243
|
|
|
@@ -215,8 +258,9 @@ The asset is optional for core server-rendered navigation and form submission; t
|
|
|
215
258
|
- Buttons such as import/export/chat provide SSR UI and route contracts only. Consumers
|
|
216
259
|
must implement the corresponding loader or form action for their backend.
|
|
217
260
|
- IE11 can consume server-rendered HTML and the optional ES5 enhancement layer, with
|
|
218
|
-
|
|
219
|
-
|
|
261
|
+
native links/forms and the IE11-safe Lite CSS contract. Keep the entire Lite route
|
|
262
|
+
subtree on `ssr = true` and `csr = false`; Svelte 5 hydration/runtime execution in
|
|
263
|
+
IE11 is not supported or promised by this package.
|
|
220
264
|
|
|
221
265
|
## License
|
|
222
266
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
* LiteConfirmDialog — SSR-compatible confirmation dialog.
|
|
4
|
-
* Utilizes pure HTML <details> and <summary> to show a popup.
|
|
5
|
-
* No client-side JS required.
|
|
6
|
-
*/
|
|
2
|
+
import { liteFragmentId } from '../fragment-id';
|
|
7
3
|
|
|
8
4
|
interface Props {
|
|
5
|
+
confirmationId?: string;
|
|
9
6
|
title?: string;
|
|
10
7
|
description?: string;
|
|
11
8
|
confirmLabel?: string;
|
|
@@ -21,6 +18,7 @@
|
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
let {
|
|
21
|
+
confirmationId,
|
|
24
22
|
title = 'Are you sure?',
|
|
25
23
|
description = 'This action cannot be undone.',
|
|
26
24
|
confirmLabel = 'Confirm',
|
|
@@ -31,30 +29,42 @@
|
|
|
31
29
|
hiddenInputs = {},
|
|
32
30
|
align = 'right'
|
|
33
31
|
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
const componentId = $props.id();
|
|
34
|
+
const fragmentId = $derived(liteFragmentId(
|
|
35
|
+
'confirm',
|
|
36
|
+
confirmationId ?? componentId,
|
|
37
|
+
));
|
|
38
|
+
const titleId = $derived(`${fragmentId}-title`);
|
|
39
|
+
const descriptionId = $derived(`${fragmentId}-description`);
|
|
34
40
|
</script>
|
|
35
41
|
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
42
|
+
<div class="lite-confirm">
|
|
43
|
+
<span id={`${fragmentId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
44
|
+
<a href={`#${fragmentId}`} class={triggerClass} aria-controls={fragmentId} aria-haspopup="dialog">{triggerLabel}</a>
|
|
45
|
+
<div
|
|
46
|
+
id={fragmentId}
|
|
47
|
+
class="lite-confirm-panel lite-confirm-target"
|
|
48
|
+
style="text-align:left; {align === 'right' ? 'right:0;' : 'left:0;'}"
|
|
49
|
+
role="dialog"
|
|
50
|
+
aria-labelledby={titleId}
|
|
51
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
52
|
+
tabindex="-1"
|
|
53
|
+
>
|
|
54
|
+
<div id={titleId} style="font-weight:600;margin-bottom:8px;color:#0f172a;">{title}</div>
|
|
40
55
|
{#if description}
|
|
41
|
-
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">{description}</div>
|
|
56
|
+
<div id={descriptionId} style="font-size:12px;color:#64748b;margin-bottom:16px;">{description}</div>
|
|
42
57
|
{/if}
|
|
43
|
-
<form method="POST" {action}
|
|
58
|
+
<form method="POST" {action} class="lite-inline-actions lite-justify-end">
|
|
44
59
|
{#each Object.entries(hiddenInputs) as [key, val] (key)}
|
|
45
60
|
<input type="hidden" name={key} value={val} />
|
|
46
61
|
{/each}
|
|
47
|
-
<
|
|
48
|
-
type="button"
|
|
49
|
-
class="lite-btn lite-btn-sm"
|
|
50
|
-
style="margin-right:8px;"
|
|
51
|
-
onclick={(e) => (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open')}
|
|
52
|
-
>
|
|
62
|
+
<a href={`#${fragmentId}-closed`} class="lite-btn lite-btn-sm">
|
|
53
63
|
{cancelLabel}
|
|
54
|
-
</
|
|
64
|
+
</a>
|
|
55
65
|
<button type="submit" class="lite-btn lite-btn-primary lite-btn-sm lite-btn-danger">
|
|
56
66
|
{confirmLabel}
|
|
57
67
|
</button>
|
|
58
68
|
</form>
|
|
59
69
|
</div>
|
|
60
|
-
</
|
|
70
|
+
</div>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LiteConfirmDialog — SSR-compatible confirmation dialog.
|
|
3
|
-
* Utilizes pure HTML <details> and <summary> to show a popup.
|
|
4
|
-
* No client-side JS required.
|
|
5
|
-
*/
|
|
6
1
|
interface Props {
|
|
2
|
+
confirmationId?: string;
|
|
7
3
|
title?: string;
|
|
8
4
|
description?: string;
|
|
9
5
|
confirmLabel?: string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ResourceDefinition, MenuItem } from '@svadmin/core';
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
import { filterVisibleMenu, filterVisibleResources } from '../menu-visibility';
|
|
9
|
+
import LiteMenuList from './layout/LiteMenuList.svelte';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
resources: ResourceDefinition[];
|
|
@@ -67,41 +68,7 @@
|
|
|
67
68
|
<nav class="lite-sidebar">
|
|
68
69
|
<div class="lite-sidebar-brand">{brandName}</div>
|
|
69
70
|
{#if hasCustomMenu}
|
|
70
|
-
{
|
|
71
|
-
{#if item.children && item.children.length > 0}
|
|
72
|
-
<details class="lite-menu-group">
|
|
73
|
-
<summary class="lite-menu-parent">{item.label ?? item.name}</summary>
|
|
74
|
-
{#each item.children as child, _i (_i)}
|
|
75
|
-
{#if child.children && child.children.length > 0}
|
|
76
|
-
<details class="lite-menu-group" style="margin-left:12px">
|
|
77
|
-
<summary class="lite-menu-parent">{child.label ?? child.name}</summary>
|
|
78
|
-
{#each child.children as grandchild, _i (_i)}
|
|
79
|
-
<a
|
|
80
|
-
href={grandchild.href ?? `${basePath}/${grandchild.name}`}
|
|
81
|
-
class={isActive(grandchild.href ?? `${basePath}/${grandchild.name}`) ? 'active' : ''}
|
|
82
|
-
target={grandchild.target === '_blank' ? '_blank' : undefined}
|
|
83
|
-
style="padding-left:40px"
|
|
84
|
-
>{grandchild.label ?? grandchild.name}</a>
|
|
85
|
-
{/each}
|
|
86
|
-
</details>
|
|
87
|
-
{:else}
|
|
88
|
-
<a
|
|
89
|
-
href={child.href ?? `${basePath}/${child.name}`}
|
|
90
|
-
class={isActive(child.href ?? `${basePath}/${child.name}`) ? 'active' : ''}
|
|
91
|
-
target={child.target === '_blank' ? '_blank' : undefined}
|
|
92
|
-
style="padding-left:28px"
|
|
93
|
-
>{child.label ?? child.name}</a>
|
|
94
|
-
{/if}
|
|
95
|
-
{/each}
|
|
96
|
-
</details>
|
|
97
|
-
{:else}
|
|
98
|
-
<a
|
|
99
|
-
href={item.href ?? `${basePath}/${item.name}`}
|
|
100
|
-
class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
|
|
101
|
-
target={item.target === '_blank' ? '_blank' : undefined}
|
|
102
|
-
>{item.label ?? item.name}</a>
|
|
103
|
-
{/if}
|
|
104
|
-
{/each}
|
|
71
|
+
<LiteMenuList items={visibleMenu} {basePath} {currentResource} />
|
|
105
72
|
{:else}
|
|
106
73
|
{#each menuResources as res, _i (_i)}
|
|
107
74
|
<a
|
|
@@ -123,26 +90,25 @@
|
|
|
123
90
|
</nav>
|
|
124
91
|
|
|
125
92
|
<nav class="lite-mobile-nav" aria-label="Mobile navigation">
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</details>
|
|
93
|
+
<div class="lite-mobile-nav-brand">{brandName}</div>
|
|
94
|
+
<div class="lite-mobile-nav-links">
|
|
95
|
+
{#each mobileItems as item (item.name)}
|
|
96
|
+
<a
|
|
97
|
+
href={item.href ?? `${basePath}/${item.name}`}
|
|
98
|
+
class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
|
|
99
|
+
target={item.target === '_blank' ? '_blank' : undefined}
|
|
100
|
+
rel={item.target === '_blank' ? 'noreferrer' : undefined}
|
|
101
|
+
>{item.label ?? item.name}</a>
|
|
102
|
+
{/each}
|
|
103
|
+
{#if userName}
|
|
104
|
+
<div class="lite-mobile-user">
|
|
105
|
+
<span>{userName}</span>
|
|
106
|
+
<form method="POST" action={`${basePath}/login?/logout`}>
|
|
107
|
+
<button type="submit" class="lite-btn lite-btn-sm">Logout</button>
|
|
108
|
+
</form>
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
</div>
|
|
146
112
|
</nav>
|
|
147
113
|
|
|
148
114
|
<!-- Main Content -->
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ResourceDefinition, FieldDefinition } from '@svadmin/core';
|
|
7
7
|
import { t } from '@svadmin/core/i18n';
|
|
8
8
|
import { isExplicitBooleanTrue } from '../value-normalization';
|
|
9
|
+
import { liteFragmentId } from '../fragment-id';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
records: Record<string, unknown>[];
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
canDelete,
|
|
33
34
|
}: Props = $props();
|
|
34
35
|
|
|
36
|
+
const tableId = $props.id();
|
|
37
|
+
|
|
35
38
|
let pk = $derived(resource.primaryKey ?? 'id');
|
|
36
39
|
const showView = $derived(canShow ?? resource.canShow !== false);
|
|
37
40
|
const showEdit = $derived(canEdit ?? resource.canEdit !== false);
|
|
@@ -116,17 +119,25 @@
|
|
|
116
119
|
<a href={`${basePath}/${resource.name}/edit/${id}`} class="lite-btn lite-btn-sm">{t('common.edit') || 'Edit'}</a>
|
|
117
120
|
{/if}
|
|
118
121
|
{#if showDelete}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
{@const confirmationId = liteFragmentId('delete', tableId, resource.name, String(id))}
|
|
123
|
+
{@const confirmationTitleId = `${confirmationId}-title`}
|
|
124
|
+
<div class="lite-confirm">
|
|
125
|
+
<span id={`${confirmationId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
126
|
+
<a
|
|
127
|
+
href={`#${confirmationId}`}
|
|
128
|
+
class="lite-btn lite-btn-sm lite-btn-danger"
|
|
129
|
+
aria-controls={confirmationId}
|
|
130
|
+
aria-haspopup="dialog"
|
|
131
|
+
>{t('common.delete') || 'Delete'}</a>
|
|
132
|
+
<div id={confirmationId} class="lite-confirm-panel lite-confirm-target" role="dialog" aria-labelledby={confirmationTitleId} tabindex="-1">
|
|
133
|
+
<p id={confirmationTitleId} style="margin:0 0 8px;font-size:13px;">{t('common.areYouSure') || 'Are you sure?'}</p>
|
|
134
|
+
<form method="POST" action="?/delete" class="lite-inline-actions">
|
|
125
135
|
<input type="hidden" name="id" value={String(id)} />
|
|
136
|
+
<a href={`#${confirmationId}-closed`} class="lite-btn lite-btn-sm">{t('common.cancel') || 'Cancel'}</a>
|
|
126
137
|
<button type="submit" class="lite-btn lite-btn-sm lite-btn-danger">{t('common.confirm') || 'Confirm'}</button>
|
|
127
138
|
</form>
|
|
128
139
|
</div>
|
|
129
|
-
</
|
|
140
|
+
</div>
|
|
130
141
|
{/if}
|
|
131
142
|
</td>
|
|
132
143
|
{/if}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const cfg = $derived(statusConfig[status] ?? statusConfig.idle);
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
|
-
<span style="display: inline-flex; align-items: center;
|
|
23
|
+
<span class="lite-inline-sm" style="display: inline-flex; align-items: center; font-size: 13px; color: {cfg.color};">
|
|
24
24
|
<span>{cfg.icon}</span>
|
|
25
25
|
<span>{message ?? cfg.text}</span>
|
|
26
26
|
</span>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
3
|
import { Trash2 } from '@lucide/svelte';
|
|
4
|
+
import { liteFragmentId } from '../../fragment-id';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
resource: string;
|
|
@@ -21,37 +22,41 @@
|
|
|
21
22
|
size = 'default',
|
|
22
23
|
redirectUrl = ''
|
|
23
24
|
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const componentId = $props.id();
|
|
27
|
+
const confirmationId = $derived(liteFragmentId('delete', componentId, resource, recordItemId));
|
|
28
|
+
const confirmationTitleId = $derived(`${confirmationId}-title`);
|
|
24
29
|
</script>
|
|
25
30
|
|
|
26
|
-
<
|
|
27
|
-
<
|
|
31
|
+
<div class="lite-confirm {className}">
|
|
32
|
+
<span id={`${confirmationId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
33
|
+
<a
|
|
34
|
+
href={`#${confirmationId}`}
|
|
28
35
|
class="lite-btn lite-btn-danger {size === 'sm' ? 'lite-btn-sm' : ''}"
|
|
29
36
|
title={t('common.delete') || 'Delete'}
|
|
37
|
+
aria-controls={confirmationId}
|
|
38
|
+
aria-haspopup="dialog"
|
|
30
39
|
>
|
|
31
40
|
<Trash2 size={16} />
|
|
32
41
|
{#if !hideText}
|
|
33
42
|
<span style="margin-left: 4px">{t('common.delete') || 'Delete'}</span>
|
|
34
43
|
{/if}
|
|
35
|
-
</
|
|
36
|
-
<div class="lite-confirm-panel" style="right: 0; left: auto;">
|
|
37
|
-
<p style="margin: 0 0 8px; font-size: 13px; color: #111827;">
|
|
44
|
+
</a>
|
|
45
|
+
<div id={confirmationId} class="lite-confirm-panel lite-confirm-target" style="right: 0; left: auto;" role="dialog" aria-labelledby={confirmationTitleId} tabindex="-1">
|
|
46
|
+
<p id={confirmationTitleId} style="margin: 0 0 8px; font-size: 13px; color: #111827;">
|
|
38
47
|
{t('common.areYouSure') || 'Are you sure?'}
|
|
39
48
|
</p>
|
|
40
|
-
<form method="POST" action={`${basePath}/${resource}?/delete`}
|
|
49
|
+
<form method="POST" action={`${basePath}/${resource}?/delete`} class="lite-inline-actions">
|
|
41
50
|
<input type="hidden" name="id" value={String(recordItemId)} />
|
|
42
51
|
{#if redirectUrl}
|
|
43
52
|
<input type="hidden" name="redirect" value={redirectUrl} />
|
|
44
53
|
{/if}
|
|
45
|
-
<
|
|
46
|
-
type="button"
|
|
47
|
-
class="lite-btn lite-btn-sm"
|
|
48
|
-
onclick={(e) => (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open')}
|
|
49
|
-
>
|
|
54
|
+
<a href={`#${confirmationId}-closed`} class="lite-btn lite-btn-sm">
|
|
50
55
|
{t('common.cancel') || 'Cancel'}
|
|
51
|
-
</
|
|
56
|
+
</a>
|
|
52
57
|
<button type="submit" class="lite-btn lite-btn-sm lite-btn-danger">
|
|
53
58
|
{t('common.confirm') || 'Confirm'}
|
|
54
59
|
</button>
|
|
55
60
|
</form>
|
|
56
61
|
</div>
|
|
57
|
-
</
|
|
62
|
+
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
3
|
import { Upload } from '@lucide/svelte';
|
|
4
|
+
import { liteFragmentId } from '../../fragment-id';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
resource: string;
|
|
@@ -17,34 +18,38 @@
|
|
|
17
18
|
class: className = '',
|
|
18
19
|
size = 'default'
|
|
19
20
|
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
const componentId = $props.id();
|
|
23
|
+
const importPanelId = $derived(liteFragmentId('import', componentId, resource));
|
|
24
|
+
const importPanelTitleId = $derived(`${importPanelId}-title`);
|
|
20
25
|
</script>
|
|
21
26
|
|
|
22
|
-
<
|
|
23
|
-
<
|
|
27
|
+
<div class="lite-confirm {className}">
|
|
28
|
+
<span id={`${importPanelId}-closed`} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
29
|
+
<a
|
|
30
|
+
href={`#${importPanelId}`}
|
|
24
31
|
class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''}"
|
|
25
32
|
title={t('common.import') || 'Import'}
|
|
33
|
+
aria-controls={importPanelId}
|
|
34
|
+
aria-haspopup="dialog"
|
|
26
35
|
>
|
|
27
36
|
<Upload size={16} />
|
|
28
37
|
{#if !hideText}
|
|
29
38
|
<span style="margin-left: 4px">{t('common.import') || 'Import'}</span>
|
|
30
39
|
{/if}
|
|
31
|
-
</
|
|
32
|
-
<div class="lite-confirm-panel">
|
|
33
|
-
<p style="margin: 0 0 8px; font-size: 13px;">{t('common.importData') || 'Import data (CSV/JSON)'}</p>
|
|
34
|
-
<form method="POST" action={`${basePath}/${resource}?/${resource}_import`} enctype="multipart/form-data"
|
|
40
|
+
</a>
|
|
41
|
+
<div id={importPanelId} class="lite-confirm-panel lite-confirm-target" role="dialog" aria-labelledby={importPanelTitleId} tabindex="-1">
|
|
42
|
+
<p id={importPanelTitleId} style="margin: 0 0 8px; font-size: 13px;">{t('common.importData') || 'Import data (CSV/JSON)'}</p>
|
|
43
|
+
<form method="POST" action={`${basePath}/${resource}?/${resource}_import`} enctype="multipart/form-data" class="lite-stack-sm">
|
|
35
44
|
<input type="file" name="file" accept=".csv,.json" required style="font-size: 13px;" />
|
|
36
|
-
<div
|
|
37
|
-
<
|
|
38
|
-
type="button"
|
|
39
|
-
class="lite-btn lite-btn-sm"
|
|
40
|
-
onclick={(e) => (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open')}
|
|
41
|
-
>
|
|
45
|
+
<div class="lite-inline-actions lite-justify-end">
|
|
46
|
+
<a href={`#${importPanelId}-closed`} class="lite-btn lite-btn-sm">
|
|
42
47
|
{t('common.cancel') || 'Cancel'}
|
|
43
|
-
</
|
|
48
|
+
</a>
|
|
44
49
|
<button type="submit" class="lite-btn lite-btn-sm lite-btn-primary">
|
|
45
50
|
{t('common.upload') || 'Upload'}
|
|
46
51
|
</button>
|
|
47
52
|
</div>
|
|
48
53
|
</form>
|
|
49
54
|
</div>
|
|
50
|
-
</
|
|
55
|
+
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
{#if mode === 'show'}
|
|
23
23
|
{@const urls = getUrls(value)}
|
|
24
|
-
<div
|
|
24
|
+
<div class="lite-inline-md lite-flex-wrap">
|
|
25
25
|
{#each urls as url, _i (_i)}
|
|
26
26
|
<img src={url} alt={field.label} style="max-height: 100px; border-radius: 4px; border: 1px solid #e5e7eb;" />
|
|
27
27
|
{:else}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{@const urls = getUrls(value)}
|
|
33
33
|
<div>
|
|
34
34
|
{#if urls.length > 0}
|
|
35
|
-
<div style="margin-bottom: 8px;
|
|
35
|
+
<div class="lite-inline-md lite-flex-wrap" style="margin-bottom: 8px;">
|
|
36
36
|
{#each urls as url, _i (_i)}
|
|
37
37
|
<img src={url} alt="Current" style="height: 60px; border-radius: 4px; border: 1px solid #e5e7eb; opacity: 0.6;" />
|
|
38
38
|
{/each}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
{:else}
|
|
23
23
|
<div class="lite-markdown-editor">
|
|
24
|
-
<div style="padding: 8px 12px; border: 1px solid #cbd5e1; border-bottom: none; border-radius: 6px 6px 0 0; background: #f8fafc; color: #64748b; font-size: 12px;
|
|
24
|
+
<div class="lite-inline-lg lite-flex-wrap" style="padding: 8px 12px; border: 1px solid #cbd5e1; border-bottom: none; border-radius: 6px 6px 0 0; background: #f8fafc; color: #64748b; font-size: 12px; font-weight: 500;">
|
|
25
25
|
<span>Markdown is supported</span>
|
|
26
26
|
<span style="color: #94a3b8;">**bold**</span>
|
|
27
27
|
<span style="color: #94a3b8; font-style: italic;">*italic*</span>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<header class="lite-header" style="padding: 16px 24px; background: white; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
|
|
13
13
|
<h2 style="margin: 0; font-size: 18px; font-weight: 600; color: #0f172a;">{title}</h2>
|
|
14
|
-
<div style="
|
|
14
|
+
<div class="lite-inline-lg" style="align-items: center;">
|
|
15
15
|
{#if children}
|
|
16
16
|
{@render children()}
|
|
17
17
|
{/if}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MenuItem } from '@svadmin/core';
|
|
3
|
+
import LiteMenuList from './LiteMenuList.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: MenuItem[];
|
|
7
|
+
basePath: string;
|
|
8
|
+
currentResource: string;
|
|
9
|
+
depth?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { items, basePath, currentResource, depth = 0 }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function itemHref(menuItem: MenuItem): string {
|
|
15
|
+
return menuItem.href ?? `${basePath}/${menuItem.name}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isActive(menuItem: MenuItem): boolean {
|
|
19
|
+
return currentResource === itemHref(menuItem).replace(basePath + '/', '');
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<ul class="lite-menu-list lite-menu-depth-{depth}">
|
|
24
|
+
{#each items as menuItem (menuItem.name)}
|
|
25
|
+
<li>
|
|
26
|
+
{#if menuItem.children && menuItem.children.length > 0}
|
|
27
|
+
<span class="lite-menu-parent">{menuItem.label ?? menuItem.name}</span>
|
|
28
|
+
<LiteMenuList
|
|
29
|
+
items={menuItem.children}
|
|
30
|
+
{basePath}
|
|
31
|
+
{currentResource}
|
|
32
|
+
depth={depth + 1}
|
|
33
|
+
/>
|
|
34
|
+
{:else}
|
|
35
|
+
<a
|
|
36
|
+
href={itemHref(menuItem)}
|
|
37
|
+
class={isActive(menuItem) ? 'active' : ''}
|
|
38
|
+
target={menuItem.target === '_blank' ? '_blank' : undefined}
|
|
39
|
+
rel={menuItem.target === '_blank' ? 'noreferrer' : undefined}
|
|
40
|
+
>{menuItem.label ?? menuItem.name}</a>
|
|
41
|
+
{/if}
|
|
42
|
+
</li>
|
|
43
|
+
{/each}
|
|
44
|
+
</ul>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MenuItem } from '@svadmin/core';
|
|
2
|
+
import LiteMenuList from './LiteMenuList.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
items: MenuItem[];
|
|
5
|
+
basePath: string;
|
|
6
|
+
currentResource: string;
|
|
7
|
+
depth?: number;
|
|
8
|
+
}
|
|
9
|
+
declare const LiteMenuList: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type LiteMenuList = ReturnType<typeof LiteMenuList>;
|
|
11
|
+
export default LiteMenuList;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ResourceDefinition, MenuItem } from '@svadmin/core';
|
|
3
3
|
import { filterVisibleMenu, filterVisibleResources } from '../../menu-visibility';
|
|
4
|
+
import LiteMenuList from './LiteMenuList.svelte';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
resources: ResourceDefinition[];
|
|
@@ -26,56 +27,12 @@
|
|
|
26
27
|
|
|
27
28
|
const hasCustomMenu = $derived(menu !== undefined);
|
|
28
29
|
const visibleMenu = $derived(menu ? filterVisibleMenu(menu, canAccess) : []);
|
|
29
|
-
|
|
30
|
-
function isActive(href: string | undefined): boolean {
|
|
31
|
-
if (!href) return false;
|
|
32
|
-
return currentResource === href.replace(basePath + '/', '');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function hasActiveChild(children?: MenuItem[]): boolean {
|
|
36
|
-
if (!children) return false;
|
|
37
|
-
return children.some(c => isActive(c.href ?? `${basePath}/${c.name}`));
|
|
38
|
-
}
|
|
39
30
|
</script>
|
|
40
31
|
|
|
41
32
|
<nav class="lite-sidebar">
|
|
42
33
|
<div class="lite-sidebar-brand">{brandName}</div>
|
|
43
34
|
{#if hasCustomMenu}
|
|
44
|
-
{
|
|
45
|
-
{#if item.children && item.children.length > 0}
|
|
46
|
-
<details class="lite-menu-group" open={hasActiveChild(item.children)}>
|
|
47
|
-
<summary class="lite-menu-parent">{item.label ?? item.name}</summary>
|
|
48
|
-
{#each item.children as child, _i (_i)}
|
|
49
|
-
{#if child.children && child.children.length > 0}
|
|
50
|
-
<details class="lite-menu-group" style="margin-left:12px" open={hasActiveChild(child.children)}>
|
|
51
|
-
<summary class="lite-menu-parent">{child.label ?? child.name}</summary>
|
|
52
|
-
{#each child.children as grandchild, _i (_i)}
|
|
53
|
-
<a
|
|
54
|
-
href={grandchild.href ?? `${basePath}/${grandchild.name}`}
|
|
55
|
-
class={isActive(grandchild.href ?? `${basePath}/${grandchild.name}`) ? 'active' : ''}
|
|
56
|
-
target={grandchild.target === '_blank' ? '_blank' : undefined}
|
|
57
|
-
style="padding-left:40px"
|
|
58
|
-
>{grandchild.label ?? grandchild.name}</a>
|
|
59
|
-
{/each}
|
|
60
|
-
</details>
|
|
61
|
-
{:else}
|
|
62
|
-
<a
|
|
63
|
-
href={child.href ?? `${basePath}/${child.name}`}
|
|
64
|
-
class={isActive(child.href ?? `${basePath}/${child.name}`) ? 'active' : ''}
|
|
65
|
-
target={child.target === '_blank' ? '_blank' : undefined}
|
|
66
|
-
style="padding-left:28px"
|
|
67
|
-
>{child.label ?? child.name}</a>
|
|
68
|
-
{/if}
|
|
69
|
-
{/each}
|
|
70
|
-
</details>
|
|
71
|
-
{:else}
|
|
72
|
-
<a
|
|
73
|
-
href={item.href ?? `${basePath}/${item.name}`}
|
|
74
|
-
class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
|
|
75
|
-
target={item.target === '_blank' ? '_blank' : undefined}
|
|
76
|
-
>{item.label ?? item.name}</a>
|
|
77
|
-
{/if}
|
|
78
|
-
{/each}
|
|
35
|
+
<LiteMenuList items={visibleMenu} {basePath} {currentResource} />
|
|
79
36
|
{:else}
|
|
80
37
|
{#each menuResources as res, _i (_i)}
|
|
81
38
|
<a
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
<p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
<form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
|
|
28
|
+
<form method="POST" {action} class="lite-stack-lg">
|
|
30
29
|
<div class="lite-form-group">
|
|
31
30
|
<label for="email">Email</label>
|
|
32
31
|
<input type="email" id="email" name="email" class="lite-input" required />
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<div class="lite-card" style="max-width: 600px; margin: 0 auto; padding: 24px;">
|
|
25
|
-
<form method="POST" {action}
|
|
25
|
+
<form method="POST" {action} class="lite-stack-lg">
|
|
26
26
|
<div class="lite-form-group">
|
|
27
27
|
<label for="name">Name</label>
|
|
28
28
|
<input type="text" id="name" name="name" class="lite-input" value={user?.name ?? ''} />
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
<p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
<form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
|
|
27
|
+
<form method="POST" {action} class="lite-stack-lg">
|
|
29
28
|
<div class="lite-form-group">
|
|
30
29
|
<label for="email">Email</label>
|
|
31
30
|
<input type="email" id="email" name="email" class="lite-input" required />
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
49
|
<div class="lite-card" style="padding: 24px;">
|
|
50
|
-
<div
|
|
50
|
+
<div class="lite-stack-lg">
|
|
51
51
|
{#each showFields as field, _i (_i)}
|
|
52
52
|
<div>
|
|
53
53
|
<div style="display: block; font-size: 13px; font-weight: 500; color: #64748b; margin-bottom: 8px;">
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
<p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
<form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
|
|
24
|
+
<form method="POST" {action} class="lite-stack-lg">
|
|
26
25
|
<div class="lite-form-group">
|
|
27
26
|
<label for="password">New Password</label>
|
|
28
27
|
<input type="password" id="password" name="password" class="lite-input" required minlength="6" />
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{title}
|
|
27
27
|
</div>
|
|
28
28
|
{/if}
|
|
29
|
-
<div style="padding: 16px;
|
|
29
|
+
<div class="lite-stack-md" style="padding: 16px;">
|
|
30
30
|
{#each data as point, _i (_i)}
|
|
31
31
|
<div>
|
|
32
32
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px;">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<div class="lite-card" style="margin-bottom: 16px;">
|
|
20
|
-
<div style="padding: 12px 16px; border-bottom: 1px solid #e2e8f0; font-weight: 600; font-size: 14px; color: #0f172a;
|
|
20
|
+
<div class="lite-inline-md" style="padding: 12px 16px; border-bottom: 1px solid #e2e8f0; font-weight: 600; font-size: 14px; color: #0f172a; align-items: center;">
|
|
21
21
|
<span style="color: #f59e0b;">✦</span>
|
|
22
22
|
{title}
|
|
23
23
|
</div>
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
{title}
|
|
28
28
|
</div>
|
|
29
29
|
{/if}
|
|
30
|
-
<div style="padding: 16px;
|
|
30
|
+
<div class="lite-stack-md" style="padding: 16px;">
|
|
31
31
|
{#each data as point, i (i)}
|
|
32
32
|
{@const pct = total > 0 ? ((point.value / total) * 100).toFixed(1) : '0.0'}
|
|
33
|
-
<div style="
|
|
33
|
+
<div class="lite-inline-lg" style="align-items: center; font-size: 13px;">
|
|
34
34
|
<span style="width: 12px; height: 12px; border-radius: 50%; background: {point.color ?? defaultColors[i % defaultColors.length]}; flex-shrink: 0;"></span>
|
|
35
35
|
<span style="flex: 1; color: #334155;">{point.label}</span>
|
|
36
36
|
<span style="color: #64748b; font-weight: 500;">{point.value}</span>
|
package/dist/enhance.js
CHANGED
|
@@ -54,20 +54,29 @@
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function hasClass(element, className) {
|
|
58
|
+
return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function closeActiveConfirmation(clickedNode) {
|
|
62
|
+
var targetId = window.location.hash.slice(1);
|
|
63
|
+
if (!targetId) return;
|
|
64
|
+
|
|
65
|
+
var confirmation = document.getElementById(targetId);
|
|
66
|
+
if (!confirmation || !hasClass(confirmation, 'lite-confirm-target')) return;
|
|
67
|
+
if (clickedNode && confirmation.contains(clickedNode)) return;
|
|
68
|
+
|
|
69
|
+
var closedTarget = document.getElementById(targetId + '-closed');
|
|
70
|
+
if (closedTarget) window.location.hash = closedTarget.id;
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
function init() {
|
|
58
74
|
document.documentElement.className += ' lite-js';
|
|
59
75
|
|
|
60
|
-
// 1. Delete confirmation — close <details> when clicking outside
|
|
61
76
|
document.addEventListener('click', function(e) {
|
|
62
|
-
|
|
63
|
-
for (var i = 0; i < details.length; i++) {
|
|
64
|
-
if (!details[i].contains(e.target)) {
|
|
65
|
-
details[i].removeAttribute('open');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
77
|
+
closeActiveConfirmation(e.target);
|
|
68
78
|
});
|
|
69
79
|
|
|
70
|
-
// 2. Highlight current table row on hover (for older browsers without :hover)
|
|
71
80
|
var rows = document.querySelectorAll('.lite-table tbody tr');
|
|
72
81
|
for (var j = 0; j < rows.length; j++) {
|
|
73
82
|
rows[j].addEventListener('mouseenter', function() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function liteFragmentId(scope: string, ...tokens: Array<string | number>): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function fragmentToken(rawToken) {
|
|
2
|
+
const encodedToken = Array.from(String(rawToken), (character) => {
|
|
3
|
+
if (/^[a-z0-9]$/iu.test(character))
|
|
4
|
+
return character;
|
|
5
|
+
return `_${character.codePointAt(0)?.toString(16)}_`;
|
|
6
|
+
}).join('');
|
|
7
|
+
return `${encodedToken.length}_${encodedToken}`;
|
|
8
|
+
}
|
|
9
|
+
export function liteFragmentId(scope, ...tokens) {
|
|
10
|
+
return `lite-${[scope, ...tokens].map(fragmentToken).join('-')}`;
|
|
11
|
+
}
|
package/dist/lite.css
CHANGED
|
@@ -62,7 +62,7 @@ a:hover {
|
|
|
62
62
|
border-bottom: 1px solid #e2e8f0;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
.lite-mobile-nav
|
|
65
|
+
.lite-mobile-nav-brand {
|
|
66
66
|
cursor: pointer;
|
|
67
67
|
padding: 14px 16px;
|
|
68
68
|
font-weight: 700;
|
|
@@ -137,37 +137,34 @@ a:hover {
|
|
|
137
137
|
background: #4f46e5;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/* ─── Multi-Level Menu
|
|
141
|
-
.lite-menu-
|
|
140
|
+
/* ─── Multi-Level Menu ───────────────────────────────────── */
|
|
141
|
+
.lite-menu-list {
|
|
142
|
+
list-style: none;
|
|
142
143
|
margin: 0;
|
|
143
|
-
|
|
144
|
+
padding: 0;
|
|
144
145
|
}
|
|
145
|
-
.lite-menu-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
.lite-menu-list .lite-menu-list {
|
|
147
|
+
margin-left: 12px;
|
|
148
|
+
}
|
|
149
|
+
.lite-menu-parent {
|
|
150
|
+
display: block;
|
|
148
151
|
padding: 8px 16px;
|
|
149
152
|
color: #64748b;
|
|
150
153
|
font-size: 11px;
|
|
151
154
|
font-weight: 700;
|
|
152
155
|
text-transform: uppercase;
|
|
153
156
|
letter-spacing: 0.06em;
|
|
154
|
-
transition: color 0.15s ease, background 0.15s ease;
|
|
155
|
-
}
|
|
156
|
-
.lite-menu-group summary::-webkit-details-marker {
|
|
157
|
-
display: none;
|
|
158
|
-
}
|
|
159
|
-
.lite-menu-group summary:hover {
|
|
160
|
-
color: #0f172a;
|
|
161
|
-
background: #f8fafc;
|
|
162
|
-
}
|
|
163
|
-
.lite-menu-group[open] > summary {
|
|
164
|
-
color: #0f172a;
|
|
165
157
|
}
|
|
166
|
-
.lite-menu-
|
|
158
|
+
.lite-menu-parent::after {
|
|
167
159
|
content: " \25BE";
|
|
168
160
|
}
|
|
169
|
-
.lite-menu-
|
|
170
|
-
|
|
161
|
+
.lite-menu-depth-1 > li > a,
|
|
162
|
+
.lite-menu-depth-1 > li > .lite-menu-parent {
|
|
163
|
+
padding-left: 28px;
|
|
164
|
+
}
|
|
165
|
+
.lite-menu-depth-2 > li > a,
|
|
166
|
+
.lite-menu-depth-2 > li > .lite-menu-parent {
|
|
167
|
+
padding-left: 40px;
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
.lite-main {
|
|
@@ -669,19 +666,9 @@ textarea.lite-input {
|
|
|
669
666
|
|
|
670
667
|
/* ─── Delete Confirmation ──────────────────────────────────── */
|
|
671
668
|
.lite-confirm {
|
|
672
|
-
display: inline;
|
|
673
|
-
}
|
|
674
|
-
.lite-confirm-details {
|
|
675
669
|
position: relative;
|
|
676
670
|
display: inline-block;
|
|
677
671
|
}
|
|
678
|
-
.lite-confirm-details summary {
|
|
679
|
-
cursor: pointer;
|
|
680
|
-
list-style: none;
|
|
681
|
-
}
|
|
682
|
-
.lite-confirm-details summary::-webkit-details-marker {
|
|
683
|
-
display: none;
|
|
684
|
-
}
|
|
685
672
|
.lite-confirm-panel {
|
|
686
673
|
position: absolute;
|
|
687
674
|
right: 0;
|
|
@@ -697,6 +684,68 @@ textarea.lite-input {
|
|
|
697
684
|
white-space: nowrap;
|
|
698
685
|
margin-top: 4px;
|
|
699
686
|
}
|
|
687
|
+
.lite-confirm-target {
|
|
688
|
+
display: none;
|
|
689
|
+
}
|
|
690
|
+
.lite-confirm-target:target {
|
|
691
|
+
display: block;
|
|
692
|
+
}
|
|
693
|
+
.lite-confirm-cancel-target {
|
|
694
|
+
display: none;
|
|
695
|
+
}
|
|
696
|
+
.lite-inline-actions {
|
|
697
|
+
display: inline-flex;
|
|
698
|
+
margin: 0;
|
|
699
|
+
}
|
|
700
|
+
.lite-inline-actions > * + * {
|
|
701
|
+
margin-left: 8px;
|
|
702
|
+
}
|
|
703
|
+
.lite-stack-sm {
|
|
704
|
+
display: flex;
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
}
|
|
707
|
+
.lite-stack-sm > * + * {
|
|
708
|
+
margin-top: 8px;
|
|
709
|
+
}
|
|
710
|
+
.lite-stack-xs,
|
|
711
|
+
.lite-stack-md,
|
|
712
|
+
.lite-stack-lg {
|
|
713
|
+
display: flex;
|
|
714
|
+
flex-direction: column;
|
|
715
|
+
}
|
|
716
|
+
.lite-stack-xs > * + * {
|
|
717
|
+
margin-top: 4px;
|
|
718
|
+
}
|
|
719
|
+
.lite-stack-md > * + * {
|
|
720
|
+
margin-top: 12px;
|
|
721
|
+
}
|
|
722
|
+
.lite-stack-lg > * + * {
|
|
723
|
+
margin-top: 16px;
|
|
724
|
+
}
|
|
725
|
+
.lite-inline-xs,
|
|
726
|
+
.lite-inline-sm,
|
|
727
|
+
.lite-inline-md,
|
|
728
|
+
.lite-inline-lg {
|
|
729
|
+
display: flex;
|
|
730
|
+
}
|
|
731
|
+
.lite-inline-xs > * + * {
|
|
732
|
+
margin-left: 4px;
|
|
733
|
+
}
|
|
734
|
+
.lite-inline-sm > * + * {
|
|
735
|
+
margin-left: 6px;
|
|
736
|
+
}
|
|
737
|
+
.lite-inline-md > * + * {
|
|
738
|
+
margin-left: 8px;
|
|
739
|
+
}
|
|
740
|
+
.lite-inline-lg > * + * {
|
|
741
|
+
margin-left: 12px;
|
|
742
|
+
}
|
|
743
|
+
.lite-flex-wrap {
|
|
744
|
+
flex-wrap: wrap;
|
|
745
|
+
}
|
|
746
|
+
.lite-justify-end {
|
|
747
|
+
justify-content: flex-end;
|
|
748
|
+
}
|
|
700
749
|
|
|
701
750
|
/* ─── Login Page ───────────────────────────────────────────── */
|
|
702
751
|
.lite-login-wrapper {
|
|
@@ -802,8 +851,7 @@ textarea.lite-input {
|
|
|
802
851
|
.lite-search,
|
|
803
852
|
.lite-pagination,
|
|
804
853
|
.lite-btn,
|
|
805
|
-
.lite-confirm
|
|
806
|
-
.lite-confirm-details {
|
|
854
|
+
.lite-confirm {
|
|
807
855
|
display: none !important;
|
|
808
856
|
}
|
|
809
857
|
.lite-main {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@sveltejs/kit": "^2.70.2"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@lucide/svelte": "^1.
|
|
56
|
+
"@lucide/svelte": "^1.31.0",
|
|
57
57
|
"zod": "^4.4.3"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist && bun ../../scripts/copy-package-assets.ts static/enhance.js dist/enhance.js",
|
|
70
70
|
"test": "bun run test:components && bun run test:server && bun run test:security",
|
|
71
71
|
"test:components": "vitest run --config ./vitest.config.ts",
|
|
72
|
-
"test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts",
|
|
72
|
+
"test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts ./src/fragment-id.test.ts ./src/ie11-ssr-contract.test.ts",
|
|
73
73
|
"test:security": "vitest run --config ./src/security.test.config.ts"
|
|
74
74
|
},
|
|
75
75
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"author": "zuohuadong",
|
|
81
81
|
"repository": {
|
|
82
82
|
"type": "git",
|
|
83
|
-
"url": "https://github.com/
|
|
83
|
+
"url": "https://github.com/vibeunion/svadmin.git",
|
|
84
84
|
"directory": "packages/lite"
|
|
85
85
|
}
|
|
86
86
|
}
|