@webx-ui/module-admin 0.2.4
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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/AdminNav.d.ts +14 -0
- package/dist/AdminShell.d.ts +29 -0
- package/dist/admin.d.ts +58 -0
- package/dist/createAdmin.d.ts +66 -0
- package/dist/http.d.ts +55 -0
- package/dist/i18n.d.ts +58 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +540 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +9 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +70 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webx-ui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @webx-ui/module-admin
|
|
2
|
+
|
|
3
|
+
The frame a WebX UI admin panel runs in: the bootstrap, the shell, the HTTP client and the
|
|
4
|
+
module registry.
|
|
5
|
+
|
|
6
|
+
It is the front-end half of the Composer package [`webx-ui/module-admin`](https://packagist.org/packages/webx-ui/module-admin),
|
|
7
|
+
and the two are paired by the manifest the server publishes — so a section appears in the panel
|
|
8
|
+
when both halves have it, and the menu is what the installation actually has rather than a list
|
|
9
|
+
written twice.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @webx-ui/module-admin @webx-ui/core @webx-ui/tokens vue vue-router
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On the Laravel side, `php artisan webx:panel` writes the entry file below and wires it into
|
|
18
|
+
Vite for you.
|
|
19
|
+
|
|
20
|
+
## The panel
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createAdmin } from '@webx-ui/module-admin'
|
|
24
|
+
import { auth, WxUserMenu } from '@webx-ui/module-auth'
|
|
25
|
+
import { pages } from '@webx-ui/module-pages'
|
|
26
|
+
|
|
27
|
+
createAdmin({
|
|
28
|
+
modules: [pages],
|
|
29
|
+
plugins: [auth()],
|
|
30
|
+
userMenu: WxUserMenu,
|
|
31
|
+
}).mount()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`createAdmin()` mounts into `#webx-app` — the element the server's Blade shell renders — and
|
|
35
|
+
reads the manifest address out of the `webx-manifest` meta tag beside it. Both are overridable
|
|
36
|
+
for a panel served some other way.
|
|
37
|
+
|
|
38
|
+
Mounting does not wait for the server. The manifest needs a signed-in session, so a visit that
|
|
39
|
+
starts at the sign-in screen would otherwise stare at a blank page waiting for a request it is
|
|
40
|
+
bound to lose.
|
|
41
|
+
|
|
42
|
+
## A module
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import type { AdminModule } from '@webx-ui/module-admin'
|
|
46
|
+
|
|
47
|
+
export const pages: AdminModule = {
|
|
48
|
+
id: 'pages', // the same id the server-side module answers to
|
|
49
|
+
path: '/pages',
|
|
50
|
+
routes: [{ path: '/pages', component: () => import('./PagesScreen.vue') }],
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A module the server reports with no front end installed has nowhere to send anybody, so it
|
|
55
|
+
stays out of the menu; one installed but not reported is not there at all.
|
|
56
|
+
|
|
57
|
+
## Language
|
|
58
|
+
|
|
59
|
+
The panel draws itself in the language of whoever is reading it — their choice, stored against
|
|
60
|
+
them on the server, not a setting of the site. A site published only in Ukrainian can still be
|
|
61
|
+
maintained by somebody who wants English menus.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { useI18n, useTranslate } from '@webx-ui/module-admin'
|
|
65
|
+
|
|
66
|
+
const t = useTranslate('webx-admin') // t('shell.retry')
|
|
67
|
+
const i18n = useI18n() // i18n.state.locale, i18n.state.panelLocales
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The words come from the Composer package's `lang` files, fetched as one dictionary at boot and
|
|
71
|
+
merged over the English this package ships in its own code. So a module is translated **once**,
|
|
72
|
+
in the half that also writes the server's validation messages, and a panel with no server
|
|
73
|
+
behind it still has labels.
|
|
74
|
+
|
|
75
|
+
`i18n.state.contentLocales` is the other list: the languages the site publishes content in.
|
|
76
|
+
That is what an editing screen builds its tabs from, and it has nothing to do with the language
|
|
77
|
+
of the interface around them.
|
|
78
|
+
|
|
79
|
+
## Talking to the backend
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { useAdmin, HttpError } from '@webx-ui/module-admin'
|
|
83
|
+
|
|
84
|
+
const { http, can, state } = useAdmin()
|
|
85
|
+
|
|
86
|
+
const body = await http.get<{ data: Page[] }>(`${state.manifest?.apiPath}/pages`)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The client speaks this backend's conventions rather than HTTP in general: a session cookie, a
|
|
90
|
+
CSRF token fetched when needed and refreshed once if the server says it has gone stale, `422`
|
|
91
|
+
arriving as `errors` ready for `WxForm`, `429` carrying `retryAfter`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
try {
|
|
95
|
+
await http.post('/api/cms/pages', page)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof HttpError && error.isValidation) {
|
|
98
|
+
formErrors.value = error.errors
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`can('pages.manage')` answers from the signed-in session — a super administrator passes
|
|
104
|
+
everything, the same rule the server applies.
|
|
105
|
+
|
|
106
|
+
## Exports
|
|
107
|
+
|
|
108
|
+
| Export | What it is |
|
|
109
|
+
| ------------------------- | ---------------------------------------------- |
|
|
110
|
+
| `createAdmin` | Assembles and mounts the panel |
|
|
111
|
+
| `useAdmin` | The context: `http`, `state`, `nav`, `can` |
|
|
112
|
+
| `createHttp`, `HttpError` | The client, usable on its own |
|
|
113
|
+
| `AdminShell`, `AdminNav` | The layout, for a panel that assembles its own |
|
|
114
|
+
|
|
115
|
+
Styles come with `@webx-ui/core`; this package adds a little of its own:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import '@webx-ui/module-admin/style.css'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Licence
|
|
122
|
+
|
|
123
|
+
MIT.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The menu, built from the manifest rather than written out. What the panel offers is what the
|
|
3
|
+
* installation actually has — adding a module on the server and installing its front end is
|
|
4
|
+
* the whole of "adding a section".
|
|
5
|
+
*/
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
collapsed?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
10
|
+
select: () => any;
|
|
11
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
+
onSelect?: (() => any) | undefined;
|
|
13
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare function __VLS_template(): {
|
|
2
|
+
attrs: Partial<{}>;
|
|
3
|
+
slots: {
|
|
4
|
+
brand?(_: {}): any;
|
|
5
|
+
user?(_: {}): any;
|
|
6
|
+
nav?(_: {
|
|
7
|
+
collapsed: boolean;
|
|
8
|
+
}): any;
|
|
9
|
+
nav?(_: {
|
|
10
|
+
collapsed: boolean;
|
|
11
|
+
onSelect: () => void;
|
|
12
|
+
}): any;
|
|
13
|
+
};
|
|
14
|
+
refs: {
|
|
15
|
+
shellEl: HTMLDivElement;
|
|
16
|
+
};
|
|
17
|
+
rootEl: any;
|
|
18
|
+
};
|
|
19
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
20
|
+
declare const __VLS_component: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {
|
|
21
|
+
shellEl: HTMLDivElement;
|
|
22
|
+
}, any>;
|
|
23
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
24
|
+
export default _default;
|
|
25
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
26
|
+
new (): {
|
|
27
|
+
$slots: S;
|
|
28
|
+
};
|
|
29
|
+
};
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { App, ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
import { Http } from './http';
|
|
3
|
+
import { I18n } from './i18n';
|
|
4
|
+
import { AdminModule, AdminStatus, AdminUser, Manifest, NavEntry } from './types';
|
|
5
|
+
export interface AdminContext {
|
|
6
|
+
/** The panel's own backend. */
|
|
7
|
+
readonly http: Http;
|
|
8
|
+
/** Where the panel is served — the router's base. */
|
|
9
|
+
readonly basePath: string;
|
|
10
|
+
/** Where its JSON lives, so a module does not have to be told twice. */
|
|
11
|
+
readonly apiPath: string;
|
|
12
|
+
readonly state: AdminState;
|
|
13
|
+
/** The interface's own words, and the languages it can be shown in. */
|
|
14
|
+
readonly i18n: I18n;
|
|
15
|
+
/** Modules registered on the front end, whether or not the server reports them. */
|
|
16
|
+
readonly modules: readonly AdminModule[];
|
|
17
|
+
/** Navigation, in the order the server gave, for the modules that exist on both sides. */
|
|
18
|
+
readonly nav: ComputedRef<NavEntry[]>;
|
|
19
|
+
/** Ask the server what the panel is and who is signed in again. */
|
|
20
|
+
reload(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Draw the panel in another language: fetches that dictionary and remembers the choice for
|
|
23
|
+
* the next visit. Storing it against the administrator is an auth module's business — this
|
|
24
|
+
* only changes what is on screen.
|
|
25
|
+
*/
|
|
26
|
+
setLocale(code: string): Promise<void>;
|
|
27
|
+
/** Filled in by an auth module; `null` means nobody is signed in. */
|
|
28
|
+
setUser(user: AdminUser | null): void;
|
|
29
|
+
/**
|
|
30
|
+
* How the panel finds out who is signed in, set by an auth module. Without one the panel
|
|
31
|
+
* simply asks for the manifest and lets a 401 answer the question.
|
|
32
|
+
*/
|
|
33
|
+
useSessionLoader(loader: () => Promise<AdminUser | null>): void;
|
|
34
|
+
can(permission: string): boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface AdminState {
|
|
37
|
+
status: AdminStatus;
|
|
38
|
+
manifest: Manifest | null;
|
|
39
|
+
user: AdminUser | null;
|
|
40
|
+
error: string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare const adminKey: InjectionKey<AdminContext>;
|
|
43
|
+
export declare function useAdmin(): AdminContext;
|
|
44
|
+
/**
|
|
45
|
+
* Permissions are flattened by the server, so a check is a lookup. A super administrator
|
|
46
|
+
* carries no permissions and passes everything — the same rule as on the server, in the one
|
|
47
|
+
* place the front end asks the question.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createAdminContext(options: {
|
|
50
|
+
http: Http;
|
|
51
|
+
basePath: string;
|
|
52
|
+
apiPath: string;
|
|
53
|
+
modules: AdminModule[];
|
|
54
|
+
i18n: I18n;
|
|
55
|
+
loadManifest: () => Promise<Manifest>;
|
|
56
|
+
loadDictionary?: (locale: string) => Promise<void>;
|
|
57
|
+
}): AdminContext;
|
|
58
|
+
export declare function provideAdmin(app: App, admin: AdminContext): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { App, Component } from 'vue';
|
|
2
|
+
import { Router, RouteRecordRaw } from 'vue-router';
|
|
3
|
+
import { AdminContext } from './admin';
|
|
4
|
+
import { Http } from './http';
|
|
5
|
+
import { I18n } from './i18n';
|
|
6
|
+
import { AdminModule } from './types';
|
|
7
|
+
export interface CreateAdminOptions {
|
|
8
|
+
/** Where to mount. Defaults to `#webx-app`, which is what the Blade shell renders. */
|
|
9
|
+
el?: string | Element;
|
|
10
|
+
/**
|
|
11
|
+
* Where the manifest lives. Defaults to the `webx-manifest` meta tag the Blade shell writes,
|
|
12
|
+
* and to `/api/cms/manifest` when there is none — which is the case under a dev server,
|
|
13
|
+
* where the page is Vite's own index.html.
|
|
14
|
+
*/
|
|
15
|
+
manifestUrl?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Where the panel's JSON lives, e.g. `/api/cms`. Modules build their own addresses from it.
|
|
18
|
+
* Derived from the manifest URL when not given.
|
|
19
|
+
*/
|
|
20
|
+
apiPath?: string;
|
|
21
|
+
/** Sections of the panel. */
|
|
22
|
+
modules?: AdminModule[];
|
|
23
|
+
/** Routes that belong to no module: a dashboard, a 404. */
|
|
24
|
+
routes?: RouteRecordRaw[];
|
|
25
|
+
/**
|
|
26
|
+
* Where the panel is served, for the router's history base. Taken from the manifest when it
|
|
27
|
+
* arrives; given here for the first paint, before it has.
|
|
28
|
+
*/
|
|
29
|
+
basePath?: string;
|
|
30
|
+
/** Replaces the panel's name in the header — a logo, usually. */
|
|
31
|
+
brand?: Component;
|
|
32
|
+
/** The corner of the header: who is signed in, and the way out. */
|
|
33
|
+
userMenu?: Component;
|
|
34
|
+
/** Extensions that need the router and the context: an auth module, most of all. */
|
|
35
|
+
plugins?: AdminPlugin[];
|
|
36
|
+
/**
|
|
37
|
+
* The language to draw the panel in before the server has been asked. Defaults to the last
|
|
38
|
+
* one used, then to the page's `lang`, then to the browser's. Whatever is chosen, the server
|
|
39
|
+
* narrows it to a language the panel actually has.
|
|
40
|
+
*/
|
|
41
|
+
locale?: string;
|
|
42
|
+
/** Swappable for tests. */
|
|
43
|
+
http?: Http;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Something that needs the assembled panel rather than a slot in it — it adds routes, guards
|
|
47
|
+
* the router, or tells the panel how to find out who is signed in.
|
|
48
|
+
*/
|
|
49
|
+
export interface AdminPlugin {
|
|
50
|
+
install(admin: Admin): void;
|
|
51
|
+
}
|
|
52
|
+
export interface Admin {
|
|
53
|
+
app: App;
|
|
54
|
+
router: Router;
|
|
55
|
+
context: AdminContext;
|
|
56
|
+
i18n: I18n;
|
|
57
|
+
mount(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Assemble the panel.
|
|
61
|
+
*
|
|
62
|
+
* Mounting does not wait for the server. The manifest needs a signed-in session, so a visit
|
|
63
|
+
* that starts at the sign-in screen would otherwise stare at a blank page until a request it
|
|
64
|
+
* is bound to lose comes back.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createAdmin(options?: CreateAdminOptions): Admin;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's way of talking to its own backend.
|
|
3
|
+
*
|
|
4
|
+
* Small on purpose — it is not a general HTTP library, it is the handful of conventions
|
|
5
|
+
* `webx-ui/module-admin` and `webx-ui/module-auth` answer with: a session cookie rather than a token,
|
|
6
|
+
* 422 for a bad form, 401 for a stranger, 429 with `Retry-After` when somebody is guessing.
|
|
7
|
+
*/
|
|
8
|
+
export interface HttpOptions {
|
|
9
|
+
/** Prefixed to every relative path, e.g. `/api/cms`. */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** Where to fetch the CSRF cookie from before an unsafe request. */
|
|
12
|
+
csrfUrl?: string;
|
|
13
|
+
/** Called whenever the server answers 401, however deep in the app the call was. */
|
|
14
|
+
onUnauthenticated?: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Headers added to every request, read at the time of the request rather than fixed when
|
|
17
|
+
* the client is made — the panel's language changes while it runs.
|
|
18
|
+
*/
|
|
19
|
+
headers?: () => Record<string, string>;
|
|
20
|
+
/** Swappable for tests. */
|
|
21
|
+
fetch?: typeof globalThis.fetch;
|
|
22
|
+
}
|
|
23
|
+
export interface RequestOptions {
|
|
24
|
+
/** Query parameters; `undefined` and `null` are left out rather than sent empty. */
|
|
25
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Everything that went wrong, in the shape the panel needs to react:
|
|
31
|
+
* `errors` goes straight into `WxForm`, `retryAfter` into "try again in a moment".
|
|
32
|
+
*/
|
|
33
|
+
export declare class HttpError extends Error {
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly errors: Record<string, string[]>;
|
|
36
|
+
readonly retryAfter: number | null;
|
|
37
|
+
readonly body: unknown;
|
|
38
|
+
constructor(message: string, status: number, errors?: Record<string, string[]>, retryAfter?: number | null, body?: unknown);
|
|
39
|
+
/** A failed form rather than a failed request. */
|
|
40
|
+
get isValidation(): boolean;
|
|
41
|
+
get isUnauthenticated(): boolean;
|
|
42
|
+
get isThrottled(): boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface Http {
|
|
45
|
+
get<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
46
|
+
post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
47
|
+
put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
48
|
+
patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
49
|
+
delete<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
50
|
+
}
|
|
51
|
+
export declare function createHttp(options?: HttpOptions): Http;
|
|
52
|
+
/**
|
|
53
|
+
* Laravel writes the token URL-encoded, and it is read back the same way it was written.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readCookie(name: string): string | null;
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { App, InjectionKey } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* One language the panel can be drawn in. Mirrors what `GET /api/cms/locales` answers with.
|
|
4
|
+
*/
|
|
5
|
+
export interface LocaleDescriptor {
|
|
6
|
+
code: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** The language's name in itself — what belongs in a language picker. */
|
|
9
|
+
nativeName: string;
|
|
10
|
+
direction: 'ltr' | 'rtl';
|
|
11
|
+
default: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A group of lines, nested as deeply as the `lang` file that produced it. */
|
|
14
|
+
export type Messages = {
|
|
15
|
+
[key: string]: string | Messages;
|
|
16
|
+
};
|
|
17
|
+
/** Namespace → group → lines, which is the shape the server assembles. */
|
|
18
|
+
export type Dictionary = Record<string, Record<string, Messages>>;
|
|
19
|
+
export interface I18nState {
|
|
20
|
+
/** The language the interface is being drawn in. */
|
|
21
|
+
locale: string;
|
|
22
|
+
/** Where a missing line is looked for next. */
|
|
23
|
+
fallback: string;
|
|
24
|
+
/** Languages the interface can be switched to. */
|
|
25
|
+
panelLocales: LocaleDescriptor[];
|
|
26
|
+
/** Languages the site publishes content in — every editing screen is built around this. */
|
|
27
|
+
contentLocales: LocaleDescriptor[];
|
|
28
|
+
}
|
|
29
|
+
export type Translate = (key: string, params?: Record<string, string | number>) => string;
|
|
30
|
+
export interface I18n {
|
|
31
|
+
readonly state: I18nState;
|
|
32
|
+
/**
|
|
33
|
+
* Strings a package ships in its own code, used until the server's dictionary arrives and
|
|
34
|
+
* for whatever the dictionary does not carry.
|
|
35
|
+
*
|
|
36
|
+
* This is what lets a package work with no server at all — a story, a test, a panel
|
|
37
|
+
* assembled by hand — and what stops a missing translation from showing a key to somebody.
|
|
38
|
+
*/
|
|
39
|
+
defaults(namespace: string, messages: Record<string, Messages>): void;
|
|
40
|
+
/** Replace the dictionary with what the server sent. */
|
|
41
|
+
load(dictionary: Dictionary, locale: string, fallback?: string): void;
|
|
42
|
+
/** A `t()` bound to one namespace, so a component writes `t('shell.loading')`. */
|
|
43
|
+
scope(namespace: string): Translate;
|
|
44
|
+
/** Absolute form: `t('webx-admin::shell.loading')`. */
|
|
45
|
+
t: Translate;
|
|
46
|
+
}
|
|
47
|
+
export declare const i18nKey: InjectionKey<I18n>;
|
|
48
|
+
export declare function useI18n(): I18n;
|
|
49
|
+
/**
|
|
50
|
+
* A component that may be used outside a panel — a login card placed by hand — needs a
|
|
51
|
+
* translator either way. This gives it the package's own English when there is no panel.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useTranslate(namespace: string): Translate;
|
|
54
|
+
export declare function provideI18n(app: App, i18n: I18n): void;
|
|
55
|
+
export declare function createI18n(options?: {
|
|
56
|
+
locale?: string;
|
|
57
|
+
fallback?: string;
|
|
58
|
+
}): I18n;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createAdmin, type Admin, type AdminPlugin, type CreateAdminOptions } from './createAdmin';
|
|
2
|
+
export { createAdminContext, provideAdmin, useAdmin, adminKey, type AdminContext, type AdminState, } from './admin';
|
|
3
|
+
export { createI18n, provideI18n, useI18n, useTranslate, i18nKey, type Dictionary, type I18n, type I18nState, type LocaleDescriptor, type Messages, type Translate, } from './i18n';
|
|
4
|
+
export { adminMessages } from './messages';
|
|
5
|
+
export { createHttp, readCookie, HttpError, type Http, type HttpOptions, type RequestOptions, } from './http';
|
|
6
|
+
export type { AdminModule, AdminStatus, AdminUser, Manifest, ManifestModule, NavEntry, } from './types';
|
|
7
|
+
export { default as AdminShell } from './AdminShell';
|
|
8
|
+
export { default as AdminNav } from './AdminNav';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { inject as C, reactive as T, computed as A, defineComponent as O, resolveComponent as v, openBlock as _, createBlock as U, unref as d, withCtx as g, createElementBlock as x, Fragment as j, renderList as z, ref as M, createVNode as w, createTextVNode as R, toDisplayString as N, renderSlot as L, createCommentVNode as G, isRef as J, createApp as Q, h as k } from "vue";
|
|
2
|
+
import { useRouter as Y, useRoute as Z, createRouter as ee, createWebHistory as te } from "vue-router";
|
|
3
|
+
import { useResponsiveShell as ne, WxToaster as ae, WebxUI as oe, localesKey as se } from "@webx-ui/core";
|
|
4
|
+
const D = /* @__PURE__ */ Symbol("webx-admin");
|
|
5
|
+
function H() {
|
|
6
|
+
const e = C(D, null);
|
|
7
|
+
if (e === null)
|
|
8
|
+
throw new Error("useAdmin() was called outside a panel created by createAdmin().");
|
|
9
|
+
return e;
|
|
10
|
+
}
|
|
11
|
+
function le(e) {
|
|
12
|
+
const t = T({
|
|
13
|
+
status: "loading",
|
|
14
|
+
manifest: null,
|
|
15
|
+
user: null,
|
|
16
|
+
error: null
|
|
17
|
+
}), n = A(() => {
|
|
18
|
+
const s = t.manifest;
|
|
19
|
+
if (s === null)
|
|
20
|
+
return [];
|
|
21
|
+
const o = [];
|
|
22
|
+
for (const l of s.modules) {
|
|
23
|
+
const a = e.modules.find((i) => i.id === l.id);
|
|
24
|
+
if (a === void 0)
|
|
25
|
+
continue;
|
|
26
|
+
const r = a.path ?? a.routes?.[0]?.path;
|
|
27
|
+
r !== void 0 && o.push({
|
|
28
|
+
id: l.id,
|
|
29
|
+
title: l.title,
|
|
30
|
+
icon: l.icon,
|
|
31
|
+
path: r
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return o;
|
|
35
|
+
});
|
|
36
|
+
let c = null;
|
|
37
|
+
async function u() {
|
|
38
|
+
t.status = "loading", t.error = null;
|
|
39
|
+
try {
|
|
40
|
+
if (c !== null && (t.user = await c(), t.user === null)) {
|
|
41
|
+
t.manifest = null, t.status = "unauthenticated";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const s = await e.loadManifest();
|
|
45
|
+
t.manifest = s, e.i18n.state.contentLocales = s.locales ?? [], e.i18n.state.panelLocales = s.panelLocales ?? e.i18n.state.panelLocales, s.locale !== void 0 && s.locale !== e.i18n.state.locale && await f(s.locale), t.status = "ready";
|
|
46
|
+
} catch (s) {
|
|
47
|
+
if (ie(s)) {
|
|
48
|
+
t.manifest = null, t.user = null, t.status = "unauthenticated";
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
t.error = s instanceof Error ? s.message : String(s), t.status = "error";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function f(s) {
|
|
55
|
+
if (e.loadDictionary === void 0) {
|
|
56
|
+
e.i18n.state.locale = s;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (await e.loadDictionary(s), t.manifest !== null)
|
|
60
|
+
try {
|
|
61
|
+
t.manifest = await e.loadManifest();
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
http: e.http,
|
|
67
|
+
basePath: e.basePath,
|
|
68
|
+
apiPath: e.apiPath,
|
|
69
|
+
state: t,
|
|
70
|
+
i18n: e.i18n,
|
|
71
|
+
modules: e.modules,
|
|
72
|
+
nav: n,
|
|
73
|
+
reload: u,
|
|
74
|
+
setLocale: f,
|
|
75
|
+
setUser(s) {
|
|
76
|
+
t.user = s;
|
|
77
|
+
},
|
|
78
|
+
useSessionLoader(s) {
|
|
79
|
+
c = s;
|
|
80
|
+
},
|
|
81
|
+
can(s) {
|
|
82
|
+
const o = t.user;
|
|
83
|
+
return o === null ? !1 : o.isSuper || o.permissions.includes(s);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function re(e, t) {
|
|
88
|
+
e.provide(D, t);
|
|
89
|
+
}
|
|
90
|
+
function ie(e) {
|
|
91
|
+
return typeof e == "object" && e !== null && "status" in e && e.status === 401;
|
|
92
|
+
}
|
|
93
|
+
const $ = /* @__PURE__ */ Symbol("webx-i18n");
|
|
94
|
+
function Re() {
|
|
95
|
+
const e = C($, null);
|
|
96
|
+
if (e === null)
|
|
97
|
+
throw new Error("useI18n() was called outside a panel created by createAdmin().");
|
|
98
|
+
return e;
|
|
99
|
+
}
|
|
100
|
+
function F(e) {
|
|
101
|
+
const t = C($, null);
|
|
102
|
+
return t === null ? V().scope(e) : t.scope(e);
|
|
103
|
+
}
|
|
104
|
+
function ce(e, t) {
|
|
105
|
+
e.provide($, t);
|
|
106
|
+
}
|
|
107
|
+
function V(e = {}) {
|
|
108
|
+
const t = e.fallback ?? "en", n = T({
|
|
109
|
+
locale: e.locale ?? t,
|
|
110
|
+
fallback: t,
|
|
111
|
+
panelLocales: [],
|
|
112
|
+
contentLocales: []
|
|
113
|
+
}), c = {}, u = T({ value: {} });
|
|
114
|
+
function f(o, l, a) {
|
|
115
|
+
let r = o[l]?.[a[0] ?? ""];
|
|
116
|
+
for (const i of a.slice(1)) {
|
|
117
|
+
if (typeof r != "object" || r === null)
|
|
118
|
+
return null;
|
|
119
|
+
r = r[i];
|
|
120
|
+
}
|
|
121
|
+
return typeof r == "string" ? r : null;
|
|
122
|
+
}
|
|
123
|
+
function s(o, l, a) {
|
|
124
|
+
const [r, i] = l.includes("::") ? l.split("::", 2) : [o, l], m = i.split("."), y = f(u.value, r, m) ?? f(c, r, m) ?? // Not an empty string: a key on screen is ugly, but it says which key, and a blank
|
|
125
|
+
// label says nothing to anybody trying to fix it.
|
|
126
|
+
l;
|
|
127
|
+
return a === void 0 ? y : ue(y, a);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
state: n,
|
|
131
|
+
defaults(o, l) {
|
|
132
|
+
c[o] = { ...c[o], ...l };
|
|
133
|
+
},
|
|
134
|
+
load(o, l, a) {
|
|
135
|
+
u.value = o ?? {}, n.locale = l ?? n.locale, a !== void 0 && (n.fallback = a);
|
|
136
|
+
},
|
|
137
|
+
scope(o) {
|
|
138
|
+
return (l, a) => s(o, l, a);
|
|
139
|
+
},
|
|
140
|
+
t: (o, l) => s("", o, l)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function ue(e, t) {
|
|
144
|
+
let n = e;
|
|
145
|
+
for (const [c, u] of Object.entries(t))
|
|
146
|
+
n = n.replaceAll(`:${c}`, String(u));
|
|
147
|
+
return n;
|
|
148
|
+
}
|
|
149
|
+
const de = /* @__PURE__ */ O({
|
|
150
|
+
__name: "AdminNav",
|
|
151
|
+
props: {
|
|
152
|
+
collapsed: { type: Boolean }
|
|
153
|
+
},
|
|
154
|
+
emits: ["select"],
|
|
155
|
+
setup(e, { emit: t }) {
|
|
156
|
+
const n = t, c = H(), u = F("webx-admin"), f = Y(), s = Z(), o = A({
|
|
157
|
+
get: () => c.nav.value.find((a) => s.path.startsWith(a.path))?.id ?? "",
|
|
158
|
+
set: (l) => {
|
|
159
|
+
const a = c.nav.value.find((r) => r.id === l);
|
|
160
|
+
a !== void 0 && f.push(a.path);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return (l, a) => {
|
|
164
|
+
const r = v("wx-menu-item"), i = v("wx-menu");
|
|
165
|
+
return _(), U(i, {
|
|
166
|
+
modelValue: o.value,
|
|
167
|
+
"onUpdate:modelValue": a[0] || (a[0] = (m) => o.value = m),
|
|
168
|
+
collapsed: e.collapsed,
|
|
169
|
+
label: d(u)("nav.sections"),
|
|
170
|
+
onSelect: a[1] || (a[1] = (m) => n("select"))
|
|
171
|
+
}, {
|
|
172
|
+
default: g(() => [
|
|
173
|
+
(_(!0), x(j, null, z(d(c).nav.value, (m) => (_(), U(r, {
|
|
174
|
+
key: m.id,
|
|
175
|
+
value: m.id,
|
|
176
|
+
icon: m.icon ?? void 0,
|
|
177
|
+
label: m.title
|
|
178
|
+
}, null, 8, ["value", "icon", "label"]))), 128))
|
|
179
|
+
]),
|
|
180
|
+
_: 1
|
|
181
|
+
}, 8, ["modelValue", "collapsed", "label"]);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}), fe = {
|
|
185
|
+
key: 0,
|
|
186
|
+
class: "wx-root wx-admin-plain"
|
|
187
|
+
}, me = {
|
|
188
|
+
key: 1,
|
|
189
|
+
class: "wx-root wx-admin-plain"
|
|
190
|
+
}, pe = {
|
|
191
|
+
key: 2,
|
|
192
|
+
class: "wx-root wx-admin-plain"
|
|
193
|
+
}, he = /* @__PURE__ */ O({
|
|
194
|
+
__name: "AdminShell",
|
|
195
|
+
setup(e) {
|
|
196
|
+
const t = H(), n = F("webx-admin"), c = M(null), { layout: u, collapsed: f, showAside: s, drawerOpen: o, toggle: l, close: a } = ne(c, {
|
|
197
|
+
persist: "webx-admin-shell"
|
|
198
|
+
});
|
|
199
|
+
return (r, i) => {
|
|
200
|
+
const m = v("router-view"), y = v("wx-loading"), b = v("wx-button"), p = v("wx-result"), h = v("wx-action"), S = v("wx-text"), K = v("wx-header"), W = v("wx-aside"), q = v("wx-main"), P = v("wx-container"), B = v("wx-drawer");
|
|
201
|
+
return _(), x(j, null, [
|
|
202
|
+
w(d(ae)),
|
|
203
|
+
d(t).state.status === "unauthenticated" ? (_(), x("div", fe, [
|
|
204
|
+
w(m)
|
|
205
|
+
])) : d(t).state.status === "loading" ? (_(), x("div", me, [
|
|
206
|
+
w(y, {
|
|
207
|
+
label: d(n)("shell.loading")
|
|
208
|
+
}, null, 8, ["label"])
|
|
209
|
+
])) : d(t).state.status === "error" ? (_(), x("div", pe, [
|
|
210
|
+
w(p, {
|
|
211
|
+
status: "error",
|
|
212
|
+
title: d(n)("shell.error-title"),
|
|
213
|
+
description: d(t).state.error
|
|
214
|
+
}, {
|
|
215
|
+
default: g(() => [
|
|
216
|
+
w(b, {
|
|
217
|
+
type: "primary",
|
|
218
|
+
onClick: i[0] || (i[0] = (E) => d(t).reload())
|
|
219
|
+
}, {
|
|
220
|
+
default: g(() => [
|
|
221
|
+
R(N(d(n)("shell.retry")), 1)
|
|
222
|
+
]),
|
|
223
|
+
_: 1
|
|
224
|
+
})
|
|
225
|
+
]),
|
|
226
|
+
_: 1
|
|
227
|
+
}, 8, ["title", "description"])
|
|
228
|
+
])) : (_(), x("div", {
|
|
229
|
+
key: 3,
|
|
230
|
+
ref_key: "shellEl",
|
|
231
|
+
ref: c,
|
|
232
|
+
class: "wx-root wx-admin"
|
|
233
|
+
}, [
|
|
234
|
+
w(P, { viewport: "" }, {
|
|
235
|
+
default: g(() => [
|
|
236
|
+
w(K, null, {
|
|
237
|
+
end: g(() => [
|
|
238
|
+
L(r.$slots, "user", {}, void 0, !0)
|
|
239
|
+
]),
|
|
240
|
+
default: g(() => [
|
|
241
|
+
w(h, {
|
|
242
|
+
icon: d(u) === "drawer" ? "menu" : "sidebar",
|
|
243
|
+
title: d(u) === "drawer" ? d(n)("nav.menu") : d(n)("nav.collapse"),
|
|
244
|
+
onClick: d(l)
|
|
245
|
+
}, null, 8, ["icon", "title", "onClick"]),
|
|
246
|
+
L(r.$slots, "brand", {}, () => [
|
|
247
|
+
w(S, { weight: "semibold" }, {
|
|
248
|
+
default: g(() => [
|
|
249
|
+
R(N(d(t).state.manifest?.title), 1)
|
|
250
|
+
]),
|
|
251
|
+
_: 1
|
|
252
|
+
})
|
|
253
|
+
], !0)
|
|
254
|
+
]),
|
|
255
|
+
_: 3
|
|
256
|
+
}),
|
|
257
|
+
w(P, { direction: "horizontal" }, {
|
|
258
|
+
default: g(() => [
|
|
259
|
+
d(s) ? (_(), U(W, {
|
|
260
|
+
key: 0,
|
|
261
|
+
collapsed: d(f),
|
|
262
|
+
width: 220,
|
|
263
|
+
scroll: ""
|
|
264
|
+
}, {
|
|
265
|
+
default: g(() => [
|
|
266
|
+
L(r.$slots, "nav", { collapsed: d(f) }, void 0, !0)
|
|
267
|
+
]),
|
|
268
|
+
_: 3
|
|
269
|
+
}, 8, ["collapsed"])) : G("", !0),
|
|
270
|
+
w(q, {
|
|
271
|
+
padding: "md",
|
|
272
|
+
scroll: "",
|
|
273
|
+
class: "wx-admin__screen"
|
|
274
|
+
}, {
|
|
275
|
+
default: g(() => [
|
|
276
|
+
w(m)
|
|
277
|
+
]),
|
|
278
|
+
_: 1
|
|
279
|
+
})
|
|
280
|
+
]),
|
|
281
|
+
_: 3
|
|
282
|
+
})
|
|
283
|
+
]),
|
|
284
|
+
_: 3
|
|
285
|
+
}),
|
|
286
|
+
w(B, {
|
|
287
|
+
open: d(o),
|
|
288
|
+
"onUpdate:open": i[2] || (i[2] = (E) => J(o) ? o.value = E : null),
|
|
289
|
+
title: d(n)("nav.menu"),
|
|
290
|
+
side: "left",
|
|
291
|
+
size: 260,
|
|
292
|
+
closable: ""
|
|
293
|
+
}, {
|
|
294
|
+
default: g(() => [
|
|
295
|
+
L(r.$slots, "nav", {
|
|
296
|
+
collapsed: !1,
|
|
297
|
+
onSelect: i[1] || (i[1] = //@ts-ignore
|
|
298
|
+
(...E) => d(a) && d(a)(...E))
|
|
299
|
+
}, void 0, !0)
|
|
300
|
+
]),
|
|
301
|
+
_: 3
|
|
302
|
+
}, 8, ["open", "title"])
|
|
303
|
+
], 512))
|
|
304
|
+
], 64);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}), we = (e, t) => {
|
|
308
|
+
const n = e.__vccOpts || e;
|
|
309
|
+
for (const [c, u] of t)
|
|
310
|
+
n[c] = u;
|
|
311
|
+
return n;
|
|
312
|
+
}, ve = /* @__PURE__ */ we(he, [["__scopeId", "data-v-9ef35d4d"]]);
|
|
313
|
+
class ge extends Error {
|
|
314
|
+
constructor(t, n, c = {}, u = null, f = null) {
|
|
315
|
+
super(t), this.status = n, this.errors = c, this.retryAfter = u, this.body = f, this.name = "HttpError";
|
|
316
|
+
}
|
|
317
|
+
status;
|
|
318
|
+
errors;
|
|
319
|
+
retryAfter;
|
|
320
|
+
body;
|
|
321
|
+
/** A failed form rather than a failed request. */
|
|
322
|
+
get isValidation() {
|
|
323
|
+
return this.status === 422;
|
|
324
|
+
}
|
|
325
|
+
get isUnauthenticated() {
|
|
326
|
+
return this.status === 401;
|
|
327
|
+
}
|
|
328
|
+
get isThrottled() {
|
|
329
|
+
return this.status === 429;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const ye = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
333
|
+
function _e(e = {}) {
|
|
334
|
+
const t = (e.baseUrl ?? "").replace(/\/$/, ""), n = e.csrfUrl ?? "/sanctum/csrf-cookie", c = e.fetch ?? globalThis.fetch.bind(globalThis), u = e.onUnauthenticated, f = e.headers;
|
|
335
|
+
let s = !1;
|
|
336
|
+
async function o(a = !1) {
|
|
337
|
+
s && !a && I("XSRF-TOKEN") !== null || (await c(n, { credentials: "same-origin" }), s = !0);
|
|
338
|
+
}
|
|
339
|
+
async function l(a, r, i, m = {}, y = !1) {
|
|
340
|
+
const b = ye.has(a);
|
|
341
|
+
b && await o();
|
|
342
|
+
const p = {
|
|
343
|
+
Accept: "application/json",
|
|
344
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
345
|
+
// Standing headers first, so a caller can still override one for a single request.
|
|
346
|
+
...f?.(),
|
|
347
|
+
...m.headers
|
|
348
|
+
};
|
|
349
|
+
if (b) {
|
|
350
|
+
const S = I("XSRF-TOKEN");
|
|
351
|
+
S !== null && (p["X-XSRF-TOKEN"] = S);
|
|
352
|
+
}
|
|
353
|
+
i !== void 0 && (p["Content-Type"] = "application/json");
|
|
354
|
+
const h = await c(xe(t, r, m.query), {
|
|
355
|
+
method: a,
|
|
356
|
+
credentials: "same-origin",
|
|
357
|
+
headers: p,
|
|
358
|
+
signal: m.signal,
|
|
359
|
+
body: i === void 0 ? void 0 : JSON.stringify(i)
|
|
360
|
+
});
|
|
361
|
+
if (h.status === 419 && b && !y)
|
|
362
|
+
return await o(!0), l(a, r, i, m, !0);
|
|
363
|
+
if (h.status === 401 && u?.(), !h.ok)
|
|
364
|
+
throw await be(h);
|
|
365
|
+
if (h.status !== 204)
|
|
366
|
+
return await h.json();
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
get: (a, r) => l("GET", a, void 0, r),
|
|
370
|
+
post: (a, r, i) => l("POST", a, r, i),
|
|
371
|
+
put: (a, r, i) => l("PUT", a, r, i),
|
|
372
|
+
patch: (a, r, i) => l("PATCH", a, r, i),
|
|
373
|
+
delete: (a, r) => l("DELETE", a, void 0, r)
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function be(e) {
|
|
377
|
+
let t = null;
|
|
378
|
+
try {
|
|
379
|
+
t = await e.json();
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
const n = t ?? {}, c = typeof n.message == "string" && n.message !== "" ? n.message : e.statusText || `Request failed with ${e.status}`, u = n.errors !== null && typeof n.errors == "object" ? n.errors : {}, f = e.headers.get("Retry-After"), s = f === null ? null : Number.parseInt(f, 10);
|
|
383
|
+
return new ge(
|
|
384
|
+
c,
|
|
385
|
+
e.status,
|
|
386
|
+
u,
|
|
387
|
+
Number.isFinite(s) ? s : null,
|
|
388
|
+
t
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function xe(e, t, n) {
|
|
392
|
+
const u = /^https?:\/\//i.test(t) ? t : `${e}/${t.replace(/^\//, "")}`;
|
|
393
|
+
if (n === void 0)
|
|
394
|
+
return u;
|
|
395
|
+
const f = new URLSearchParams();
|
|
396
|
+
for (const [o, l] of Object.entries(n))
|
|
397
|
+
l != null && f.set(o, String(l));
|
|
398
|
+
const s = f.toString();
|
|
399
|
+
return s === "" ? u : `${u}${u.includes("?") ? "&" : "?"}${s}`;
|
|
400
|
+
}
|
|
401
|
+
function I(e) {
|
|
402
|
+
if (typeof document > "u")
|
|
403
|
+
return null;
|
|
404
|
+
for (const t of document.cookie.split(";")) {
|
|
405
|
+
const [n, ...c] = t.trim().split("=");
|
|
406
|
+
if (n === e)
|
|
407
|
+
return decodeURIComponent(c.join("="));
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const Se = {
|
|
412
|
+
shell: {
|
|
413
|
+
loading: "Loading the panel…",
|
|
414
|
+
"error-title": "The panel could not start",
|
|
415
|
+
retry: "Try again",
|
|
416
|
+
"empty-title": "Nothing is installed yet",
|
|
417
|
+
"empty-description": "This panel has no modules. Install one and it will appear here."
|
|
418
|
+
},
|
|
419
|
+
nav: {
|
|
420
|
+
sections: "Sections",
|
|
421
|
+
menu: "Menu",
|
|
422
|
+
collapse: "Collapse the menu",
|
|
423
|
+
language: "Language"
|
|
424
|
+
}
|
|
425
|
+
}, X = "webx.locale";
|
|
426
|
+
function Ne(e = {}) {
|
|
427
|
+
const t = e.manifestUrl ?? Ue() ?? "/api/cms/manifest", n = e.apiPath ?? t.replace(/\/manifest\/?$/, ""), c = e.basePath ?? "/cms", u = e.modules ?? [], f = [...e.routes ?? []];
|
|
428
|
+
for (const p of u)
|
|
429
|
+
f.push(...p.routes ?? []);
|
|
430
|
+
const s = ee({
|
|
431
|
+
history: te(c),
|
|
432
|
+
routes: f
|
|
433
|
+
}), o = V({ locale: e.locale ?? Le() }), l = e.http ?? _e({
|
|
434
|
+
baseUrl: "",
|
|
435
|
+
onUnauthenticated: () => {
|
|
436
|
+
r.setUser(null), r.state.status = "unauthenticated";
|
|
437
|
+
},
|
|
438
|
+
// Every request says which language the panel is currently showing. It decides what
|
|
439
|
+
// the server writes its own messages in — a 422 under a field — for anybody who has
|
|
440
|
+
// not stored a preference yet, which is everybody until they choose one. Without it,
|
|
441
|
+
// signing in on a Russian sign-in screen lands in an English panel.
|
|
442
|
+
headers: () => ({ "X-Webx-Locale": o.state.locale })
|
|
443
|
+
});
|
|
444
|
+
o.defaults("webx-admin", Se);
|
|
445
|
+
async function a(p) {
|
|
446
|
+
const h = await l.get(`${n}/translations/${p}`);
|
|
447
|
+
o.load(h.data.namespaces, h.data.locale, h.data.fallback), ke(h.data.locale), Te(h.data.locale, o.state.panelLocales);
|
|
448
|
+
}
|
|
449
|
+
const r = le({
|
|
450
|
+
http: l,
|
|
451
|
+
basePath: c,
|
|
452
|
+
apiPath: n,
|
|
453
|
+
modules: u,
|
|
454
|
+
i18n: o,
|
|
455
|
+
loadDictionary: a,
|
|
456
|
+
loadManifest: async () => (await l.get(t)).data
|
|
457
|
+
}), i = Q(Ee(e));
|
|
458
|
+
i.use(oe), re(i, r), ce(i, o);
|
|
459
|
+
const m = M("");
|
|
460
|
+
i.provide(se, {
|
|
461
|
+
list: A(
|
|
462
|
+
() => o.state.contentLocales.map((p) => ({
|
|
463
|
+
code: p.code,
|
|
464
|
+
label: p.code.toUpperCase()
|
|
465
|
+
}))
|
|
466
|
+
),
|
|
467
|
+
active: A({
|
|
468
|
+
get: () => m.value || (o.state.contentLocales[0]?.code ?? ""),
|
|
469
|
+
set: (p) => {
|
|
470
|
+
m.value = p;
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
});
|
|
474
|
+
const y = {
|
|
475
|
+
app: i,
|
|
476
|
+
router: s,
|
|
477
|
+
context: r,
|
|
478
|
+
i18n: o,
|
|
479
|
+
async mount() {
|
|
480
|
+
await Promise.all([b(), a(o.state.locale)]).catch(() => {
|
|
481
|
+
}), i.mount(e.el ?? "#webx-app"), await r.reload();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
async function b() {
|
|
485
|
+
const p = await l.get(`${n}/locales`);
|
|
486
|
+
o.state.panelLocales = p.data.panel, o.state.contentLocales = p.data.content;
|
|
487
|
+
}
|
|
488
|
+
for (const p of e.plugins ?? [])
|
|
489
|
+
p.install(y);
|
|
490
|
+
return i.use(s), y;
|
|
491
|
+
}
|
|
492
|
+
function Ee(e) {
|
|
493
|
+
const t = {
|
|
494
|
+
// The menu is the panel's own: it is the manifest, drawn.
|
|
495
|
+
nav: (n) => k(de, { collapsed: n.collapsed === !0 })
|
|
496
|
+
};
|
|
497
|
+
return e.brand !== void 0 && (t.brand = () => k(e.brand)), e.userMenu !== void 0 && (t.user = () => k(e.userMenu)), { render: () => k(ve, null, t) };
|
|
498
|
+
}
|
|
499
|
+
function Le() {
|
|
500
|
+
const e = Ae(X);
|
|
501
|
+
return e !== null ? e : typeof document < "u" && document.documentElement.lang !== "" ? document.documentElement.lang : typeof navigator > "u" ? "en" : navigator.language;
|
|
502
|
+
}
|
|
503
|
+
function ke(e) {
|
|
504
|
+
try {
|
|
505
|
+
localStorage.setItem(X, e);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function Ae(e) {
|
|
510
|
+
try {
|
|
511
|
+
return localStorage.getItem(e);
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function Te(e, t) {
|
|
517
|
+
typeof document > "u" || (document.documentElement.lang = e, document.documentElement.dir = t.find((n) => n.code === e)?.direction ?? "ltr");
|
|
518
|
+
}
|
|
519
|
+
function Ue() {
|
|
520
|
+
return typeof document > "u" ? null : document.querySelector('meta[name="webx-manifest"]')?.getAttribute("content") ?? null;
|
|
521
|
+
}
|
|
522
|
+
export {
|
|
523
|
+
de as AdminNav,
|
|
524
|
+
ve as AdminShell,
|
|
525
|
+
ge as HttpError,
|
|
526
|
+
D as adminKey,
|
|
527
|
+
Se as adminMessages,
|
|
528
|
+
Ne as createAdmin,
|
|
529
|
+
le as createAdminContext,
|
|
530
|
+
_e as createHttp,
|
|
531
|
+
V as createI18n,
|
|
532
|
+
$ as i18nKey,
|
|
533
|
+
re as provideAdmin,
|
|
534
|
+
ce as provideI18n,
|
|
535
|
+
I as readCookie,
|
|
536
|
+
H as useAdmin,
|
|
537
|
+
Re as useI18n,
|
|
538
|
+
F as useTranslate
|
|
539
|
+
};
|
|
540
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/admin.ts","../src/i18n.ts","../src/AdminNav.vue","../src/AdminShell.vue","../src/http.ts","../src/messages.ts","../src/createAdmin.ts"],"sourcesContent":["import { computed, inject, reactive, type App, type ComputedRef, type InjectionKey } from 'vue'\nimport type { Http } from './http'\nimport type { I18n } from './i18n'\nimport type { AdminModule, AdminStatus, AdminUser, Manifest, NavEntry } from './types'\n\nexport interface AdminContext {\n /** The panel's own backend. */\n readonly http: Http\n /** Where the panel is served — the router's base. */\n readonly basePath: string\n /** Where its JSON lives, so a module does not have to be told twice. */\n readonly apiPath: string\n readonly state: AdminState\n /** The interface's own words, and the languages it can be shown in. */\n readonly i18n: I18n\n /** Modules registered on the front end, whether or not the server reports them. */\n readonly modules: readonly AdminModule[]\n /** Navigation, in the order the server gave, for the modules that exist on both sides. */\n readonly nav: ComputedRef<NavEntry[]>\n /** Ask the server what the panel is and who is signed in again. */\n reload(): Promise<void>\n /**\n * Draw the panel in another language: fetches that dictionary and remembers the choice for\n * the next visit. Storing it against the administrator is an auth module's business — this\n * only changes what is on screen.\n */\n setLocale(code: string): Promise<void>\n /** Filled in by an auth module; `null` means nobody is signed in. */\n setUser(user: AdminUser | null): void\n /**\n * How the panel finds out who is signed in, set by an auth module. Without one the panel\n * simply asks for the manifest and lets a 401 answer the question.\n */\n useSessionLoader(loader: () => Promise<AdminUser | null>): void\n can(permission: string): boolean\n}\n\nexport interface AdminState {\n status: AdminStatus\n manifest: Manifest | null\n user: AdminUser | null\n error: string | null\n}\n\nexport const adminKey: InjectionKey<AdminContext> = Symbol('webx-admin')\n\nexport function useAdmin(): AdminContext {\n const admin = inject(adminKey, null)\n\n if (admin === null) {\n throw new Error('useAdmin() was called outside a panel created by createAdmin().')\n }\n\n return admin\n}\n\n/**\n * Permissions are flattened by the server, so a check is a lookup. A super administrator\n * carries no permissions and passes everything — the same rule as on the server, in the one\n * place the front end asks the question.\n */\nexport function createAdminContext(options: {\n http: Http\n basePath: string\n apiPath: string\n modules: AdminModule[]\n i18n: I18n\n loadManifest: () => Promise<Manifest>\n loadDictionary?: (locale: string) => Promise<void>\n}): AdminContext {\n const state = reactive<AdminState>({\n status: 'loading',\n manifest: null,\n user: null,\n error: null,\n })\n\n const nav = computed<NavEntry[]>(() => {\n const manifest = state.manifest\n\n if (manifest === null) {\n return []\n }\n\n const entries: NavEntry[] = []\n\n for (const module of manifest.modules) {\n const registered = options.modules.find((candidate) => candidate.id === module.id)\n\n // A module the server has and the front end does not is not a bug worth shouting\n // about — the panel is assembled from two halves and they are deployed separately —\n // but it has nowhere to send anybody, so it stays out of the menu.\n if (registered === undefined) {\n continue\n }\n\n const path = registered.path ?? registered.routes?.[0]?.path\n\n if (path === undefined) {\n continue\n }\n\n entries.push({\n id: module.id,\n title: module.title,\n icon: module.icon,\n path,\n })\n }\n\n return entries\n })\n\n let loadSession: (() => Promise<AdminUser | null>) | null = null\n\n async function reload(): Promise<void> {\n state.status = 'loading'\n state.error = null\n\n try {\n if (loadSession !== null) {\n state.user = await loadSession()\n\n // Asking for the manifest as a stranger would only produce the 401 we already know\n // about, and a spurious one in the network log for whoever is debugging.\n if (state.user === null) {\n state.manifest = null\n state.status = 'unauthenticated'\n\n return\n }\n }\n\n const manifest = await options.loadManifest()\n\n state.manifest = manifest\n options.i18n.state.contentLocales = manifest.locales ?? []\n options.i18n.state.panelLocales = manifest.panelLocales ?? options.i18n.state.panelLocales\n\n // The administrator's own choice, which the sign-in screen had no way of knowing: it\n // drew itself in whatever the browser asked for.\n if (manifest.locale !== undefined && manifest.locale !== options.i18n.state.locale) {\n await setLocale(manifest.locale)\n }\n\n state.status = 'ready'\n } catch (error) {\n // 401 is not a failure: it is the panel finding out nobody is signed in, which is the\n // normal way a visit starts.\n if (isUnauthenticated(error)) {\n state.manifest = null\n state.user = null\n state.status = 'unauthenticated'\n\n return\n }\n\n state.error = error instanceof Error ? error.message : String(error)\n state.status = 'error'\n }\n }\n\n async function setLocale(code: string): Promise<void> {\n if (options.loadDictionary === undefined) {\n options.i18n.state.locale = code\n\n return\n }\n\n await options.loadDictionary(code)\n\n // The dictionary is not the whole of the interface. Section titles are translated on the\n // server and travel inside the manifest, which was fetched in the previous language — so\n // without this the panel switches everything except its own navigation, and the sidebar\n // goes on naming the section in the language nobody is reading any more until the page is\n // reloaded. Only worth doing once there is a manifest to replace: during the first load\n // the caller is `reload()` itself, which is about to fetch one.\n if (state.manifest !== null) {\n try {\n state.manifest = await options.loadManifest()\n } catch {\n // A manifest that will not come back is `reload()`'s problem to report. The language\n // did change, and a stale section title is not worth throwing away a working panel.\n }\n }\n }\n\n return {\n http: options.http,\n basePath: options.basePath,\n apiPath: options.apiPath,\n state,\n i18n: options.i18n,\n modules: options.modules,\n nav,\n reload,\n setLocale,\n setUser(user) {\n state.user = user\n },\n useSessionLoader(loader) {\n loadSession = loader\n },\n can(permission) {\n const user = state.user\n\n if (user === null) {\n return false\n }\n\n return user.isSuper || user.permissions.includes(permission)\n },\n }\n}\n\nexport function provideAdmin(app: App, admin: AdminContext): void {\n app.provide(adminKey, admin)\n}\n\nfunction isUnauthenticated(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === 401\n )\n}\n","import { inject, reactive, type App, type InjectionKey } from 'vue'\n\n/**\n * One language the panel can be drawn in. Mirrors what `GET /api/cms/locales` answers with.\n */\nexport interface LocaleDescriptor {\n code: string\n name: string\n /** The language's name in itself — what belongs in a language picker. */\n nativeName: string\n direction: 'ltr' | 'rtl'\n default: boolean\n}\n\n/** A group of lines, nested as deeply as the `lang` file that produced it. */\nexport type Messages = { [key: string]: string | Messages }\n\n/** Namespace → group → lines, which is the shape the server assembles. */\nexport type Dictionary = Record<string, Record<string, Messages>>\n\nexport interface I18nState {\n /** The language the interface is being drawn in. */\n locale: string\n /** Where a missing line is looked for next. */\n fallback: string\n /** Languages the interface can be switched to. */\n panelLocales: LocaleDescriptor[]\n /** Languages the site publishes content in — every editing screen is built around this. */\n contentLocales: LocaleDescriptor[]\n}\n\nexport type Translate = (key: string, params?: Record<string, string | number>) => string\n\nexport interface I18n {\n readonly state: I18nState\n /**\n * Strings a package ships in its own code, used until the server's dictionary arrives and\n * for whatever the dictionary does not carry.\n *\n * This is what lets a package work with no server at all — a story, a test, a panel\n * assembled by hand — and what stops a missing translation from showing a key to somebody.\n */\n defaults(namespace: string, messages: Record<string, Messages>): void\n /** Replace the dictionary with what the server sent. */\n load(dictionary: Dictionary, locale: string, fallback?: string): void\n /** A `t()` bound to one namespace, so a component writes `t('shell.loading')`. */\n scope(namespace: string): Translate\n /** Absolute form: `t('webx-admin::shell.loading')`. */\n t: Translate\n}\n\nexport const i18nKey: InjectionKey<I18n> = Symbol('webx-i18n')\n\nexport function useI18n(): I18n {\n const i18n = inject(i18nKey, null)\n\n if (i18n === null) {\n throw new Error('useI18n() was called outside a panel created by createAdmin().')\n }\n\n return i18n\n}\n\n/**\n * A component that may be used outside a panel — a login card placed by hand — needs a\n * translator either way. This gives it the package's own English when there is no panel.\n */\nexport function useTranslate(namespace: string): Translate {\n const i18n = inject(i18nKey, null)\n\n return i18n === null ? createI18n().scope(namespace) : i18n.scope(namespace)\n}\n\nexport function provideI18n(app: App, i18n: I18n): void {\n app.provide(i18nKey, i18n)\n}\n\nexport function createI18n(options: { locale?: string; fallback?: string } = {}): I18n {\n const fallback = options.fallback ?? 'en'\n\n const state = reactive<I18nState>({\n locale: options.locale ?? fallback,\n fallback,\n panelLocales: [],\n contentLocales: [],\n })\n\n // Kept apart from the dictionary rather than merged into it: the server's answer is\n // replaced wholesale on every language change, and built-in strings have to survive that.\n const builtIn: Dictionary = {}\n const dictionary = reactive<{ value: Dictionary }>({ value: {} })\n\n function lookup(source: Dictionary, namespace: string, path: string[]): string | null {\n let node: string | Messages | undefined = source[namespace]?.[path[0] ?? '']\n\n for (const segment of path.slice(1)) {\n if (typeof node !== 'object' || node === null) {\n return null\n }\n\n node = node[segment]\n }\n\n return typeof node === 'string' ? node : null\n }\n\n function translate(\n namespace: string,\n key: string,\n params?: Record<string, string | number>,\n ): string {\n // An absolute key wins, so one namespace can borrow a line from another without a second\n // translator.\n const [explicitNamespace, rest] = key.includes('::')\n ? (key.split('::', 2) as [string, string])\n : [namespace, key]\n\n const path = rest.split('.')\n\n const line =\n lookup(dictionary.value, explicitNamespace, path) ??\n lookup(builtIn, explicitNamespace, path) ??\n // Not an empty string: a key on screen is ugly, but it says which key, and a blank\n // label says nothing to anybody trying to fix it.\n key\n\n return params === undefined ? line : fill(line, params)\n }\n\n return {\n state,\n defaults(namespace, messages) {\n builtIn[namespace] = { ...builtIn[namespace], ...messages }\n },\n load(next, locale, nextFallback) {\n // Guarded rather than trusted: an answer that is not the shape expected should leave\n // the panel in English, not without any words at all.\n dictionary.value = next ?? {}\n state.locale = locale ?? state.locale\n\n if (nextFallback !== undefined) {\n state.fallback = nextFallback\n }\n },\n scope(namespace) {\n return (key, params) => translate(namespace, key, params)\n },\n t: (key, params) => translate('', key, params),\n }\n}\n\n/**\n * `:name` placeholders, the way Laravel writes them — the strings come from its `lang` files,\n * so they should read the same on both sides.\n */\nfunction fill(line: string, params: Record<string, string | number>): string {\n let filled = line\n\n for (const [name, value] of Object.entries(params)) {\n filled = filled.replaceAll(`:${name}`, String(value))\n }\n\n return filled\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { useAdmin } from './admin'\nimport { useTranslate } from './i18n'\n\n/**\n * The menu, built from the manifest rather than written out. What the panel offers is what the\n * installation actually has — adding a module on the server and installing its front end is\n * the whole of \"adding a section\".\n */\ndefineProps<{ collapsed?: boolean }>()\n\nconst emit = defineEmits<{ select: [] }>()\n\nconst admin = useAdmin()\nconst t = useTranslate('webx-admin')\nconst router = useRouter()\nconst route = useRoute()\n\nconst current = computed<string>({\n get: () => {\n const match = admin.nav.value.find((entry) => route.path.startsWith(entry.path))\n\n return match?.id ?? ''\n },\n set: (id) => {\n const entry = admin.nav.value.find((candidate) => candidate.id === id)\n\n if (entry !== undefined) {\n void router.push(entry.path)\n }\n },\n})\n</script>\n\n<template>\n <wx-menu\n v-model=\"current\"\n :collapsed=\"collapsed\"\n :label=\"t('nav.sections')\"\n @select=\"emit('select')\"\n >\n <wx-menu-item\n v-for=\"entry in admin.nav.value\"\n :key=\"entry.id\"\n :value=\"entry.id\"\n :icon=\"entry.icon ?? undefined\"\n :label=\"entry.title\"\n />\n </wx-menu>\n</template>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useResponsiveShell, WxToaster } from '@webx-ui/core'\nimport { useAdmin } from './admin'\nimport { useTranslate } from './i18n'\n\n/**\n * The panel around the screen: navigation built from the manifest, a header, and the hole the\n * router fills.\n *\n * Three shapes, chosen by the width of the shell rather than the window: the full sidebar on a\n * desktop, an icon rail on a tablet, a drawer behind a burger on a phone.\n *\n * It draws nothing until the manifest has arrived, and nothing but the route while nobody is\n * signed in — the sign-in screen is a route like any other, and it has no business being\n * wrapped in a menu of sections the visitor cannot reach.\n */\nconst admin = useAdmin()\nconst t = useTranslate('webx-admin')\nconst shellEl = ref<HTMLElement | null>(null)\n\nconst { layout, collapsed, showAside, drawerOpen, toggle, close } = useResponsiveShell(shellEl, {\n persist: 'webx-admin-shell',\n})\n</script>\n\n<template>\n <wx-toaster />\n\n <div v-if=\"admin.state.status === 'unauthenticated'\" class=\"wx-root wx-admin-plain\">\n <router-view />\n </div>\n\n <div v-else-if=\"admin.state.status === 'loading'\" class=\"wx-root wx-admin-plain\">\n <wx-loading :label=\"t('shell.loading')\" />\n </div>\n\n <div v-else-if=\"admin.state.status === 'error'\" class=\"wx-root wx-admin-plain\">\n <wx-result status=\"error\" :title=\"t('shell.error-title')\" :description=\"admin.state.error\">\n <wx-button type=\"primary\" @click=\"admin.reload()\">{{ t('shell.retry') }}</wx-button>\n </wx-result>\n </div>\n\n <div v-else ref=\"shellEl\" class=\"wx-root wx-admin\">\n <wx-container viewport>\n <wx-header>\n <wx-action\n :icon=\"layout === 'drawer' ? 'menu' : 'sidebar'\"\n :title=\"layout === 'drawer' ? t('nav.menu') : t('nav.collapse')\"\n @click=\"toggle\"\n />\n\n <slot name=\"brand\">\n <wx-text weight=\"semibold\">{{ admin.state.manifest?.title }}</wx-text>\n </slot>\n\n <template #end>\n <slot name=\"user\" />\n </template>\n </wx-header>\n\n <wx-container direction=\"horizontal\">\n <wx-aside v-if=\"showAside\" :collapsed=\"collapsed\" :width=\"220\" scroll>\n <slot name=\"nav\" :collapsed=\"collapsed\" />\n </wx-aside>\n\n <wx-main padding=\"md\" scroll class=\"wx-admin__screen\">\n <router-view />\n </wx-main>\n </wx-container>\n </wx-container>\n\n <wx-drawer v-model:open=\"drawerOpen\" :title=\"t('nav.menu')\" side=\"left\" :size=\"260\" closable>\n <slot name=\"nav\" :collapsed=\"false\" @select=\"close\" />\n </wx-drawer>\n </div>\n</template>\n\n<style scoped>\n.wx-admin {\n height: 100dvh;\n}\n\n.wx-admin__screen {\n min-width: 0;\n min-height: 0;\n}\n\n/* The states with no shell around them: sign-in, loading, and the one where the panel could\n not start. Each is a single thing in the middle of an empty page. */\n.wx-admin-plain {\n display: grid;\n place-items: center;\n /* Without this the padding is added to the viewport height and the page scrolls by exactly\n the padding. */\n box-sizing: border-box;\n min-height: 100dvh;\n padding: var(--wx-space-16);\n background: var(--wx-bg-body);\n}\n</style>\n\n<style>\n/* Not scoped, and global on purpose: the panel is the whole page, so the browser default\n margin on <body> shows up as a gap around the shell and puts a scrollbar under a column\n that is exactly one viewport tall. */\nhtml:has(> body > #webx-app),\nbody:has(> #webx-app) {\n margin: 0;\n}\n</style>\n","/**\n * The panel's way of talking to its own backend.\n *\n * Small on purpose — it is not a general HTTP library, it is the handful of conventions\n * `webx-ui/module-admin` and `webx-ui/module-auth` answer with: a session cookie rather than a token,\n * 422 for a bad form, 401 for a stranger, 429 with `Retry-After` when somebody is guessing.\n */\n\nexport interface HttpOptions {\n /** Prefixed to every relative path, e.g. `/api/cms`. */\n baseUrl?: string\n /** Where to fetch the CSRF cookie from before an unsafe request. */\n csrfUrl?: string\n /** Called whenever the server answers 401, however deep in the app the call was. */\n onUnauthenticated?: () => void\n /**\n * Headers added to every request, read at the time of the request rather than fixed when\n * the client is made — the panel's language changes while it runs.\n */\n headers?: () => Record<string, string>\n /** Swappable for tests. */\n fetch?: typeof globalThis.fetch\n}\n\nexport interface RequestOptions {\n /** Query parameters; `undefined` and `null` are left out rather than sent empty. */\n query?: Record<string, string | number | boolean | null | undefined>\n headers?: Record<string, string>\n signal?: AbortSignal\n}\n\n/**\n * Everything that went wrong, in the shape the panel needs to react:\n * `errors` goes straight into `WxForm`, `retryAfter` into \"try again in a moment\".\n */\nexport class HttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly errors: Record<string, string[]> = {},\n readonly retryAfter: number | null = null,\n readonly body: unknown = null,\n ) {\n super(message)\n this.name = 'HttpError'\n }\n\n /** A failed form rather than a failed request. */\n get isValidation(): boolean {\n return this.status === 422\n }\n\n get isUnauthenticated(): boolean {\n return this.status === 401\n }\n\n get isThrottled(): boolean {\n return this.status === 429\n }\n}\n\nexport interface Http {\n get<T>(path: string, options?: RequestOptions): Promise<T>\n post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>\n put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>\n patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>\n delete<T>(path: string, options?: RequestOptions): Promise<T>\n}\n\nconst UNSAFE = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport function createHttp(options: HttpOptions = {}): Http {\n const baseUrl = (options.baseUrl ?? '').replace(/\\/$/, '')\n const csrfUrl = options.csrfUrl ?? '/sanctum/csrf-cookie'\n const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n const onUnauthenticated = options.onUnauthenticated\n const standingHeaders = options.headers\n\n let csrfFetched = false\n\n async function ensureCsrfCookie(force = false): Promise<void> {\n if (csrfFetched && !force && readCookie('XSRF-TOKEN') !== null) {\n return\n }\n\n await doFetch(csrfUrl, { credentials: 'same-origin' })\n csrfFetched = true\n }\n\n async function request<T>(\n method: string,\n path: string,\n body?: unknown,\n options: RequestOptions = {},\n retried = false,\n ): Promise<T> {\n const unsafe = UNSAFE.has(method)\n\n if (unsafe) {\n await ensureCsrfCookie()\n }\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n // Standing headers first, so a caller can still override one for a single request.\n ...standingHeaders?.(),\n ...options.headers,\n }\n\n if (unsafe) {\n const token = readCookie('XSRF-TOKEN')\n\n if (token !== null) {\n headers['X-XSRF-TOKEN'] = token\n }\n }\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n }\n\n const response = await doFetch(url(baseUrl, path, options.query), {\n method,\n credentials: 'same-origin',\n headers,\n signal: options.signal,\n body: body === undefined ? undefined : JSON.stringify(body),\n })\n\n // 419 is Laravel for \"your CSRF token has gone stale\", which happens after a session is\n // regenerated — at sign-in, most of all. Fetching a fresh one and going again is what a\n // person would do by reloading, without the reload.\n if (response.status === 419 && unsafe && !retried) {\n await ensureCsrfCookie(true)\n\n return request<T>(method, path, body, options, true)\n }\n\n if (response.status === 401) {\n onUnauthenticated?.()\n }\n\n if (!response.ok) {\n throw await toError(response)\n }\n\n if (response.status === 204) {\n return undefined as T\n }\n\n return (await response.json()) as T\n }\n\n return {\n get: (path, options) => request('GET', path, undefined, options),\n post: (path, body, options) => request('POST', path, body, options),\n put: (path, body, options) => request('PUT', path, body, options),\n patch: (path, body, options) => request('PATCH', path, body, options),\n delete: (path, options) => request('DELETE', path, undefined, options),\n }\n}\n\nasync function toError(response: Response): Promise<HttpError> {\n let body: unknown = null\n\n try {\n body = await response.json()\n } catch {\n // A gateway or a fatal error answers with HTML; there is nothing to read out of it.\n }\n\n const payload = (body ?? {}) as { message?: unknown; errors?: unknown }\n const message =\n typeof payload.message === 'string' && payload.message !== ''\n ? payload.message\n : response.statusText || `Request failed with ${response.status}`\n\n const errors =\n payload.errors !== null && typeof payload.errors === 'object'\n ? (payload.errors as Record<string, string[]>)\n : {}\n\n const header = response.headers.get('Retry-After')\n const retryAfter = header === null ? null : Number.parseInt(header, 10)\n\n return new HttpError(\n message,\n response.status,\n errors,\n Number.isFinite(retryAfter) ? retryAfter : null,\n body,\n )\n}\n\nfunction url(baseUrl: string, path: string, query?: RequestOptions['query']): string {\n const absolute = /^https?:\\/\\//i.test(path)\n const full = absolute ? path : `${baseUrl}/${path.replace(/^\\//, '')}`\n\n if (query === undefined) {\n return full\n }\n\n const search = new URLSearchParams()\n\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n search.set(key, String(value))\n }\n }\n\n const serialised = search.toString()\n\n return serialised === '' ? full : `${full}${full.includes('?') ? '&' : '?'}${serialised}`\n}\n\n/**\n * Laravel writes the token URL-encoded, and it is read back the same way it was written.\n */\nexport function readCookie(name: string): string | null {\n if (typeof document === 'undefined') {\n return null\n }\n\n for (const part of document.cookie.split(';')) {\n const [key, ...rest] = part.trim().split('=')\n\n if (key === name) {\n return decodeURIComponent(rest.join('='))\n }\n }\n\n return null\n}\n","import type { Messages } from './i18n'\n\n/**\n * The panel's own words, in English.\n *\n * The same keys `webx-ui/module-admin` ships as `lang/en/*.php`, kept here so the package works with\n * no server behind it. Anything the server sends wins; this is the floor, not the source of\n * truth. Translations belong in the Composer package, where one file serves both halves.\n */\nexport const adminMessages: Record<string, Messages> = {\n shell: {\n loading: 'Loading the panel…',\n 'error-title': 'The panel could not start',\n retry: 'Try again',\n 'empty-title': 'Nothing is installed yet',\n 'empty-description': 'This panel has no modules. Install one and it will appear here.',\n },\n nav: {\n sections: 'Sections',\n menu: 'Menu',\n collapse: 'Collapse the menu',\n language: 'Language',\n },\n}\n","import { computed, createApp, h, ref, type App, type Component } from 'vue'\nimport { createRouter, createWebHistory, type Router, type RouteRecordRaw } from 'vue-router'\nimport { localesKey, WebxUI, type LocaleOption } from '@webx-ui/core'\nimport AdminNav from './AdminNav.vue'\nimport AdminShell from './AdminShell.vue'\nimport { createAdminContext, provideAdmin, type AdminContext } from './admin'\nimport { createHttp, type Http } from './http'\nimport { createI18n, provideI18n, type Dictionary, type I18n, type LocaleDescriptor } from './i18n'\nimport { adminMessages } from './messages'\nimport type { AdminModule, Manifest } from './types'\n\nconst STORED_LOCALE = 'webx.locale'\n\nexport interface CreateAdminOptions {\n /** Where to mount. Defaults to `#webx-app`, which is what the Blade shell renders. */\n el?: string | Element\n /**\n * Where the manifest lives. Defaults to the `webx-manifest` meta tag the Blade shell writes,\n * and to `/api/cms/manifest` when there is none — which is the case under a dev server,\n * where the page is Vite's own index.html.\n */\n manifestUrl?: string\n /**\n * Where the panel's JSON lives, e.g. `/api/cms`. Modules build their own addresses from it.\n * Derived from the manifest URL when not given.\n */\n apiPath?: string\n /** Sections of the panel. */\n modules?: AdminModule[]\n /** Routes that belong to no module: a dashboard, a 404. */\n routes?: RouteRecordRaw[]\n /**\n * Where the panel is served, for the router's history base. Taken from the manifest when it\n * arrives; given here for the first paint, before it has.\n */\n basePath?: string\n /** Replaces the panel's name in the header — a logo, usually. */\n brand?: Component\n /** The corner of the header: who is signed in, and the way out. */\n userMenu?: Component\n /** Extensions that need the router and the context: an auth module, most of all. */\n plugins?: AdminPlugin[]\n /**\n * The language to draw the panel in before the server has been asked. Defaults to the last\n * one used, then to the page's `lang`, then to the browser's. Whatever is chosen, the server\n * narrows it to a language the panel actually has.\n */\n locale?: string\n /** Swappable for tests. */\n http?: Http\n}\n\n/**\n * Something that needs the assembled panel rather than a slot in it — it adds routes, guards\n * the router, or tells the panel how to find out who is signed in.\n */\nexport interface AdminPlugin {\n install(admin: Admin): void\n}\n\nexport interface Admin {\n app: App\n router: Router\n context: AdminContext\n i18n: I18n\n mount(): Promise<void>\n}\n\n/**\n * Assemble the panel.\n *\n * Mounting does not wait for the server. The manifest needs a signed-in session, so a visit\n * that starts at the sign-in screen would otherwise stare at a blank page until a request it\n * is bound to lose comes back.\n */\nexport function createAdmin(options: CreateAdminOptions = {}): Admin {\n const manifestUrl = options.manifestUrl ?? readManifestUrl() ?? '/api/cms/manifest'\n // The Blade shell writes the manifest address rather than the API root, and every module\n // needs the root, so it is read back out of the one thing the page does say.\n const apiPath = options.apiPath ?? manifestUrl.replace(/\\/manifest\\/?$/, '')\n const basePath = options.basePath ?? '/cms'\n const modules = options.modules ?? []\n\n const routes: RouteRecordRaw[] = [...(options.routes ?? [])]\n\n for (const module of modules) {\n routes.push(...(module.routes ?? []))\n }\n\n const router = createRouter({\n history: createWebHistory(basePath),\n routes,\n })\n\n const i18n = createI18n({ locale: options.locale ?? preferredLocale() })\n\n const http =\n options.http ??\n createHttp({\n baseUrl: '',\n onUnauthenticated: () => {\n context.setUser(null)\n context.state.status = 'unauthenticated'\n },\n // Every request says which language the panel is currently showing. It decides what\n // the server writes its own messages in — a 422 under a field — for anybody who has\n // not stored a preference yet, which is everybody until they choose one. Without it,\n // signing in on a Russian sign-in screen lands in an English panel.\n headers: () => ({ 'X-Webx-Locale': i18n.state.locale }),\n })\n\n i18n.defaults('webx-admin', adminMessages)\n\n async function loadDictionary(locale: string): Promise<void> {\n const body = await http.get<{\n data: { locale: string; fallback: string; namespaces: Dictionary }\n }>(`${apiPath}/translations/${locale}`)\n\n i18n.load(body.data.namespaces, body.data.locale, body.data.fallback)\n rememberLocale(body.data.locale)\n markDocumentLanguage(body.data.locale, i18n.state.panelLocales)\n }\n\n const context = createAdminContext({\n http,\n basePath,\n apiPath,\n modules,\n i18n,\n loadDictionary,\n loadManifest: async () => {\n const body = await http.get<{ data: Manifest }>(manifestUrl)\n\n return body.data\n },\n })\n\n const app = createApp(rootComponent(options))\n\n app.use(WebxUI)\n provideAdmin(app, context)\n provideI18n(app, i18n)\n\n /*\n * The languages a localized field offers are the site's *content* languages, not the ones the\n * panel can be drawn in: a panel in English routinely edits a site published in Ukrainian and\n * Russian. They arrive with the manifest, so this is a computed over what is already there\n * rather than a second request — and a form written before they arrive simply has nothing to\n * switch between yet.\n */\n const editing = ref('')\n\n app.provide(localesKey, {\n list: computed<LocaleOption[]>(() =>\n i18n.state.contentLocales.map((locale) => ({\n code: locale.code,\n label: locale.code.toUpperCase(),\n })),\n ),\n active: computed({\n get: () => editing.value || (i18n.state.contentLocales[0]?.code ?? ''),\n set: (code: string) => {\n editing.value = code\n },\n }),\n })\n\n const admin: Admin = {\n app,\n router,\n context,\n i18n,\n async mount() {\n // The one thing worth waiting for. It is a public, cached request, and painting the\n // sign-in screen in English and then swapping every label a moment later looks like a\n // bug rather than like a translation arriving. The manifest is still not waited for —\n // that one needs a session and is bound to 401 for a visitor.\n await Promise.all([loadPanelLocales(), loadDictionary(i18n.state.locale)]).catch(() => {\n // A server that cannot answer these cannot run a panel either, and the built-in\n // English is a better thing to fail with than a blank page.\n })\n\n app.mount(options.el ?? '#webx-app')\n\n await context.reload()\n },\n }\n\n async function loadPanelLocales(): Promise<void> {\n const body = await http.get<{\n data: { panel: LocaleDescriptor[]; content: LocaleDescriptor[] }\n }>(`${apiPath}/locales`)\n\n i18n.state.panelLocales = body.data.panel\n i18n.state.contentLocales = body.data.content\n }\n\n // Plugins go on before the router does, because installing the router is what starts the\n // first navigation. A route added after that is a route the visit already failed to match:\n // opening /login directly would land on nothing while /cms worked, because / matched and the\n // redirect to /login happened later, by which time the route existed.\n for (const plugin of options.plugins ?? []) {\n plugin.install(admin)\n }\n\n app.use(router)\n\n return admin\n}\n\nfunction rootComponent(options: CreateAdminOptions): Component {\n const slots: Record<string, (props: { collapsed?: boolean }) => unknown> = {\n // The menu is the panel's own: it is the manifest, drawn.\n nav: (props) => h(AdminNav, { collapsed: props.collapsed === true }),\n }\n\n if (options.brand !== undefined) {\n slots.brand = () => h(options.brand as Component)\n }\n\n if (options.userMenu !== undefined) {\n slots.user = () => h(options.userMenu as Component)\n }\n\n return { render: () => h(AdminShell, null, slots) }\n}\n\n/**\n * The language to ask for first. A guess, and treated as one — the server answers with the\n * language it actually has, and that is what the panel adopts.\n */\nfunction preferredLocale(): string {\n const remembered = read(STORED_LOCALE)\n\n if (remembered !== null) {\n return remembered\n }\n\n if (typeof document !== 'undefined' && document.documentElement.lang !== '') {\n return document.documentElement.lang\n }\n\n return typeof navigator === 'undefined' ? 'en' : navigator.language\n}\n\nfunction rememberLocale(locale: string): void {\n // A per-browser convenience, so a signed-out reload of the sign-in screen keeps the\n // language. The choice that lasts is the one stored against the administrator.\n try {\n localStorage.setItem(STORED_LOCALE, locale)\n } catch {\n // Private windows, blocked site data. Nothing here is worth an error.\n }\n}\n\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key)\n } catch {\n return null\n }\n}\n\n/** So the browser hyphenates, spell-checks and reads the page aloud in the right language. */\nfunction markDocumentLanguage(locale: string, locales: LocaleDescriptor[]): void {\n if (typeof document === 'undefined') {\n return\n }\n\n document.documentElement.lang = locale\n document.documentElement.dir =\n locales.find((candidate) => candidate.code === locale)?.direction ?? 'ltr'\n}\n\nfunction readManifestUrl(): string | null {\n if (typeof document === 'undefined') {\n return null\n }\n\n const meta = document.querySelector('meta[name=\"webx-manifest\"]')\n\n return meta?.getAttribute('content') ?? null\n}\n"],"names":["adminKey","useAdmin","admin","inject","createAdminContext","options","state","reactive","nav","computed","manifest","entries","module","registered","candidate","path","loadSession","reload","setLocale","error","isUnauthenticated","code","user","loader","permission","provideAdmin","app","i18nKey","useI18n","i18n","useTranslate","namespace","createI18n","provideI18n","fallback","builtIn","dictionary","lookup","source","node","segment","translate","key","params","explicitNamespace","rest","line","fill","messages","next","locale","nextFallback","filled","name","value","emit","__emit","t","router","useRouter","route","useRoute","current","entry","id","_createBlock","_component_wx_menu","$event","__props","_unref","_openBlock","_createElementBlock","_Fragment","_component_wx_menu_item","shellEl","ref","layout","collapsed","showAside","drawerOpen","toggle","close","useResponsiveShell","_createVNode","WxToaster","_hoisted_1","_component_router_view","_hoisted_2","_component_wx_loading","_hoisted_3","_component_wx_result","_component_wx_button","_cache","_component_wx_container","_component_wx_header","_renderSlot","_ctx","_component_wx_action","_component_wx_text","_createTextVNode","_toDisplayString","_component_wx_aside","_component_wx_main","_component_wx_drawer","args","HttpError","message","status","errors","retryAfter","body","UNSAFE","createHttp","baseUrl","csrfUrl","doFetch","onUnauthenticated","standingHeaders","csrfFetched","ensureCsrfCookie","force","readCookie","request","method","retried","unsafe","headers","token","response","url","toError","payload","header","query","full","search","serialised","part","adminMessages","STORED_LOCALE","createAdmin","manifestUrl","readManifestUrl","apiPath","basePath","modules","routes","createRouter","createWebHistory","preferredLocale","http","context","loadDictionary","rememberLocale","markDocumentLanguage","createApp","rootComponent","WebxUI","editing","localesKey","loadPanelLocales","plugin","slots","props","h","AdminNav","AdminShell","remembered","read","locales"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Messages } from './i18n';
|
|
2
|
+
/**
|
|
3
|
+
* The panel's own words, in English.
|
|
4
|
+
*
|
|
5
|
+
* The same keys `webx-ui/module-admin` ships as `lang/en/*.php`, kept here so the package works with
|
|
6
|
+
* no server behind it. Anything the server sends wins; this is the floor, not the source of
|
|
7
|
+
* truth. Translations belong in the Composer package, where one file serves both halves.
|
|
8
|
+
*/
|
|
9
|
+
export declare const adminMessages: Record<string, Messages>;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.wx-admin[data-v-9ef35d4d]{height:100dvh}.wx-admin__screen[data-v-9ef35d4d]{min-width:0;min-height:0}.wx-admin-plain[data-v-9ef35d4d]{display:grid;place-items:center;box-sizing:border-box;min-height:100dvh;padding:var(--wx-space-16);background:var(--wx-bg-body)}html:has(>body>#webx-app),body:has(>#webx-app){margin:0}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import { LocaleDescriptor } from './i18n';
|
|
3
|
+
/**
|
|
4
|
+
* What `GET /api/cms/manifest` answers with — the panel's own description of itself, and the
|
|
5
|
+
* first thing the front end asks for. Mirrors `WebxUi\Admin\Manifest\ManifestBuilder`.
|
|
6
|
+
*/
|
|
7
|
+
export interface Manifest {
|
|
8
|
+
title: string;
|
|
9
|
+
/** Where the panel is served, e.g. `/cms`. Becomes the router's base. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Where its JSON lives, e.g. `/api/cms`. */
|
|
12
|
+
apiPath: string;
|
|
13
|
+
/** The language this administrator reads the panel in — their choice, not the site's. */
|
|
14
|
+
locale: string;
|
|
15
|
+
/** The languages the site publishes content in. Editing screens are built around this. */
|
|
16
|
+
locales: LocaleDescriptor[];
|
|
17
|
+
/** The languages the interface itself can be switched to. */
|
|
18
|
+
panelLocales: LocaleDescriptor[];
|
|
19
|
+
modules: ManifestModule[];
|
|
20
|
+
}
|
|
21
|
+
export interface ManifestModule {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
icon: string | null;
|
|
25
|
+
order: number;
|
|
26
|
+
permissions: string[];
|
|
27
|
+
/** Whatever the server-side module wanted to say, in its own room. */
|
|
28
|
+
meta: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whoever is signed in. Filled in by an auth module — this package defines the shape and
|
|
32
|
+
* answers `can()` from it, but knows nothing about how anybody signs in.
|
|
33
|
+
*/
|
|
34
|
+
export interface AdminUser {
|
|
35
|
+
id: number | string;
|
|
36
|
+
name: string;
|
|
37
|
+
email: string;
|
|
38
|
+
isSuper: boolean;
|
|
39
|
+
permissions: string[];
|
|
40
|
+
/** The panel language they chose, or null if they never have. */
|
|
41
|
+
locale?: string | null;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A section of the panel, on the front end.
|
|
46
|
+
*
|
|
47
|
+
* Pairs with a module on the server by `id`: the server says a module exists and what it is
|
|
48
|
+
* called, this says what it looks like. A front-end module the server does not report is not
|
|
49
|
+
* shown — the panel is whatever the installation actually has.
|
|
50
|
+
*/
|
|
51
|
+
export interface AdminModule {
|
|
52
|
+
/** Same id the server-side module answers to. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Routes mounted under the panel's base path. */
|
|
55
|
+
routes?: RouteRecordRaw[];
|
|
56
|
+
/** Where the navigation entry points. Defaults to the first route's path. */
|
|
57
|
+
path?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Shown before the manifest has arrived, and for routes outside it — the sign-in screen is
|
|
60
|
+
* the reason this exists.
|
|
61
|
+
*/
|
|
62
|
+
public?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export type AdminStatus = 'loading' | 'ready' | 'unauthenticated' | 'error';
|
|
65
|
+
export interface NavEntry {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
icon: string | null;
|
|
69
|
+
path: string;
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webx-ui/module-admin",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "The frame a WebX UI admin panel runs in: bootstrap, the shell, the HTTP client and the module registry that pairs with webx-ui/module-admin on the server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"webx-ui",
|
|
12
|
+
"vue",
|
|
13
|
+
"vue3",
|
|
14
|
+
"admin",
|
|
15
|
+
"cms",
|
|
16
|
+
"laravel"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/webx-ui/webx-ui.git",
|
|
21
|
+
"directory": "packages/module-admin"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://webx-ui.github.io/webx-ui/guide/roadmap.html",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./style.css": "./dist/style.css",
|
|
39
|
+
"./dist/style.css": "./dist/style.css",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"vue": "^3.5.0",
|
|
44
|
+
"vue-router": "^4.5.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@webx-ui/core": "^0.16.0",
|
|
48
|
+
"@webx-ui/tokens": "^0.2.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.10.1",
|
|
52
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^7.1.14",
|
|
55
|
+
"vite-plugin-dts": "^4.5.4",
|
|
56
|
+
"vue": "^3.5.24",
|
|
57
|
+
"vue-router": "^4.5.1",
|
|
58
|
+
"vue-tsc": "^3.1.3"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "vite build",
|
|
62
|
+
"dev": "vite build --watch",
|
|
63
|
+
"typecheck": "vue-tsc -p tsconfig.json --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|