@webx-ui/module-admin 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminNav.d.ts +3 -0
- package/dist/Screen.d.ts +39 -0
- package/dist/admin.d.ts +18 -1
- package/dist/createAdmin.d.ts +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +415 -307
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +27 -0
- package/package.json +3 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +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":";;;AA4CO,MAAMA,2BAA8C,YAAY;AAEhE,SAASC,IAAyB;AACvC,QAAMC,IAAQC,EAAOH,GAAU,IAAI;AAEnC,MAAIE,MAAU;AACZ,UAAM,IAAI,MAAM,iEAAiE;AAGnF,SAAOA;AACT;AAOO,SAASE,GAAmBC,GAQlB;AACf,QAAMC,IAAQC,EAAqB;AAAA,IACjC,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,EAAA,CACR,GAEKC,IAAMC,EAAqB,MAAM;AACrC,UAAMC,IAAWJ,EAAM;AAEvB,QAAII,MAAa;AACf,aAAO,CAAA;AAGT,UAAMC,IAAsB,CAAA;AAE5B,eAAWC,KAAUF,EAAS,SAAS;AACrC,YAAMG,IAAaR,EAAQ,QAAQ,KAAK,CAACS,MAAcA,EAAU,OAAOF,EAAO,EAAE;AAKjF,UAAIC,MAAe;AACjB;AAGF,YAAME,IAAOF,EAAW,QAAQA,EAAW,SAAS,CAAC,GAAG;AAExD,MAAIE,MAAS,UAIbJ,EAAQ,KAAK;AAAA,QACX,IAAIC,EAAO;AAAA,QACX,OAAOA,EAAO;AAAA,QACd,MAAMA,EAAO;AAAA,QACb,MAAAG;AAAA,MAAA,CACD;AAAA,IACH;AAEA,WAAOJ;AAAA,EACT,CAAC;AAED,MAAIK,IAAwD;AAE5D,iBAAeC,IAAwB;AACrC,IAAAX,EAAM,SAAS,WACfA,EAAM,QAAQ;AAEd,QAAI;AACF,UAAIU,MAAgB,SAClBV,EAAM,OAAO,MAAMU,EAAA,GAIfV,EAAM,SAAS,OAAM;AACvB,QAAAA,EAAM,WAAW,MACjBA,EAAM,SAAS;AAEf;AAAA,MACF;AAGF,YAAMI,IAAW,MAAML,EAAQ,aAAA;AAE/B,MAAAC,EAAM,WAAWI,GACjBL,EAAQ,KAAK,MAAM,iBAAiBK,EAAS,WAAW,CAAA,GACxDL,EAAQ,KAAK,MAAM,eAAeK,EAAS,gBAAgBL,EAAQ,KAAK,MAAM,cAI1EK,EAAS,WAAW,UAAaA,EAAS,WAAWL,EAAQ,KAAK,MAAM,UAC1E,MAAMa,EAAUR,EAAS,MAAM,GAGjCJ,EAAM,SAAS;AAAA,IACjB,SAASa,GAAO;AAGd,UAAIC,GAAkBD,CAAK,GAAG;AAC5B,QAAAb,EAAM,WAAW,MACjBA,EAAM,OAAO,MACbA,EAAM,SAAS;AAEf;AAAA,MACF;AAEA,MAAAA,EAAM,QAAQa,aAAiB,QAAQA,EAAM,UAAU,OAAOA,CAAK,GACnEb,EAAM,SAAS;AAAA,IACjB;AAAA,EACF;AAEA,iBAAeY,EAAUG,GAA6B;AACpD,QAAIhB,EAAQ,mBAAmB,QAAW;AACxC,MAAAA,EAAQ,KAAK,MAAM,SAASgB;AAE5B;AAAA,IACF;AAUA,QARA,MAAMhB,EAAQ,eAAegB,CAAI,GAQ7Bf,EAAM,aAAa;AACrB,UAAI;AACF,QAAAA,EAAM,WAAW,MAAMD,EAAQ,aAAA;AAAA,MACjC,QAAQ;AAAA,MAGR;AAAA,EAEJ;AAEA,SAAO;AAAA,IACL,MAAMA,EAAQ;AAAA,IACd,UAAUA,EAAQ;AAAA,IAClB,SAASA,EAAQ;AAAA,IACjB,OAAAC;AAAA,IACA,MAAMD,EAAQ;AAAA,IACd,SAASA,EAAQ;AAAA,IACjB,KAAAG;AAAA,IACA,QAAAS;AAAA,IACA,WAAAC;AAAA,IACA,QAAQI,GAAM;AACZ,MAAAhB,EAAM,OAAOgB;AAAA,IACf;AAAA,IACA,iBAAiBC,GAAQ;AACvB,MAAAP,IAAcO;AAAA,IAChB;AAAA,IACA,IAAIC,GAAY;AACd,YAAMF,IAAOhB,EAAM;AAEnB,aAAIgB,MAAS,OACJ,KAGFA,EAAK,WAAWA,EAAK,YAAY,SAASE,CAAU;AAAA,IAC7D;AAAA,EAAA;AAEJ;AAEO,SAASC,GAAaC,GAAUxB,GAA2B;AAChE,EAAAwB,EAAI,QAAQ1B,GAAUE,CAAK;AAC7B;AAEA,SAASkB,GAAkBD,GAAyB;AAClD,SACE,OAAOA,KAAU,YACjBA,MAAU,QACV,YAAYA,KACXA,EAA8B,WAAW;AAE9C;AC/KO,MAAMQ,2BAAqC,WAAW;AAEtD,SAASC,KAAgB;AAC9B,QAAMC,IAAO1B,EAAOwB,GAAS,IAAI;AAEjC,MAAIE,MAAS;AACX,UAAM,IAAI,MAAM,gEAAgE;AAGlF,SAAOA;AACT;AAMO,SAASC,EAAaC,GAA8B;AACzD,QAAMF,IAAO1B,EAAOwB,GAAS,IAAI;AAEjC,SAAOE,MAAS,OAAOG,IAAa,MAAMD,CAAS,IAAIF,EAAK,MAAME,CAAS;AAC7E;AAEO,SAASE,GAAYP,GAAUG,GAAkB;AACtD,EAAAH,EAAI,QAAQC,GAASE,CAAI;AAC3B;AAEO,SAASG,EAAW3B,IAAkD,IAAU;AACrF,QAAM6B,IAAW7B,EAAQ,YAAY,MAE/BC,IAAQC,EAAoB;AAAA,IAChC,QAAQF,EAAQ,UAAU6B;AAAA,IAC1B,UAAAA;AAAA,IACA,cAAc,CAAA;AAAA,IACd,gBAAgB,CAAA;AAAA,EAAC,CAClB,GAIKC,IAAsB,CAAA,GACtBC,IAAa7B,EAAgC,EAAE,OAAO,CAAA,GAAI;AAEhE,WAAS8B,EAAOC,GAAoBP,GAAmBhB,GAA+B;AACpF,QAAIwB,IAAsCD,EAAOP,CAAS,IAAIhB,EAAK,CAAC,KAAK,EAAE;AAE3E,eAAWyB,KAAWzB,EAAK,MAAM,CAAC,GAAG;AACnC,UAAI,OAAOwB,KAAS,YAAYA,MAAS;AACvC,eAAO;AAGT,MAAAA,IAAOA,EAAKC,CAAO;AAAA,IACrB;AAEA,WAAO,OAAOD,KAAS,WAAWA,IAAO;AAAA,EAC3C;AAEA,WAASE,EACPV,GACAW,GACAC,GACQ;AAGR,UAAM,CAACC,GAAmBC,CAAI,IAAIH,EAAI,SAAS,IAAI,IAC9CA,EAAI,MAAM,MAAM,CAAC,IAClB,CAACX,GAAWW,CAAG,GAEb3B,IAAO8B,EAAK,MAAM,GAAG,GAErBC,IACJT,EAAOD,EAAW,OAAOQ,GAAmB7B,CAAI,KAChDsB,EAAOF,GAASS,GAAmB7B,CAAI;AAAA;AAAA,IAGvC2B;AAEF,WAAOC,MAAW,SAAYG,IAAOC,GAAKD,GAAMH,CAAM;AAAA,EACxD;AAEA,SAAO;AAAA,IACL,OAAArC;AAAA,IACA,SAASyB,GAAWiB,GAAU;AAC5B,MAAAb,EAAQJ,CAAS,IAAI,EAAE,GAAGI,EAAQJ,CAAS,GAAG,GAAGiB,EAAA;AAAA,IACnD;AAAA,IACA,KAAKC,GAAMC,GAAQC,GAAc;AAG/B,MAAAf,EAAW,QAAQa,KAAQ,CAAA,GAC3B3C,EAAM,SAAS4C,KAAU5C,EAAM,QAE3B6C,MAAiB,WACnB7C,EAAM,WAAW6C;AAAA,IAErB;AAAA,IACA,MAAMpB,GAAW;AACf,aAAO,CAACW,GAAKC,MAAWF,EAAUV,GAAWW,GAAKC,CAAM;AAAA,IAC1D;AAAA,IACA,GAAG,CAACD,GAAKC,MAAWF,EAAU,IAAIC,GAAKC,CAAM;AAAA,EAAA;AAEjD;AAMA,SAASI,GAAKD,GAAcH,GAAiD;AAC3E,MAAIS,IAASN;AAEb,aAAW,CAACO,GAAMC,CAAK,KAAK,OAAO,QAAQX,CAAM;AAC/C,IAAAS,IAASA,EAAO,WAAW,IAAIC,CAAI,IAAI,OAAOC,CAAK,CAAC;AAGtD,SAAOF;AACT;;;;;;;;ACtJA,UAAMG,IAAOC,GAEPtD,IAAQD,EAAA,GACRwD,IAAI3B,EAAa,YAAY,GAC7B4B,IAASC,EAAA,GACTC,IAAQC,EAAA,GAERC,IAAUrD,EAAiB;AAAA,MAC/B,KAAK,MACWP,EAAM,IAAI,MAAM,KAAK,CAAC6D,MAAUH,EAAM,KAAK,WAAWG,EAAM,IAAI,CAAC,GAEjE,MAAM;AAAA,MAEtB,KAAK,CAACC,MAAO;AACX,cAAMD,IAAQ7D,EAAM,IAAI,MAAM,KAAK,CAACY,MAAcA,EAAU,OAAOkD,CAAE;AAErE,QAAID,MAAU,UACPL,EAAO,KAAKK,EAAM,IAAI;AAAA,MAE/B;AAAA,IAAA,CACD;;;kBAICE,EAaUC,GAAA;AAAA,oBAZCJ,EAAA;AAAA,sDAAAA,EAAO,QAAAK;AAAA,QACf,WAAWC,EAAA;AAAA,QACX,OAAOC,EAAAZ,CAAA,EAAC,cAAA;AAAA,QACR,iCAAQF,EAAI,QAAA;AAAA,MAAA;mBAGX,MAAgC;AAAA,WADlCe,EAAA,EAAA,GAAAC,EAMEC,WALgBH,EAAAnE,CAAA,EAAM,IAAI,QAAnB6D,YADTE,EAMEQ,GAAA;AAAA,YAJC,KAAKV,EAAM;AAAA,YACX,OAAOA,EAAM;AAAA,YACb,MAAMA,EAAM,QAAQ;AAAA,YACpB,OAAOA,EAAM;AAAA,UAAA;;;;;;;;;;;;;;;;;;AC/BpB,UAAM7D,IAAQD,EAAA,GACRwD,IAAI3B,EAAa,YAAY,GAC7B4C,IAAUC,EAAwB,IAAI,GAEtC,EAAE,QAAAC,GAAQ,WAAAC,GAAW,WAAAC,GAAW,YAAAC,GAAY,QAAAC,GAAQ,OAAAC,EAAA,IAAUC,GAAmBR,GAAS;AAAA,MAC9F,SAAS;AAAA,IAAA,CACV;;;;QAICS,EAAcd,EAAAe,EAAA,CAAA;AAAA,QAEHf,EAAAnE,CAAA,EAAM,MAAM,WAAM,qBAA7BoE,KAAAC,EAEM,OAFNc,IAEM;AAAA,UADJF,EAAeG,CAAA;AAAA,QAAA,MAGDjB,EAAAnE,CAAA,EAAM,MAAM,WAAM,aAAlCoE,EAAA,GAAAC,EAEM,OAFNgB,IAEM;AAAA,UADJJ,EAA0CK,GAAA;AAAA,YAA7B,OAAOnB,EAAAZ,CAAA,EAAC,eAAA;AAAA,UAAA;cAGPY,EAAAnE,CAAA,EAAM,MAAM,WAAM,WAAlCoE,EAAA,GAAAC,EAIM,OAJNkB,IAIM;AAAA,UAHJN,EAEYO,GAAA;AAAA,YAFD,QAAO;AAAA,YAAS,OAAOrB,EAAAZ,CAAA,EAAC,mBAAA;AAAA,YAAwB,aAAaY,EAAAnE,CAAA,EAAM,MAAM;AAAA,UAAA;uBAClF,MAAoF;AAAA,cAApFiF,EAAoFQ,GAAA;AAAA,gBAAzE,MAAK;AAAA,gBAAW,SAAKC,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAAzB,MAAEE,EAAAnE,CAAA,EAAM,OAAA;AAAA,cAAM;2BAAI,MAAsB;AAAA,sBAAnBmE,EAAAZ,CAAA,EAAC,aAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;;;;oBAI1Dc,EAgCM,OAAA;AAAA;mBAhCU;AAAA,UAAJ,KAAIG;AAAA,UAAU,OAAM;AAAA,QAAA;UAC9BS,EA0BeU,GAAA,EA1BD,UAAA,MAAQ;AAAA,uBACpB,MAcY;AAAA,cAdZV,EAcYW,GAAA,MAAA;AAAA,gBAHC,OACT,MAAoB;AAAA,kBAApBC,EAAoBC,EAAA,QAAA,QAAA,CAAA,GAAA,QAAA,EAAA;AAAA,gBAAA;2BAXtB,MAIE;AAAA,kBAJFb,EAIEc,GAAA;AAAA,oBAHC,MAAM5B,EAAAO,CAAA,MAAM,WAAA,SAAA;AAAA,oBACZ,OAAOP,EAAAO,CAAA,MAAM,WAAgBP,EAAAZ,CAAA,gBAAgBY,EAAAZ,CAAA,EAAC,cAAA;AAAA,oBAC9C,SAAOY,EAAAW,CAAA;AAAA,kBAAA;kBAGVe,EAEOC,uBAFP,MAEO;AAAA,oBADLb,EAAsEe,GAAA,EAA7D,QAAO,cAAU;AAAA,iCAAC,MAAiC;AAAA,wBAA9BC,EAAAC,EAAA/B,EAAAnE,CAAA,EAAM,MAAM,UAAU,KAAK,GAAA,CAAA;AAAA,sBAAA;;;;;;;cAQ7DiF,EAQeU,GAAA,EARD,WAAU,gBAAY;AAAA,2BAClC,MAEW;AAAA,kBAFKxB,EAAAS,CAAA,UAAhBb,EAEWoC,GAAA;AAAA;oBAFiB,WAAWhC,EAAAQ,CAAA;AAAA,oBAAY,OAAO;AAAA,oBAAK,QAAA;AAAA,kBAAA;+BAC7D,MAA0C;AAAA,sBAA1CkB,EAA0CC,EAAA,QAAA,OAAA,EAAxB,WAAW3B,EAAAQ,CAAA,KAAS,QAAA,EAAA;AAAA,oBAAA;;;kBAGxCM,EAEUmB,GAAA;AAAA,oBAFD,SAAQ;AAAA,oBAAK,QAAA;AAAA,oBAAO,OAAM;AAAA,kBAAA;+BACjC,MAAe;AAAA,sBAAfnB,EAAeG,CAAA;AAAA,oBAAA;;;;;;;;;UAKrBH,EAEYoB,GAAA;AAAA,YAFO,MAAMlC,EAAAU,CAAA;AAAA,2DAAAA,EAAU,QAAAZ,IAAA;AAAA,YAAG,OAAOE,EAAAZ,CAAA,EAAC,UAAA;AAAA,YAAc,MAAK;AAAA,YAAQ,MAAM;AAAA,YAAK,UAAA;AAAA,UAAA;uBAClF,MAAsD;AAAA,cAAtDsC,EAAsDC,EAAA,QAAA,OAAA;AAAA,gBAApC,WAAW;AAAA,gBAAQ,UAAMJ,EAAA,CAAA,MAAAA,EAAA,CAAA;AAAA,0BAAEvB,EAAAY,CAAA,KAAAZ,EAAAY,CAAA,EAAA,GAAAuB,CAAA;AAAA,cAAA;;;;;;;;;;;;;;ACtC5C,MAAMC,WAAkB,MAAM;AAAA,EACnC,YACEC,GACSC,GACAC,IAAmC,CAAA,GACnCC,IAA4B,MAC5BC,IAAgB,MACzB;AACA,UAAMJ,CAAO,GALJ,KAAA,SAAAC,GACA,KAAA,SAAAC,GACA,KAAA,aAAAC,GACA,KAAA,OAAAC,GAGT,KAAK,OAAO;AAAA,EACd;AAAA,EAPW;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAOX,IAAI,eAAwB;AAC1B,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,oBAA6B;AAC/B,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,cAAuB;AACzB,WAAO,KAAK,WAAW;AAAA,EACzB;AACF;AAUA,MAAMC,yBAAa,IAAI,CAAC,QAAQ,OAAO,SAAS,QAAQ,CAAC;AAElD,SAASC,GAAW3G,IAAuB,IAAU;AAC1D,QAAM4G,KAAW5G,EAAQ,WAAW,IAAI,QAAQ,OAAO,EAAE,GACnD6G,IAAU7G,EAAQ,WAAW,wBAC7B8G,IAAU9G,EAAQ,SAAS,WAAW,MAAM,KAAK,UAAU,GAC3D+G,IAAoB/G,EAAQ,mBAC5BgH,IAAkBhH,EAAQ;AAEhC,MAAIiH,IAAc;AAElB,iBAAeC,EAAiBC,IAAQ,IAAsB;AAC5D,IAAIF,KAAe,CAACE,KAASC,EAAW,YAAY,MAAM,SAI1D,MAAMN,EAAQD,GAAS,EAAE,aAAa,eAAe,GACrDI,IAAc;AAAA,EAChB;AAEA,iBAAeI,EACbC,GACA5G,GACA+F,GACAzG,IAA0B,CAAA,GAC1BuH,IAAU,IACE;AACZ,UAAMC,IAASd,GAAO,IAAIY,CAAM;AAEhC,IAAIE,KACF,MAAMN,EAAA;AAGR,UAAMO,IAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,oBAAoB;AAAA;AAAA,MAEpB,GAAGT,IAAA;AAAA,MACH,GAAGhH,EAAQ;AAAA,IAAA;AAGb,QAAIwH,GAAQ;AACV,YAAME,IAAQN,EAAW,YAAY;AAErC,MAAIM,MAAU,SACZD,EAAQ,cAAc,IAAIC;AAAA,IAE9B;AAEA,IAAIjB,MAAS,WACXgB,EAAQ,cAAc,IAAI;AAG5B,UAAME,IAAW,MAAMb,EAAQc,GAAIhB,GAASlG,GAAMV,EAAQ,KAAK,GAAG;AAAA,MAChE,QAAAsH;AAAA,MACA,aAAa;AAAA,MACb,SAAAG;AAAA,MACA,QAAQzH,EAAQ;AAAA,MAChB,MAAMyG,MAAS,SAAY,SAAY,KAAK,UAAUA,CAAI;AAAA,IAAA,CAC3D;AAKD,QAAIkB,EAAS,WAAW,OAAOH,KAAU,CAACD;AACxC,mBAAML,EAAiB,EAAI,GAEpBG,EAAWC,GAAQ5G,GAAM+F,GAAMzG,GAAS,EAAI;AAOrD,QAJI2H,EAAS,WAAW,OACtBZ,IAAA,GAGE,CAACY,EAAS;AACZ,YAAM,MAAME,GAAQF,CAAQ;AAG9B,QAAIA,EAAS,WAAW;AAIxB,aAAQ,MAAMA,EAAS,KAAA;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,KAAK,CAACjH,GAAMV,MAAYqH,EAAQ,OAAO3G,GAAM,QAAWV,CAAO;AAAA,IAC/D,MAAM,CAACU,GAAM+F,GAAMzG,MAAYqH,EAAQ,QAAQ3G,GAAM+F,GAAMzG,CAAO;AAAA,IAClE,KAAK,CAACU,GAAM+F,GAAMzG,MAAYqH,EAAQ,OAAO3G,GAAM+F,GAAMzG,CAAO;AAAA,IAChE,OAAO,CAACU,GAAM+F,GAAMzG,MAAYqH,EAAQ,SAAS3G,GAAM+F,GAAMzG,CAAO;AAAA,IACpE,QAAQ,CAACU,GAAMV,MAAYqH,EAAQ,UAAU3G,GAAM,QAAWV,CAAO;AAAA,EAAA;AAEzE;AAEA,eAAe6H,GAAQF,GAAwC;AAC7D,MAAIlB,IAAgB;AAEpB,MAAI;AACF,IAAAA,IAAO,MAAMkB,EAAS,KAAA;AAAA,EACxB,QAAQ;AAAA,EAER;AAEA,QAAMG,IAAWrB,KAAQ,CAAA,GACnBJ,IACJ,OAAOyB,EAAQ,WAAY,YAAYA,EAAQ,YAAY,KACvDA,EAAQ,UACRH,EAAS,cAAc,uBAAuBA,EAAS,MAAM,IAE7DpB,IACJuB,EAAQ,WAAW,QAAQ,OAAOA,EAAQ,UAAW,WAChDA,EAAQ,SACT,CAAA,GAEAC,IAASJ,EAAS,QAAQ,IAAI,aAAa,GAC3CnB,IAAauB,MAAW,OAAO,OAAO,OAAO,SAASA,GAAQ,EAAE;AAEtE,SAAO,IAAI3B;AAAA,IACTC;AAAA,IACAsB,EAAS;AAAA,IACTpB;AAAA,IACA,OAAO,SAASC,CAAU,IAAIA,IAAa;AAAA,IAC3CC;AAAA,EAAA;AAEJ;AAEA,SAASmB,GAAIhB,GAAiBlG,GAAcsH,GAAyC;AAEnF,QAAMC,IADW,gBAAgB,KAAKvH,CAAI,IAClBA,IAAO,GAAGkG,CAAO,IAAIlG,EAAK,QAAQ,OAAO,EAAE,CAAC;AAEpE,MAAIsH,MAAU;AACZ,WAAOC;AAGT,QAAMC,IAAS,IAAI,gBAAA;AAEnB,aAAW,CAAC7F,GAAKY,CAAK,KAAK,OAAO,QAAQ+E,CAAK;AAC7C,IAA2B/E,KAAU,QACnCiF,EAAO,IAAI7F,GAAK,OAAOY,CAAK,CAAC;AAIjC,QAAMkF,IAAaD,EAAO,SAAA;AAE1B,SAAOC,MAAe,KAAKF,IAAO,GAAGA,CAAI,GAAGA,EAAK,SAAS,GAAG,IAAI,MAAM,GAAG,GAAGE,CAAU;AACzF;AAKO,SAASf,EAAWpE,GAA6B;AACtD,MAAI,OAAO,WAAa;AACtB,WAAO;AAGT,aAAWoF,KAAQ,SAAS,OAAO,MAAM,GAAG,GAAG;AAC7C,UAAM,CAAC/F,GAAK,GAAGG,CAAI,IAAI4F,EAAK,KAAA,EAAO,MAAM,GAAG;AAE5C,QAAI/F,MAAQW;AACV,aAAO,mBAAmBR,EAAK,KAAK,GAAG,CAAC;AAAA,EAE5C;AAEA,SAAO;AACT;AChOO,MAAM6F,KAA0C;AAAA,EACrD,OAAO;AAAA,IACL,SAAS;AAAA,IACT,eAAe;AAAA,IACf,OAAO;AAAA,IACP,eAAe;AAAA,IACf,qBAAqB;AAAA,EAAA;AAAA,EAEvB,KAAK;AAAA,IACH,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,UAAU;AAAA,EAAA;AAEd,GCZMC,IAAgB;AAgEf,SAASC,GAAYvI,IAA8B,IAAW;AACnE,QAAMwI,IAAcxI,EAAQ,eAAeyI,GAAA,KAAqB,qBAG1DC,IAAU1I,EAAQ,WAAWwI,EAAY,QAAQ,kBAAkB,EAAE,GACrEG,IAAW3I,EAAQ,YAAY,QAC/B4I,IAAU5I,EAAQ,WAAW,CAAA,GAE7B6I,IAA2B,CAAC,GAAI7I,EAAQ,UAAU,CAAA,CAAG;AAE3D,aAAWO,KAAUqI;AACnB,IAAAC,EAAO,KAAK,GAAItI,EAAO,UAAU,CAAA,CAAG;AAGtC,QAAM8C,IAASyF,GAAa;AAAA,IAC1B,SAASC,GAAiBJ,CAAQ;AAAA,IAClC,QAAAE;AAAA,EAAA,CACD,GAEKrH,IAAOG,EAAW,EAAE,QAAQ3B,EAAQ,UAAUgJ,GAAA,GAAmB,GAEjEC,IACJjJ,EAAQ,QACR2G,GAAW;AAAA,IACT,SAAS;AAAA,IACT,mBAAmB,MAAM;AACvB,MAAAuC,EAAQ,QAAQ,IAAI,GACpBA,EAAQ,MAAM,SAAS;AAAA,IACzB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,SAAS,OAAO,EAAE,iBAAiB1H,EAAK,MAAM,OAAA;AAAA,EAAO,CACtD;AAEH,EAAAA,EAAK,SAAS,cAAc6G,EAAa;AAEzC,iBAAec,EAAetG,GAA+B;AAC3D,UAAM4D,IAAO,MAAMwC,EAAK,IAErB,GAAGP,CAAO,iBAAiB7F,CAAM,EAAE;AAEtC,IAAArB,EAAK,KAAKiF,EAAK,KAAK,YAAYA,EAAK,KAAK,QAAQA,EAAK,KAAK,QAAQ,GACpE2C,GAAe3C,EAAK,KAAK,MAAM,GAC/B4C,GAAqB5C,EAAK,KAAK,QAAQjF,EAAK,MAAM,YAAY;AAAA,EAChE;AAEA,QAAM0H,IAAUnJ,GAAmB;AAAA,IACjC,MAAAkJ;AAAA,IACA,UAAAN;AAAA,IACA,SAAAD;AAAA,IACA,SAAAE;AAAA,IACA,MAAApH;AAAA,IACA,gBAAA2H;AAAA,IACA,cAAc,aACC,MAAMF,EAAK,IAAwBT,CAAW,GAE/C;AAAA,EACd,CACD,GAEKnH,IAAMiI,EAAUC,GAAcvJ,CAAO,CAAC;AAE5C,EAAAqB,EAAI,IAAImI,EAAM,GACdpI,GAAaC,GAAK6H,CAAO,GACzBtH,GAAYP,GAAKG,CAAI;AASrB,QAAMiI,IAAUnF,EAAI,EAAE;AAEtB,EAAAjD,EAAI,QAAQqI,IAAY;AAAA,IACtB,MAAMtJ;AAAA,MAAyB,MAC7BoB,EAAK,MAAM,eAAe,IAAI,CAACqB,OAAY;AAAA,QACzC,MAAMA,EAAO;AAAA,QACb,OAAOA,EAAO,KAAK,YAAA;AAAA,MAAY,EAC/B;AAAA,IAAA;AAAA,IAEJ,QAAQzC,EAAS;AAAA,MACf,KAAK,MAAMqJ,EAAQ,UAAUjI,EAAK,MAAM,eAAe,CAAC,GAAG,QAAQ;AAAA,MACnE,KAAK,CAACR,MAAiB;AACrB,QAAAyI,EAAQ,QAAQzI;AAAA,MAClB;AAAA,IAAA,CACD;AAAA,EAAA,CACF;AAED,QAAMnB,IAAe;AAAA,IACnB,KAAAwB;AAAA,IACA,QAAAgC;AAAA,IACA,SAAA6F;AAAA,IACA,MAAA1H;AAAA,IACA,MAAM,QAAQ;AAKZ,YAAM,QAAQ,IAAI,CAACmI,EAAA,GAAoBR,EAAe3H,EAAK,MAAM,MAAM,CAAC,CAAC,EAAE,MAAM,MAAM;AAAA,MAGvF,CAAC,GAEDH,EAAI,MAAMrB,EAAQ,MAAM,WAAW,GAEnC,MAAMkJ,EAAQ,OAAA;AAAA,IAChB;AAAA,EAAA;AAGF,iBAAeS,IAAkC;AAC/C,UAAMlD,IAAO,MAAMwC,EAAK,IAErB,GAAGP,CAAO,UAAU;AAEvB,IAAAlH,EAAK,MAAM,eAAeiF,EAAK,KAAK,OACpCjF,EAAK,MAAM,iBAAiBiF,EAAK,KAAK;AAAA,EACxC;AAMA,aAAWmD,KAAU5J,EAAQ,WAAW,CAAA;AACtC,IAAA4J,EAAO,QAAQ/J,CAAK;AAGtB,SAAAwB,EAAI,IAAIgC,CAAM,GAEPxD;AACT;AAEA,SAAS0J,GAAcvJ,GAAwC;AAC7D,QAAM6J,IAAqE;AAAA;AAAA,IAEzE,KAAK,CAACC,MAAUC,EAAEC,IAAU,EAAE,WAAWF,EAAM,cAAc,GAAA,CAAM;AAAA,EAAA;AAGrE,SAAI9J,EAAQ,UAAU,WACpB6J,EAAM,QAAQ,MAAME,EAAE/J,EAAQ,KAAkB,IAG9CA,EAAQ,aAAa,WACvB6J,EAAM,OAAO,MAAME,EAAE/J,EAAQ,QAAqB,IAG7C,EAAE,QAAQ,MAAM+J,EAAEE,IAAY,MAAMJ,CAAK,EAAA;AAClD;AAMA,SAASb,KAA0B;AACjC,QAAMkB,IAAaC,GAAK7B,CAAa;AAErC,SAAI4B,MAAe,OACVA,IAGL,OAAO,WAAa,OAAe,SAAS,gBAAgB,SAAS,KAChE,SAAS,gBAAgB,OAG3B,OAAO,YAAc,MAAc,OAAO,UAAU;AAC7D;AAEA,SAASd,GAAevG,GAAsB;AAG5C,MAAI;AACF,iBAAa,QAAQyF,GAAezF,CAAM;AAAA,EAC5C,QAAQ;AAAA,EAER;AACF;AAEA,SAASsH,GAAK9H,GAA4B;AACxC,MAAI;AACF,WAAO,aAAa,QAAQA,CAAG;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAASgH,GAAqBxG,GAAgBuH,GAAmC;AAC/E,EAAI,OAAO,WAAa,QAIxB,SAAS,gBAAgB,OAAOvH,GAChC,SAAS,gBAAgB,MACvBuH,EAAQ,KAAK,CAAC3J,MAAcA,EAAU,SAASoC,CAAM,GAAG,aAAa;AACzE;AAEA,SAAS4F,KAAiC;AACxC,SAAI,OAAO,WAAa,MACf,OAGI,SAAS,cAAc,4BAA4B,GAEnD,aAAa,SAAS,KAAK;AAC1C;"}
|
|
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","../src/Screen.vue"],"sourcesContent":["import { computed, inject, reactive, type App, type ComputedRef, type InjectionKey } from 'vue'\nimport type { Patch, ScreenNode, TypeRegistry } from '@webx-ui/schema'\nimport type { Http } from './http'\nimport type { I18n } from './i18n'\nimport type { AdminModule, AdminStatus, AdminUser, Manifest, NavEntry, NavGroup } 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 /** The same navigation, with the top-level entries first and then each group's. */\n readonly groups: ComputedRef<{ top: NavEntry[]; groups: NavGroup[] }>\n /** Node types every screen is drawn with: the modules' and the project's, over the core. */\n readonly types: TypeRegistry\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 * A screen by name, as the server hands it out — patched, cut to this administrator's\n * permissions, translated. Fetched once per name and language for the session.\n */\n loadScreen(name: string): Promise<ScreenNode[]>\n /** The project's own operations over a screen, from `createAdmin({ screens })`. */\n screenPatch(name: string): Patch\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 types?: TypeRegistry\n screens?: Record<string, Patch>\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 group: module.group ?? null,\n })\n }\n\n return entries\n })\n\n const groups = computed(() => {\n const declared = state.manifest?.groups ?? []\n const top: NavEntry[] = []\n const byGroup = new Map<string, NavEntry[]>()\n\n for (const entry of nav.value) {\n // A group the server never declared is not a group: the entry stays at the top rather\n // than vanishing under a heading nobody can name.\n if (entry.group !== null && declared.some((group) => group.id === entry.group)) {\n const list = byGroup.get(entry.group) ?? []\n list.push(entry)\n byGroup.set(entry.group, list)\n } else {\n top.push(entry)\n }\n }\n\n return {\n top,\n groups: declared\n .filter((group) => byGroup.has(group.id))\n .map((group) => ({\n id: group.id,\n title: group.title,\n entries: byGroup.get(group.id) ?? [],\n })),\n }\n })\n\n // The core types are the renderer's own default; what is merged here is only what the panel\n // adds — a module's, then the project's, which therefore wins.\n const types: TypeRegistry = {}\n\n for (const module of options.modules) {\n Object.assign(types, module.types)\n }\n\n Object.assign(types, options.types)\n\n const screens = new Map<string, Promise<ScreenNode[]>>()\n\n async function loadScreen(name: string): Promise<ScreenNode[]> {\n // The tree is translated on the server, so a screen is one thing per language.\n const key = `${options.i18n.state.locale}:${name}`\n let pending = screens.get(key)\n\n if (pending === undefined) {\n pending = options.http\n .get<{ data: { screen: string; root: ScreenNode[] } }>(`${options.apiPath}/screens/${name}`)\n .then((body) => body.data.root)\n .catch((error: unknown) => {\n // A failed request is not worth remembering: the next page open asks again.\n screens.delete(key)\n throw error\n })\n screens.set(key, pending)\n }\n\n return pending\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 groups,\n types,\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 loadScreen,\n screenPatch(name) {\n return options.screens?.[name] ?? []\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 *\n * Sections come first, then the groups the server declared — \"System\" for what keeps the\n * panel running — each as a branch that opens on its own when a section under it is current.\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.groups.value.top\"\n :key=\"entry.id\"\n :value=\"entry.id\"\n :icon=\"entry.icon ?? undefined\"\n :label=\"entry.title\"\n />\n\n <wx-submenu\n v-for=\"group in admin.groups.value.groups\"\n :key=\"group.id\"\n :value=\"`group:${group.id}`\"\n :title=\"group.title\"\n icon=\"gear\"\n >\n <wx-menu-item\n v-for=\"entry in group.entries\"\n :key=\"entry.id\"\n :value=\"entry.id\"\n :icon=\"entry.icon ?? undefined\"\n :label=\"entry.title\"\n />\n </wx-submenu>\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 { Patch, TypeRegistry } from '@webx-ui/schema'\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 /**\n * Screen node types the project adds — `{ map: { component: WxMapField, kind: 'field' } }` —\n * over the core's and the modules'.\n */\n types?: TypeRegistry\n /**\n * The project's patches over the screens modules ship, by screen name, applied on the\n * client on top of what the server hands out. What a patch cannot do from here is open a\n * key for writing: the server decides what is saved.\n */\n screens?: Record<string, Patch>\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 types: options.types,\n screens: options.screens,\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","<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { WxAlert, WxSkeleton } from '@webx-ui/core'\nimport { WxScreenRenderer, type ScreenModel, type ScreenNode } from '@webx-ui/schema'\nimport { useAdmin } from './admin'\nimport { useTranslate } from './i18n'\n\n/**\n * A screen of the panel, by name.\n *\n * Fetches the tree the server hands out — patched, permission-filtered, translated — lays the\n * project's own patch over it, and draws it with the renderer against the panel's registry,\n * dictionary and permissions. It does not load values and does not save: that is the page's\n * business, the way it is for any form.\n */\nconst props = withDefaults(\n defineProps<{\n /** `<module>.<screen>`, e.g. `settings.index`. */\n name: string\n /** Server validation errors by field name, shown under the fields. */\n errors?: Record<string, string[]>\n disabled?: boolean\n labelPosition?: 'top' | 'left'\n labelWidth?: string\n size?: 'sm' | 'md' | 'lg'\n }>(),\n {\n errors: undefined,\n disabled: false,\n labelPosition: undefined,\n labelWidth: undefined,\n size: undefined,\n },\n)\n\nconst emit = defineEmits<{\n /** The tree arrived; a page that wants to know what it draws reads it here. */\n loaded: [root: ScreenNode[]]\n}>()\n\nconst model = defineModel<ScreenModel>({ default: () => ({}) })\n\nconst admin = useAdmin()\nconst t = useTranslate('webx-admin')\n\nconst root = ref<ScreenNode[] | null>(null)\nconst failed = ref<string | null>(null)\n\nasync function load(): Promise<void> {\n root.value = null\n failed.value = null\n\n try {\n const tree = await admin.loadScreen(props.name)\n root.value = tree\n emit('loaded', tree)\n } catch (error) {\n failed.value = error instanceof Error ? error.message : String(error)\n }\n}\n\n// A new language means a new tree: the labels inside it were translated on the server.\nwatch([() => props.name, () => admin.i18n.state.locale], () => void load(), { immediate: true })\n\ndefineExpose({ reload: load })\n</script>\n\n<template>\n <div class=\"wx-screen-host\">\n <wx-alert v-if=\"failed\" type=\"danger\" :title=\"t('shell.error-title')\" :description=\"failed\" />\n <wx-skeleton v-else-if=\"root === null\" :rows=\"4\" />\n <wx-screen-renderer\n v-else\n v-model=\"model\"\n :root=\"root\"\n :patch=\"admin.screenPatch(name)\"\n :types=\"admin.types\"\n :errors=\"errors\"\n :translate=\"admin.i18n.t\"\n :can=\"admin.can\"\n :disabled=\"disabled\"\n :label-position=\"labelPosition\"\n :label-width=\"labelWidth\"\n :size=\"size\"\n />\n </div>\n</template>\n"],"names":["adminKey","useAdmin","admin","inject","createAdminContext","options","state","reactive","nav","computed","manifest","entries","module","registered","candidate","path","groups","declared","top","byGroup","entry","group","list","types","screens","loadScreen","name","key","pending","body","error","loadSession","reload","setLocale","isUnauthenticated","code","user","loader","permission","provideAdmin","app","i18nKey","useI18n","i18n","useTranslate","namespace","createI18n","provideI18n","fallback","builtIn","dictionary","lookup","source","node","segment","translate","params","explicitNamespace","rest","line","fill","messages","next","locale","nextFallback","filled","value","emit","__emit","t","router","useRouter","route","useRoute","current","id","_createBlock","_component_wx_menu","$event","__props","_unref","_createElementBlock","_Fragment","_renderList","_component_wx_menu_item","_component_wx_submenu","_openBlock","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","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","model","_useModel","root","failed","load","tree","watch","__expose","WxAlert","WxSkeleton","WxScreenRenderer"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import { TypeRegistry } from '@webx-ui/schema';
|
|
2
3
|
import { LocaleDescriptor } from './i18n';
|
|
3
4
|
/**
|
|
4
5
|
* What `GET /api/cms/manifest` answers with — the panel's own description of itself, and the
|
|
@@ -16,13 +17,24 @@ export interface Manifest {
|
|
|
16
17
|
locales: LocaleDescriptor[];
|
|
17
18
|
/** The languages the interface itself can be switched to. */
|
|
18
19
|
panelLocales: LocaleDescriptor[];
|
|
20
|
+
/** Navigation groups, translated and in order; a module names one by id. */
|
|
21
|
+
groups?: ManifestGroup[];
|
|
19
22
|
modules: ManifestModule[];
|
|
23
|
+
/** Names of the screens the server can hand out — the trees themselves travel on request. */
|
|
24
|
+
screens?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface ManifestGroup {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
order: number;
|
|
20
30
|
}
|
|
21
31
|
export interface ManifestModule {
|
|
22
32
|
id: string;
|
|
23
33
|
title: string;
|
|
24
34
|
icon: string | null;
|
|
25
35
|
order: number;
|
|
36
|
+
/** The group the section sits under, or null for the top level. */
|
|
37
|
+
group?: string | null;
|
|
26
38
|
permissions: string[];
|
|
27
39
|
/** Whatever the server-side module wanted to say, in its own room. */
|
|
28
40
|
meta: Record<string, unknown>;
|
|
@@ -39,6 +51,8 @@ export interface AdminUser {
|
|
|
39
51
|
permissions: string[];
|
|
40
52
|
/** The panel language they chose, or null if they never have. */
|
|
41
53
|
locale?: string | null;
|
|
54
|
+
/** The key their photograph is stored under — not an address; the panel resolves it. */
|
|
55
|
+
avatar?: string | null;
|
|
42
56
|
[key: string]: unknown;
|
|
43
57
|
}
|
|
44
58
|
/**
|
|
@@ -60,6 +74,11 @@ export interface AdminModule {
|
|
|
60
74
|
* the reason this exists.
|
|
61
75
|
*/
|
|
62
76
|
public?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Screen node types this module brings — `wx-media` from the media module. Merged into the
|
|
79
|
+
* registry every screen in the panel is drawn with.
|
|
80
|
+
*/
|
|
81
|
+
types?: TypeRegistry;
|
|
63
82
|
}
|
|
64
83
|
export type AdminStatus = 'loading' | 'ready' | 'unauthenticated' | 'error';
|
|
65
84
|
export interface NavEntry {
|
|
@@ -67,4 +86,12 @@ export interface NavEntry {
|
|
|
67
86
|
title: string;
|
|
68
87
|
icon: string | null;
|
|
69
88
|
path: string;
|
|
89
|
+
/** Group id, or null at the top level. */
|
|
90
|
+
group: string | null;
|
|
91
|
+
}
|
|
92
|
+
/** A group with the entries that sit under it, in navigation order. */
|
|
93
|
+
export interface NavGroup {
|
|
94
|
+
id: string;
|
|
95
|
+
title: string;
|
|
96
|
+
entries: NavEntry[];
|
|
70
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"vue-router": "^4.5.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@webx-ui/core": "^0.
|
|
47
|
+
"@webx-ui/core": "^0.18.0",
|
|
48
|
+
"@webx-ui/schema": "^0.1.1",
|
|
48
49
|
"@webx-ui/tokens": "^0.2.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|