@wikex/admin-kit 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ Các thay đổi đáng chú ý của `@wikex/admin-kit` được ghi tại đây theo Semantic Versioning.
4
+
5
+ ## 0.2.0 - 2026-09-12
6
+
7
+ - Thêm `createWikexStarter`: một factory cấu hình content schema, SEO, redirect, admin UI,
8
+ branding, route Live Preview và module registry từ một cấu hình dự án.
9
+ - Chuyển cấu hình phát hành sang npm public; dự án consumer không cần `.npmrc` hay token.
10
+
11
+ ## 0.1.0 - 2026-09-12
12
+
13
+ - Tách Payload admin shell dùng chung khỏi dự án Estate.
14
+ - Thêm Live Preview editor, drawer thuộc tính và modal chọn block dùng chung.
15
+ - Thêm typography editor với font family, line-height và resize trực tiếp.
16
+ - Thêm Minimal UI skin, chuẩn hóa tiếng Việt và hướng dẫn kích thước ảnh.
17
+ - Thêm adapter branding, block catalog và route registry theo từng dự án.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # @wikex/admin-kit
2
+
3
+ Admin shell dùng chung cho các website Payload CMS của Wikex.
4
+
5
+ Package sở hữu:
6
+
7
+ - navigation, header, đăng nhập, đổi theme và branding động;
8
+ - Minimal UI skin và các bản vá layout Payload;
9
+ - Live Preview editor, drawer thuộc tính, block picker và resize text;
10
+ - font family, line-height và runtime áp dụng typography;
11
+ - chuẩn hóa tiếng Việt và hướng dẫn kích thước upload ảnh.
12
+
13
+ Schema, block catalog, route registry, dashboard và seed data vẫn thuộc ứng dụng sử dụng package vì chúng thay đổi theo từng dự án.
14
+
15
+ `@wikex/admin-kit` phụ thuộc `@wikex/content-kit`, nên chỉ cần cài Admin Kit là có luôn
16
+ schema Page/Post/Media, SEO, redirects, robots và sitemap chuẩn. Schema, block catalog và
17
+ frontend renderer cụ thể vẫn thuộc dự án consumer.
18
+
19
+ ## Dùng trong workspace
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "@wikex/admin-kit": "workspace:*"
25
+ }
26
+ }
27
+ ```
28
+
29
+ ```ts
30
+ // next.config.ts
31
+ const nextConfig = {
32
+ transpilePackages: ['@wikex/admin-kit'],
33
+ }
34
+ ```
35
+
36
+ ```scss
37
+ // Payload custom.scss
38
+ @use '@wikex/admin-kit/styles';
39
+ ```
40
+
41
+ ```ts
42
+ // Payload plugins
43
+ import { adminExperiencePlugin, wikexAdminPlugin } from '@wikex/admin-kit'
44
+
45
+ export const plugins = [
46
+ adminExperiencePlugin,
47
+ wikexAdminPlugin({
48
+ components: {
49
+ beforeDashboard: '@/components/BeforeDashboard',
50
+ provider: '@/components/LivePreviewResizer',
51
+ },
52
+ }),
53
+ ]
54
+ ```
55
+
56
+ Live Preview provider của dự án là adapter khai báo route và component khả dụng:
57
+
58
+ ```tsx
59
+ 'use client'
60
+
61
+ import { configureAdminBrand } from '@wikex/admin-kit/admin/brand'
62
+ import LivePreviewEditor, { configureLivePreview } from '@wikex/admin-kit/live-preview'
63
+
64
+ configureLivePreview({
65
+ getVisualEditorRoute,
66
+ pageBlocks,
67
+ postBlocks,
68
+ })
69
+
70
+ configureAdminBrand({
71
+ logo: { alt: projectName, height: 64, url: '/logo.svg', width: 64 },
72
+ siteName: projectName,
73
+ })
74
+
75
+ export default LivePreviewEditor
76
+ ```
77
+
78
+ Project config và route resolver cũng dùng API chung, nhưng tên module/collection vẫn do
79
+ từng website quyết định:
80
+
81
+ ```ts
82
+ import { createVisualEditorRouteResolver, defineWikexProject } from '@wikex/admin-kit/project'
83
+
84
+ export const project = defineWikexProject({
85
+ brand: {
86
+ description: 'Website doanh nghiệp',
87
+ logo: { height: 64, url: '/logo.svg', width: 64 },
88
+ name: 'Wikex Corporate',
89
+ },
90
+ id: 'wikex-corporate',
91
+ modules: ['website', 'services'],
92
+ })
93
+
94
+ export const getVisualEditorRoute = createVisualEditorRouteResolver({
95
+ collections: ['pages'],
96
+ globals: { footer: 'footer', header: 'header', homepage: 'content' },
97
+ })
98
+ ```
99
+
100
+ Xem consumer không thuộc lĩnh vực bất động sản tại `examples/corporate-demo`.
101
+
102
+ Ứng dụng cần cung cấp các file font tại `public/fonts/google`, hoặc ghi đè các biến `--font-*` nếu dùng font riêng.
103
+
104
+ ## Cài vào dự án khác — một package, một cấu hình
105
+
106
+ ```bash
107
+ pnpm add @wikex/admin-kit@0.2.0
108
+ ```
109
+
110
+ Không cần `.npmrc`, GitLab token hay npm token ở dự án consumer. Các peer dependency
111
+ Payload/Next/React phải đã có trong dự án.
112
+
113
+ ```ts
114
+ // src/wikex.ts — server-only config, dùng một lần trong Payload config
115
+ import { createWikexStarter } from '@wikex/admin-kit/starter'
116
+ import { pageBlocks, postBlocks } from './blocks'
117
+
118
+ export const wikex = createWikexStarter({
119
+ brand: {
120
+ description: 'Website công ty Acme',
121
+ logo: { height: 64, url: '/logo.svg', width: 64 },
122
+ name: 'Acme',
123
+ },
124
+ content: {
125
+ page: { blocks: pageBlocks, previewURL: '/' },
126
+ post: { blocks: postBlocks, previewURL: '/news' },
127
+ seo: {
128
+ siteName: 'Acme',
129
+ pathFor: ({ collection, slug }) =>
130
+ collection === 'pages' ? (slug === 'home' ? '/' : `/${slug}`) : `/news/${slug}`,
131
+ },
132
+ },
133
+ globals: { footer: 'footer', header: 'header' },
134
+ id: 'acme',
135
+ provider: '@/LivePreviewAdapter',
136
+ })
137
+ ```
138
+
139
+ ```ts
140
+ // payload.config.ts
141
+ import { wikex } from './src/wikex'
142
+
143
+ export default buildConfig({
144
+ collections: [Users, ...wikex.collections, ...projectCollections],
145
+ globals: projectGlobals,
146
+ plugins: wikex.plugins,
147
+ })
148
+ ```
149
+
150
+ Giữ `LivePreviewAdapter.tsx` nhỏ ở client vì block renderer của mỗi website là riêng.
151
+ Adapter gọi `createWikexLivePreviewAdapter` từ `@wikex/admin-kit/starter/client` với
152
+ branding, block picker và route resolver tương ứng; xem implementation hoàn chỉnh tại
153
+ `examples/corporate-demo`.
154
+
155
+ ## Phát hành npm public
156
+
157
+ Khi phát hành:
158
+
159
+ 1. tăng version theo SemVer và cập nhật `CHANGELOG.md`;
160
+ 2. chạy test/typecheck/build trong starter;
161
+ 3. tạo tag `admin-kit-v<version>` để GitLab CI publish npm;
162
+ 4. ở từng dự án consumer đổi `workspace:*` thành version cố định, ví dụ `0.2.0`;
163
+ 5. nâng version có chủ đích, không dùng `latest` trong production.
164
+
165
+ GitLab CI cần một CI/CD variable bảo mật `NPM_TOKEN` có quyền publish scope `@wikex`.
166
+ Đây là credential duy nhất của phía phát hành; người dùng package công khai không cần
167
+ credential nào.
package/RELEASING.md ADDED
@@ -0,0 +1,59 @@
1
+ # Phát hành Admin Kit
2
+
3
+ Package không tự publish khi merge. Chỉ người có quyền registry mới thực hiện phát hành.
4
+
5
+ ## Kiểm tra trước khi phát hành
6
+
7
+ Từ thư mục gốc workspace:
8
+
9
+ ```bash
10
+ pnpm install --frozen-lockfile
11
+ pnpm generate:types
12
+ pnpm exec tsc --noEmit
13
+ pnpm exec tsc --noEmit -p examples/corporate-demo/tsconfig.json
14
+ pnpm lint
15
+ pnpm exec vitest run --config ./vitest.config.mts --exclude tests/int/api.int.spec.ts
16
+ pnpm build
17
+ ```
18
+
19
+ Kiểm tra nội dung package:
20
+
21
+ ```bash
22
+ cd packages/admin-kit
23
+ npm pack --dry-run
24
+ ```
25
+
26
+ ## Version
27
+
28
+ - `patch`: sửa lỗi, không đổi API.
29
+ - `minor`: thêm tính năng tương thích ngược.
30
+ - `major`: thay đổi adapter/API bắt buộc dự án consumer phải sửa.
31
+
32
+ Cập nhật `version` trong `package.json` và ghi thay đổi vào `CHANGELOG.md` trước khi publish.
33
+
34
+ ## npm public
35
+
36
+ Pipeline `.gitlab-ci.yml` publish vào `https://registry.npmjs.org/`. Thêm một GitLab
37
+ CI/CD Variable tên `NPM_TOKEN`, masked và protected, chứa npm granular access token có
38
+ quyền publish scope `@wikex` (và bypass 2FA nếu npm account bật 2FA cho publish).
39
+
40
+ Token chỉ tồn tại ở CI; consumer cài package công khai không cần `.npmrc` hay token.
41
+
42
+ Lần đầu kết nối source repository, chạy từ thư mục gốc:
43
+
44
+ ```bash
45
+ bash scripts/connect-gitlab.sh
46
+ ```
47
+
48
+ Script không ghi đè một remote `origin` khác nếu đã tồn tại.
49
+
50
+ Tạo tag trùng với version package để phát hành:
51
+
52
+ ```bash
53
+ git tag admin-kit-v0.2.0
54
+ git push origin admin-kit-v0.2.0
55
+ ```
56
+
57
+ Job phát hành kiểm tra tag phải đúng mẫu `admin-kit-v<version>` trước khi upload package.
58
+ Vì Admin Kit phụ thuộc Content Kit, release `content-kit-v<version>` trước, rồi mới tạo
59
+ tag Admin Kit dùng version Content Kit đó.
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@wikex/admin-kit",
3
+ "version": "0.2.0",
4
+ "description": "Reusable Payload CMS admin shell, visual editor and typography tools for Wikex projects",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "type": "module",
8
+ "sideEffects": [
9
+ "**/*.scss"
10
+ ],
11
+ "files": [
12
+ "src",
13
+ "CHANGELOG.md",
14
+ "README.md",
15
+ "RELEASING.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "dependencies": {
22
+ "@wikex/content-kit": "^0.1.0"
23
+ },
24
+ "exports": {
25
+ ".": "./src/index.ts",
26
+ "./admin/before-login": "./src/admin/BeforeLogin.tsx",
27
+ "./admin/brand": "./src/admin/Brand.tsx",
28
+ "./admin/header": "./src/admin/Header.tsx",
29
+ "./admin/logout-button": "./src/admin/LogoutButton.tsx",
30
+ "./admin/nav": "./src/admin/Nav.tsx",
31
+ "./admin/theme-toggle": "./src/admin/ThemeToggle.tsx",
32
+ "./admin/view-site": "./src/admin/ViewSite.tsx",
33
+ "./live-preview": "./src/live-preview/index.ts",
34
+ "./project": "./src/project.ts",
35
+ "./rich-text": "./src/rich-text/index.ts",
36
+ "./rich-text/client": "./src/rich-text/client.tsx",
37
+ "./starter": "./src/starter.ts",
38
+ "./starter/client": "./src/starter/client.tsx",
39
+ "./styles": "./src/styles/admin.scss",
40
+ "./text-style-runtime": "./src/TextStyleRuntime.tsx",
41
+ "./text-styles": "./src/textStyles.ts"
42
+ },
43
+ "peerDependencies": {
44
+ "@lexical/react": ">=0.41.0",
45
+ "@lexical/selection": ">=0.41.0",
46
+ "@payloadcms/richtext-lexical": ">=3.88.0",
47
+ "@payloadcms/ui": ">=3.88.0",
48
+ "lexical": ">=0.41.0",
49
+ "lucide-react": ">=0.563.0",
50
+ "next": ">=16.3.3",
51
+ "payload": ">=3.88.0",
52
+ "react": ">=19.0.0",
53
+ "react-dom": ">=19.0.0"
54
+ }
55
+ }
@@ -0,0 +1,90 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ import {
6
+ EDITABLE_PREVIEW_SELECTOR,
7
+ getEditableElementStyleKey,
8
+ getTextStyleCSS,
9
+ normalizeTextStyleMap,
10
+ } from './textStyles'
11
+
12
+ type OriginalStyle = {
13
+ boxSizing: string
14
+ fontFamily: string
15
+ lineHeight: string
16
+ marginLeft: string
17
+ marginRight: string
18
+ maxWidth: string
19
+ width: string
20
+ }
21
+
22
+ export const TextStyleRuntime = ({ styles }: { styles?: unknown }) => {
23
+ useEffect(() => {
24
+ const normalizedStyles = normalizeTextStyleMap(styles)
25
+ const originals = new Map<HTMLElement, OriginalStyle>()
26
+ let applyFrame = 0
27
+
28
+ const applyStyles = () => {
29
+ const fields = new Set(
30
+ document.querySelectorAll<HTMLElement>(`[data-wikex-field],${EDITABLE_PREVIEW_SELECTOR}`),
31
+ )
32
+
33
+ for (const element of fields) {
34
+ const fieldPath = element.dataset.wikexField || getEditableElementStyleKey(element)
35
+ if (!fieldPath || !normalizedStyles[fieldPath]) continue
36
+
37
+ if (!originals.has(element)) {
38
+ originals.set(element, {
39
+ boxSizing: element.style.boxSizing,
40
+ fontFamily: element.style.fontFamily,
41
+ lineHeight: element.style.lineHeight,
42
+ marginLeft: element.style.marginLeft,
43
+ marginRight: element.style.marginRight,
44
+ maxWidth: element.style.maxWidth,
45
+ width: element.style.width,
46
+ })
47
+ }
48
+
49
+ const style = getTextStyleCSS(normalizedStyles[fieldPath])
50
+ element.style.boxSizing = style.boxSizing || ''
51
+ element.style.fontFamily = style.fontFamily || ''
52
+ element.style.lineHeight = style.lineHeight || ''
53
+ element.style.marginLeft = style.marginLeft || ''
54
+ element.style.marginRight = style.marginRight || ''
55
+ element.style.maxWidth = style.maxWidth || ''
56
+ element.style.width = style.width || ''
57
+ }
58
+ }
59
+
60
+ const scheduleApplyStyles = () => {
61
+ if (applyFrame) return
62
+
63
+ applyFrame = window.requestAnimationFrame(() => {
64
+ applyFrame = 0
65
+ applyStyles()
66
+ })
67
+ }
68
+
69
+ applyStyles()
70
+ const observer = new MutationObserver(scheduleApplyStyles)
71
+ observer.observe(document.body, { childList: true, subtree: true })
72
+
73
+ return () => {
74
+ observer.disconnect()
75
+ window.cancelAnimationFrame(applyFrame)
76
+ for (const [element, original] of originals) {
77
+ if (!element.isConnected) continue
78
+ element.style.boxSizing = original.boxSizing
79
+ element.style.fontFamily = original.fontFamily
80
+ element.style.lineHeight = original.lineHeight
81
+ element.style.marginLeft = original.marginLeft
82
+ element.style.marginRight = original.marginRight
83
+ element.style.maxWidth = original.maxWidth
84
+ element.style.width = original.width
85
+ }
86
+ }
87
+ }, [styles])
88
+
89
+ return null
90
+ }
@@ -0,0 +1,19 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+
5
+ import { useAdminBrand } from './useAdminBrand'
6
+
7
+ const BeforeLogin: React.FC = () => {
8
+ const { siteName } = useAdminBrand()
9
+
10
+ return (
11
+ <div className="admin-login-intro">
12
+ <span className="admin-login-intro__eyebrow">{siteName} · Trung tâm nội dung</span>
13
+ <h1>Chào mừng trở lại.</h1>
14
+ <p>Đăng nhập để quản lý nội dung, hình ảnh và xuất bản của {siteName}.</p>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export default BeforeLogin
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import Image from 'next/image'
4
+ import React from 'react'
5
+
6
+ import { useAdminBrand } from './useAdminBrand'
7
+
8
+ export type AdminBrandAsset = {
9
+ alt: string
10
+ height: number
11
+ url: string
12
+ width: number
13
+ }
14
+
15
+ const BrandLogo = ({
16
+ asset,
17
+ className,
18
+ height,
19
+ maxWidth,
20
+ }: {
21
+ asset: AdminBrandAsset
22
+ className?: string
23
+ height: number
24
+ maxWidth: number
25
+ siteName: string
26
+ }) => (
27
+ <span
28
+ className={['brand-logo', className].filter(Boolean).join(' ')}
29
+ style={{ alignItems: 'center', display: 'inline-flex', height, maxWidth }}
30
+ >
31
+ <Image
32
+ alt=""
33
+ height={asset.height}
34
+ src={asset.url}
35
+ style={{ height: '100%', maxWidth: '100%', objectFit: 'contain', width: 'auto' }}
36
+ unoptimized
37
+ width={asset.width}
38
+ />
39
+ </span>
40
+ )
41
+
42
+ export const AdminIcon: React.FC = () => {
43
+ const brand = useAdminBrand()
44
+
45
+ return (
46
+ <span className="aurea-brand aurea-brand--icon">
47
+ <BrandLogo
48
+ asset={brand.logo}
49
+ className="aurea-brand__image"
50
+ height={32}
51
+ maxWidth={32}
52
+ siteName={brand.siteName}
53
+ />
54
+ </span>
55
+ )
56
+ }
57
+
58
+ export const AdminLogo: React.FC = () => {
59
+ const brand = useAdminBrand()
60
+
61
+ return (
62
+ <span aria-label={brand.siteName} className="aurea-brand aurea-brand--logo" role="img">
63
+ <BrandLogo
64
+ asset={brand.logo}
65
+ className="aurea-brand__image"
66
+ height={42}
67
+ maxWidth={190}
68
+ siteName={brand.siteName}
69
+ />
70
+ </span>
71
+ )
72
+ }
73
+
74
+ export { configureAdminBrand, useAdminBrand, type AdminBrandSettings } from './useAdminBrand'
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import { Link, useAuth, useConfig, useNav } from '@payloadcms/ui'
4
+ import { Menu, Settings2 } from 'lucide-react'
5
+ import { formatAdminURL } from 'payload/shared'
6
+ import React from 'react'
7
+
8
+ import { AdminIcon } from './Brand'
9
+ import AdminThemeToggle from './ThemeToggle'
10
+ import AdminViewSite from './ViewSite'
11
+
12
+ type AdminUser = {
13
+ email?: string
14
+ name?: string
15
+ }
16
+
17
+ const getInitials = (name?: string, email?: string) => {
18
+ const source = name?.trim() || email?.split('@')[0] || 'AD'
19
+
20
+ return source
21
+ .split(/\s+/)
22
+ .slice(0, 2)
23
+ .map((part) => part.charAt(0).toUpperCase())
24
+ .join('')
25
+ }
26
+
27
+ const AdminHeader = () => {
28
+ const { user } = useAuth<AdminUser>()
29
+ const { setNavOpen } = useNav()
30
+ const {
31
+ config: {
32
+ admin: { routes: adminRoutes },
33
+ routes: { admin: adminRoute },
34
+ },
35
+ } = useConfig()
36
+
37
+ const userLabel = user?.name?.trim() || user?.email?.split('@')[0] || 'Quản trị viên'
38
+ const initials = getInitials(user?.name, user?.email)
39
+
40
+ return (
41
+ <header className="minimal-admin-header">
42
+ <div className="minimal-admin-header__left">
43
+ <button
44
+ aria-label="Mở menu quản trị"
45
+ className="minimal-admin-header__menu"
46
+ onClick={() => setNavOpen(true)}
47
+ type="button"
48
+ >
49
+ <Menu aria-hidden="true" />
50
+ </button>
51
+ <Link
52
+ aria-label="Về dashboard"
53
+ className="minimal-admin-header__mobile-brand"
54
+ href={adminRoute}
55
+ prefetch={false}
56
+ >
57
+ <AdminIcon />
58
+ </Link>
59
+ </div>
60
+
61
+ <div className="minimal-admin-header__actions">
62
+ <AdminViewSite />
63
+ <AdminThemeToggle />
64
+ <Link
65
+ aria-label={`Mở tài khoản ${userLabel}`}
66
+ className="minimal-admin-header__settings"
67
+ href={formatAdminURL({ adminRoute, path: adminRoutes.account })}
68
+ prefetch={false}
69
+ title="Cài đặt tài khoản"
70
+ >
71
+ <Settings2 aria-hidden="true" />
72
+ </Link>
73
+ <Link
74
+ aria-label={`Mở tài khoản ${userLabel}`}
75
+ className="minimal-admin-header__account"
76
+ href={formatAdminURL({ adminRoute, path: adminRoutes.account })}
77
+ prefetch={false}
78
+ title={userLabel}
79
+ >
80
+ <span>{initials}</span>
81
+ </Link>
82
+ </div>
83
+ </header>
84
+ )
85
+ }
86
+
87
+ export default AdminHeader
@@ -0,0 +1,41 @@
1
+ 'use client'
2
+
3
+ import { Link, useConfig, useTranslation } from '@payloadcms/ui'
4
+ import { LogOut } from 'lucide-react'
5
+ import { formatAdminURL } from 'payload/shared'
6
+ import React from 'react'
7
+
8
+ type AdminLogoutButtonProps = {
9
+ tabIndex?: number
10
+ }
11
+
12
+ const AdminLogoutButton: React.FC<AdminLogoutButtonProps> = ({ tabIndex = 0 }) => {
13
+ const {
14
+ config: {
15
+ admin: {
16
+ routes: { logout: logoutRoute },
17
+ },
18
+ routes: { admin: adminRoute },
19
+ },
20
+ } = useConfig()
21
+ const { t } = useTranslation()
22
+ const label = t('authentication:logOut')
23
+
24
+ return (
25
+ <Link
26
+ aria-label={label}
27
+ className="admin-sign-out"
28
+ href={formatAdminURL({ adminRoute, path: logoutRoute })}
29
+ prefetch={false}
30
+ tabIndex={tabIndex}
31
+ title={label}
32
+ >
33
+ <span aria-hidden="true" className="admin-sign-out__icon">
34
+ <LogOut strokeWidth={2} />
35
+ </span>
36
+ <span className="admin-sign-out__label">{label}</span>
37
+ </Link>
38
+ )
39
+ }
40
+
41
+ export default AdminLogoutButton
@@ -0,0 +1,35 @@
1
+ import { getNavGroups } from '@payloadcms/ui/shared'
2
+ import type { ServerProps, StaticLabel } from 'payload'
3
+ import React from 'react'
4
+
5
+ import { AdminNavClient, type AdminNavGroup } from './NavClient'
6
+
7
+ const localizeLabel = (label: StaticLabel, language: string): string => {
8
+ if (typeof label === 'string') return label
9
+
10
+ return label[language] || Object.values(label)[0] || ''
11
+ }
12
+
13
+ const AdminNav = ({ i18n, payload, permissions, visibleEntities }: ServerProps) => {
14
+ if (!permissions || !visibleEntities) return null
15
+
16
+ const groups: AdminNavGroup[] = getNavGroups(
17
+ permissions,
18
+ visibleEntities,
19
+ payload.config,
20
+ i18n,
21
+ ).map((group) => ({
22
+ items: group.entities.map((entity) => ({
23
+ label: localizeLabel(entity.label, i18n.language),
24
+ slug: entity.slug,
25
+ type: entity.type,
26
+ })),
27
+ label: group.label,
28
+ }))
29
+
30
+ return <AdminNavClient groups={groups} />
31
+ }
32
+
33
+ export default AdminNav
34
+
35
+ export { AdminNavClient, type AdminNavGroup } from './NavClient'