@peanut-admin/admin 0.1.0-alpha.11

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.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/access/permission-policy.ts +37 -0
  4. package/admin-core/src/api/client.ts +200 -0
  5. package/admin-core/src/api/problem.ts +67 -0
  6. package/admin-core/src/api/refresh.ts +122 -0
  7. package/admin-core/src/auth/stores.ts +121 -0
  8. package/admin-core/src/auth/tenant-session.ts +32 -0
  9. package/admin-core/src/generated/api.d.ts +22106 -0
  10. package/admin-core/src/governance/audit.ts +56 -0
  11. package/admin-core/src/governance/catalog.ts +86 -0
  12. package/admin-core/src/governance/index.ts +26 -0
  13. package/admin-core/src/governance/menu.ts +63 -0
  14. package/admin-core/src/governance/roles.ts +144 -0
  15. package/admin-core/src/governance/types.ts +64 -0
  16. package/admin-core/src/index.ts +122 -0
  17. package/admin-core/src/lifecycle/tenant.ts +59 -0
  18. package/admin-core/src/module/contribution.ts +124 -0
  19. package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
  20. package/admin-core/src/module/tenant-modules.ts +20 -0
  21. package/admin-core/src/runtime/config.ts +55 -0
  22. package/admin-core/src/runtime/errors.ts +81 -0
  23. package/admin-core/src/runtime/guard.ts +40 -0
  24. package/admin-core/src/runtime/navigation.ts +105 -0
  25. package/admin-core/src/runtime/overrides.ts +214 -0
  26. package/admin-core/src/targets/store.ts +153 -0
  27. package/admin-shell/src/config.ts +84 -0
  28. package/admin-shell/src/deployment-mode.ts +36 -0
  29. package/admin-shell/src/index.ts +44 -0
  30. package/admin-shell/src/layout.ts +332 -0
  31. package/admin-shell/src/overrides.ts +53 -0
  32. package/admin-shell/src/states.ts +93 -0
  33. package/admin-shell/src/tabs.ts +31 -0
  34. package/admin-shell/src/targets.ts +128 -0
  35. package/admin-shell/src/theme.ts +15 -0
  36. package/client-core/src/index.ts +415 -0
  37. package/client-nuxt/src/index.ts +48 -0
  38. package/client-uniapp/src/index.ts +50 -0
  39. package/file-media/src/FileAssetSelector.vue +117 -0
  40. package/file-media/src/FileMediaPage.vue +158 -0
  41. package/file-media/src/contracts.ts +220 -0
  42. package/file-media/src/index.ts +19 -0
  43. package/file-media/src/runtime.ts +210 -0
  44. package/import-export/src/ImportExportPage.vue +155 -0
  45. package/import-export/src/contracts.ts +96 -0
  46. package/import-export/src/index.ts +3 -0
  47. package/import-export/src/runtime.ts +128 -0
  48. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  49. package/integration-security/src/contracts.ts +171 -0
  50. package/integration-security/src/index.ts +3 -0
  51. package/integration-security/src/runtime.ts +180 -0
  52. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  53. package/notification-sms/src/contracts.ts +195 -0
  54. package/notification-sms/src/index.ts +4 -0
  55. package/notification-sms/src/runtime.ts +143 -0
  56. package/ops-console/src/OpsConsolePage.vue +337 -0
  57. package/ops-console/src/contracts.ts +169 -0
  58. package/ops-console/src/index.ts +3 -0
  59. package/ops-console/src/runtime.ts +199 -0
  60. package/package.json +139 -0
  61. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  62. package/reference-codes/src/contracts.ts +484 -0
  63. package/reference-codes/src/index.ts +53 -0
  64. package/reference-codes/src/runtime.ts +855 -0
  65. package/settings/src/SettingsPage.vue +536 -0
  66. package/settings/src/contracts.ts +331 -0
  67. package/settings/src/index.ts +45 -0
  68. package/settings/src/runtime.ts +545 -0
  69. package/task-job/src/TaskJobPage.vue +120 -0
  70. package/task-job/src/contracts.ts +117 -0
  71. package/task-job/src/index.ts +2 -0
  72. package/task-job/src/runtime.ts +105 -0
  73. package/testing/src/index.ts +141 -0
@@ -0,0 +1,153 @@
1
+ import { defineStore } from 'pinia'
2
+
3
+ export interface TypedTarget {
4
+ target_resource_key: string
5
+ target_role: string
6
+ target_id: string
7
+ }
8
+
9
+ export interface TypedTargetSet {
10
+ target_resource_key: string
11
+ target_role: string
12
+ target_ids: readonly string[]
13
+ }
14
+
15
+ export interface TargetCandidate extends TypedTarget {
16
+ label: string
17
+ owner_label?: string
18
+ status?: string
19
+ }
20
+
21
+ export interface OperationTargetScope {
22
+ moduleKey: string
23
+ resourceKey: string
24
+ operation: string
25
+ targetResourceKey: string
26
+ targetRole: string
27
+ cardinality?: TargetCardinality
28
+ }
29
+
30
+ export type TargetCardinality = 'none' | 'one_required' | 'zero_or_one' | 'many_readable' | 'aggregate_read' | 'policy_publish' | 'bulk_write'
31
+
32
+ interface TargetEntry {
33
+ scope: OperationTargetScope
34
+ candidates: TargetCandidate[]
35
+ selectedIds: string[]
36
+ }
37
+
38
+ interface TargetStoreState {
39
+ entries: Record<string, TargetEntry>
40
+ generation: number
41
+ }
42
+
43
+ const scopeKey = (scope: OperationTargetScope): string => JSON.stringify([
44
+ scope.moduleKey,
45
+ scope.resourceKey,
46
+ scope.operation,
47
+ scope.targetResourceKey,
48
+ scope.targetRole,
49
+ scope.cardinality ?? 'many_readable',
50
+ ])
51
+
52
+ const normalizeCandidates = (
53
+ scope: OperationTargetScope,
54
+ candidates: readonly TargetCandidate[],
55
+ ): TargetCandidate[] => {
56
+ const ids = new Set<string>()
57
+ return candidates.map(candidate => {
58
+ if (candidate.target_resource_key !== scope.targetResourceKey
59
+ || candidate.target_role !== scope.targetRole
60
+ || candidate.target_id === ''
61
+ || ids.has(candidate.target_id)) {
62
+ throw new Error('TARGET_CANDIDATE_SCOPE_INVALID')
63
+ }
64
+ ids.add(candidate.target_id)
65
+ return { ...candidate }
66
+ })
67
+ }
68
+
69
+ export const useOperationTargets = defineStore('peanut-admin-operation-targets', {
70
+ state: (): TargetStoreState => ({ entries: {}, generation: 0 }),
71
+ actions: {
72
+ replace(scope: OperationTargetScope, candidates: readonly TargetCandidate[]): void {
73
+ const key = scopeKey(scope)
74
+ const normalized = normalizeCandidates(scope, candidates)
75
+ const available = new Set(normalized.map(candidate => candidate.target_id))
76
+ const previous = this.entries[key]?.selectedIds ?? []
77
+ const retained = previous.filter(id => available.has(id))
78
+ this.entries[key] = {
79
+ scope: { ...scope },
80
+ candidates: normalized,
81
+ selectedIds: normalized.length === 1 ? [normalized[0]!.target_id] : retained,
82
+ }
83
+ this.generation += 1
84
+ },
85
+ select(scope: OperationTargetScope, targets: readonly TypedTarget[]): void {
86
+ const key = scopeKey(scope)
87
+ const entry = this.entries[key]
88
+ if (entry === undefined) {
89
+ throw new Error('TARGET_SCOPE_NOT_LOADED')
90
+ }
91
+ const available = new Set(entry.candidates.map(candidate => candidate.target_id))
92
+ const selected = new Set<string>()
93
+ for (const target of targets) {
94
+ if (target.target_resource_key !== scope.targetResourceKey
95
+ || target.target_role !== scope.targetRole
96
+ || !available.has(target.target_id)) {
97
+ throw new Error('TARGET_SELECTION_INVALID')
98
+ }
99
+ selected.add(target.target_id)
100
+ }
101
+ const cardinality = scope.cardinality ?? 'many_readable'
102
+ if ((cardinality === 'none' && selected.size > 0)
103
+ || (cardinality === 'one_required' && selected.size !== 1)
104
+ || (cardinality === 'zero_or_one' && selected.size > 1)
105
+ || cardinality === 'bulk_write') {
106
+ throw new Error('TARGET_SELECTION_CARDINALITY_INVALID')
107
+ }
108
+ entry.selectedIds = [...selected]
109
+ this.generation += 1
110
+ },
111
+ selected(scope: OperationTargetScope): TypedTarget[] {
112
+ const entry = this.entries[scopeKey(scope)]
113
+ return (entry?.selectedIds ?? []).map(targetId => ({
114
+ target_resource_key: scope.targetResourceKey,
115
+ target_role: scope.targetRole,
116
+ target_id: targetId,
117
+ }))
118
+ },
119
+ selectedSet(scope: OperationTargetScope): TypedTargetSet {
120
+ return {
121
+ target_resource_key: scope.targetResourceKey,
122
+ target_role: scope.targetRole,
123
+ target_ids: this.selected(scope).map(target => target.target_id),
124
+ }
125
+ },
126
+ selectionForRequest(scope: OperationTargetScope): TypedTargetSet {
127
+ const selection = this.selectedSet(scope)
128
+ const cardinality = scope.cardinality ?? 'many_readable'
129
+ if ((cardinality === 'one_required' && selection.target_ids.length !== 1)
130
+ || (cardinality === 'zero_or_one' && selection.target_ids.length > 1)
131
+ || (cardinality === 'none' && selection.target_ids.length !== 0)
132
+ || cardinality === 'bulk_write') {
133
+ throw new Error('TARGET_SELECTION_CARDINALITY_INVALID')
134
+ }
135
+
136
+ return selection
137
+ },
138
+ clearScope(scope: OperationTargetScope): void {
139
+ delete this.entries[scopeKey(scope)]
140
+ this.generation += 1
141
+ },
142
+ clearModule(moduleKey: string): void {
143
+ this.entries = Object.fromEntries(
144
+ Object.entries(this.entries).filter(([, entry]) => entry.scope.moduleKey !== moduleKey),
145
+ )
146
+ this.generation += 1
147
+ },
148
+ clearAll(): void {
149
+ this.entries = {}
150
+ this.generation += 1
151
+ },
152
+ },
153
+ })
@@ -0,0 +1,84 @@
1
+ export interface ShellHostConfigInput {
2
+ brand: {
3
+ name: string
4
+ mark: string
5
+ }
6
+ audiences: {
7
+ tenant: { label: string }
8
+ platform: { label: string }
9
+ }
10
+ commands: {
11
+ switchTenantLabel: string
12
+ logoutLabel: string
13
+ }
14
+ }
15
+
16
+ export interface ShellHostConfig {
17
+ readonly brand: Readonly<ShellHostConfigInput['brand']>
18
+ readonly audiences: Readonly<{
19
+ tenant: Readonly<ShellHostConfigInput['audiences']['tenant']>
20
+ platform: Readonly<ShellHostConfigInput['audiences']['platform']>
21
+ }>
22
+ readonly commands: Readonly<ShellHostConfigInput['commands']>
23
+ }
24
+
25
+ const assertObject = (value: unknown, path: string): Record<string, unknown> => {
26
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
27
+ throw new Error(`SHELL_CONFIG_INVALID:${path}`)
28
+ }
29
+ return value as Record<string, unknown>
30
+ }
31
+
32
+ const assertKnownFields = (value: Record<string, unknown>, allowed: readonly string[], path = ''): void => {
33
+ for (const key of Object.keys(value)) {
34
+ if (!allowed.includes(key)) {
35
+ throw new Error(`SHELL_CONFIG_UNKNOWN_FIELD:${path}${key}`)
36
+ }
37
+ }
38
+ }
39
+
40
+ const displayValue = (value: unknown, path: string, maximum: number): string => {
41
+ if (typeof value !== 'string') throw new Error(`SHELL_CONFIG_INVALID:${path}`)
42
+ const normalized = value.trim()
43
+ if (normalized.length === 0 || normalized.length > maximum) {
44
+ throw new Error(`SHELL_CONFIG_INVALID:${path}`)
45
+ }
46
+ return normalized
47
+ }
48
+
49
+ export const defineShellHostConfig = (input: ShellHostConfigInput): ShellHostConfig => {
50
+ const root = assertObject(input, 'root')
51
+ assertKnownFields(root, ['brand', 'audiences', 'commands'])
52
+
53
+ const brandInput = assertObject(root.brand, 'brand')
54
+ const audiencesInput = assertObject(root.audiences, 'audiences')
55
+ const tenantInput = assertObject(audiencesInput.tenant, 'audiences.tenant')
56
+ const platformInput = assertObject(audiencesInput.platform, 'audiences.platform')
57
+ const commandsInput = assertObject(root.commands, 'commands')
58
+ assertKnownFields(brandInput, ['name', 'mark'], 'brand.')
59
+ assertKnownFields(audiencesInput, ['tenant', 'platform'], 'audiences.')
60
+ assertKnownFields(tenantInput, ['label'], 'audiences.tenant.')
61
+ assertKnownFields(platformInput, ['label'], 'audiences.platform.')
62
+ assertKnownFields(commandsInput, ['switchTenantLabel', 'logoutLabel'], 'commands.')
63
+
64
+ const brand = Object.freeze({
65
+ name: displayValue(brandInput.name, 'brand.name', 120),
66
+ mark: displayValue(brandInput.mark, 'brand.mark', 12),
67
+ })
68
+ const tenant = Object.freeze({
69
+ label: displayValue(tenantInput.label, 'audiences.tenant.label', 80),
70
+ })
71
+ const platform = Object.freeze({
72
+ label: displayValue(platformInput.label, 'audiences.platform.label', 80),
73
+ })
74
+ const commands = Object.freeze({
75
+ switchTenantLabel: displayValue(commandsInput.switchTenantLabel, 'commands.switchTenantLabel', 80),
76
+ logoutLabel: displayValue(commandsInput.logoutLabel, 'commands.logoutLabel', 80),
77
+ })
78
+
79
+ return Object.freeze({
80
+ brand,
81
+ audiences: Object.freeze({ tenant, platform }),
82
+ commands,
83
+ })
84
+ }
@@ -0,0 +1,36 @@
1
+ export type DeploymentMode = 'standalone' | 'multi-tenant'
2
+
3
+ /**
4
+ * Minimal router contract shared by host applications without coupling to a
5
+ * particular Vue Router major version.
6
+ */
7
+ export interface DeploymentRoute {
8
+ meta?: {
9
+ controlPlane?: unknown
10
+ instanceTool?: boolean
11
+ [key: string]: unknown
12
+ }
13
+ children?: DeploymentRoute[]
14
+ }
15
+
16
+ export const deploymentMode = (value: unknown): DeploymentMode => (
17
+ value === 'multi-tenant' ? 'multi-tenant' : 'standalone'
18
+ )
19
+
20
+ export const allowsInstanceTools = (value: unknown): boolean => value === 'standalone'
21
+
22
+ export const routesForDeployment = <T extends DeploymentRoute>(
23
+ routes: T[],
24
+ mode: DeploymentMode,
25
+ instanceToolsAllowed = mode === 'standalone',
26
+ ): T[] => routes.reduce<T[]>((visible, route) => {
27
+ if (mode !== 'multi-tenant' && route.meta?.controlPlane !== undefined) return visible
28
+ if (!instanceToolsAllowed && route.meta?.instanceTool === true) return visible
29
+ visible.push({
30
+ ...route,
31
+ children: route.children
32
+ ? routesForDeployment(route.children, mode, instanceToolsAllowed)
33
+ : route.children,
34
+ } as T)
35
+ return visible
36
+ }, [])
@@ -0,0 +1,44 @@
1
+ export const ADMIN_SHELL_PACKAGE = '@peanut-admin/admin/shell' as const
2
+ export const ADMIN_SHELL_VERSION = '0.1.0' as const
3
+
4
+ export { defineShellHostConfig } from './config'
5
+ export type { ShellHostConfig, ShellHostConfigInput } from './config'
6
+ export {
7
+ AdminShell,
8
+ PageContent,
9
+ PageHeader,
10
+ PageToolbar,
11
+ PlatformShell,
12
+ ShellBreadcrumb,
13
+ ShellHeader,
14
+ ShellSidebar,
15
+ ShellTabs,
16
+ } from './layout'
17
+ export type { ShellBreadcrumbItem, ShellIdentity, ShellNavigationItem } from './layout'
18
+ export {
19
+ ConflictState,
20
+ EmptyState,
21
+ ForbiddenState,
22
+ ModuleUnavailableState,
23
+ NotFoundState,
24
+ RateLimitState,
25
+ ServiceUnavailableState,
26
+ SessionExpiredState,
27
+ } from './states'
28
+ export { TargetScopeSummary, TargetSelector } from './targets'
29
+ export type { TargetScopeMode } from './targets'
30
+ export { SHELL_THEME_TOKENS } from './theme'
31
+ export type { ShellSlotName, ShellThemeToken } from './theme'
32
+ export { allowsInstanceTools, deploymentMode, routesForDeployment } from './deployment-mode'
33
+ export type { DeploymentMode, DeploymentRoute } from './deployment-mode'
34
+ export { tabFromRoute } from './tabs'
35
+ export type { ShellTab, ShellTabRoute, ShellTabState } from './tabs'
36
+ export {
37
+ ADMIN_SHELL_OVERRIDE_SLOTS,
38
+ resolveWorkspaceShell,
39
+ WORKSPACE_SHELL_OVERRIDE_KEY,
40
+ } from './overrides'
41
+ export type {
42
+ AdminShellOverrideRegistry,
43
+ WorkspaceShellResolver,
44
+ } from './overrides'
@@ -0,0 +1,332 @@
1
+ /* eslint-disable vue/one-component-per-file */
2
+
3
+ import { ElButton, ElDrawer } from 'element-plus'
4
+ import { defineComponent, h, onBeforeUnmount, onMounted } from 'vue'
5
+ import type { Component, PropType, VNodeChild } from 'vue'
6
+
7
+ import type { ShellHostConfig } from './config'
8
+
9
+ export interface ShellIdentity {
10
+ accountLabel: string
11
+ contextLabel: string
12
+ actorLabel: string
13
+ }
14
+
15
+ export interface ShellNavigationItem {
16
+ key: string
17
+ label: string
18
+ path: string | null
19
+ children: readonly ShellNavigationItem[]
20
+ }
21
+
22
+ export interface ShellBreadcrumbItem {
23
+ label: string
24
+ path: string | null
25
+ }
26
+
27
+ const shellProps = {
28
+ config: { type: Object as PropType<ShellHostConfig>, default: null },
29
+ identity: { type: Object as PropType<ShellIdentity>, default: null },
30
+ navigation: { type: Array as PropType<readonly ShellNavigationItem[]>, default: () => [] },
31
+ breadcrumbs: { type: Array as PropType<readonly ShellBreadcrumbItem[]>, default: () => [] },
32
+ activePath: { type: String, default: '' },
33
+ collapsed: { type: Boolean, default: false },
34
+ mobileOpen: { type: Boolean, default: false },
35
+ openNavigationLabel: { type: String, default: 'Open navigation' },
36
+ collapseNavigationLabel: { type: String, default: 'Collapse navigation' },
37
+ expandNavigationLabel: { type: String, default: 'Expand navigation' },
38
+ primaryNavigationLabel: { type: String, default: 'Primary navigation' },
39
+ mobileNavigationLabel: { type: String, default: 'Mobile navigation' },
40
+ breadcrumbLabel: { type: String, default: 'Breadcrumb' },
41
+ identityLabel: { type: String, default: 'Current identity' },
42
+ }
43
+
44
+ const shellEmits = {
45
+ navigate: (path: string) => typeof path === 'string',
46
+ 'update:collapsed': (collapsed: boolean) => typeof collapsed === 'boolean',
47
+ 'update:mobileOpen': (open: boolean) => typeof open === 'boolean',
48
+ 'switch-tenant': () => true,
49
+ logout: () => true,
50
+ }
51
+
52
+ const shellRouteOrigin = 'https://shell.invalid'
53
+
54
+ const trustedLocalPath = (path: string | null): path is string => {
55
+ if (path === null
56
+ || !path.startsWith('/')
57
+ || path.startsWith('//')
58
+ || /[\\\u0000-\u001f\u007f]/.test(path)) return false
59
+
60
+ try {
61
+ return new URL(path, shellRouteOrigin).origin === shellRouteOrigin
62
+ } catch {
63
+ return false
64
+ }
65
+ }
66
+
67
+ const ShellFrame = defineComponent({
68
+ name: 'ShellFrame',
69
+ inheritAttrs: false,
70
+ props: {
71
+ audience: {
72
+ type: String as () => 'tenant' | 'platform',
73
+ required: true,
74
+ },
75
+ },
76
+ setup(props, { attrs, slots }) {
77
+ return () => h('div', {
78
+ ...attrs,
79
+ class: ['pa-shell', `pa-shell--${props.audience}`, attrs.class],
80
+ 'data-audience': props.audience,
81
+ }, [
82
+ slots.header?.(),
83
+ h('div', { class: 'pa-shell__workspace' }, [
84
+ slots.sidebar?.(),
85
+ h('div', { class: 'pa-shell__main' }, [
86
+ slots.breadcrumb?.(),
87
+ slots.tabs?.(),
88
+ slots.default?.(),
89
+ ]),
90
+ ]),
91
+ ])
92
+ },
93
+ })
94
+
95
+ const WorkspaceShell = defineComponent({
96
+ name: 'WorkspaceShell',
97
+ inheritAttrs: false,
98
+ props: {
99
+ ...shellProps,
100
+ audience: {
101
+ type: String as PropType<'tenant' | 'platform'>,
102
+ required: true,
103
+ },
104
+ },
105
+ emits: shellEmits,
106
+ setup(props, { attrs, emit, slots }) {
107
+ const closeMobile = () => emit('update:mobileOpen', false)
108
+ const onKeydown = (event: KeyboardEvent) => {
109
+ if (event.key === 'Escape' && props.mobileOpen) closeMobile()
110
+ }
111
+ onMounted(() => document.addEventListener('keydown', onKeydown))
112
+ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
113
+
114
+ const renderNavigation = (items: readonly ShellNavigationItem[], mobile: boolean): VNodeChild[] => items.flatMap(item => {
115
+ const children = renderNavigation(item.children, mobile)
116
+ if (!trustedLocalPath(item.path)) {
117
+ return [h('p', { class: ['pa-shell-navigation__group', 'navigation-group'], key: item.key }, item.label), ...children]
118
+ }
119
+ return [h('a', {
120
+ key: item.key,
121
+ href: item.path,
122
+ class: ['pa-shell-navigation__link', 'navigation-link', { 'is-active': props.activePath === item.path }],
123
+ 'aria-current': props.activePath === item.path ? 'page' : undefined,
124
+ 'aria-label': item.label,
125
+ onClick: (event: MouseEvent) => {
126
+ event.preventDefault()
127
+ emit('navigate', item.path as string)
128
+ if (mobile) closeMobile()
129
+ },
130
+ }, [
131
+ h('span', { class: 'navigation-marker', 'aria-hidden': 'true' }),
132
+ h('span', item.label),
133
+ ]), ...children]
134
+ })
135
+
136
+ return () => {
137
+ if (props.config === null) {
138
+ return h(ShellFrame, { ...attrs, audience: props.audience }, slots)
139
+ }
140
+
141
+ const audienceLabel = props.config.audiences[props.audience].label
142
+ const identity = props.identity
143
+ const commands = [
144
+ props.audience === 'tenant'
145
+ ? h(ElButton as unknown as Component, {
146
+ text: true,
147
+ onClick: () => emit('switch-tenant'),
148
+ }, () => props.config?.commands.switchTenantLabel)
149
+ : null,
150
+ h(ElButton as unknown as Component, {
151
+ text: true,
152
+ onClick: () => emit('logout'),
153
+ }, () => props.config?.commands.logoutLabel),
154
+ ]
155
+ const navigation = h('nav', {
156
+ class: ['pa-shell-navigation', 'workspace-navigation'],
157
+ 'aria-label': props.primaryNavigationLabel,
158
+ }, renderNavigation(props.navigation, false))
159
+ const mobileNavigation = h('div', { class: 'pa-shell-mobile-content' }, [
160
+ identity === null ? null : h('div', { class: 'pa-shell-mobile-identity', 'aria-label': props.identityLabel }, [
161
+ h('strong', identity.actorLabel),
162
+ h('span', identity.contextLabel),
163
+ h('small', identity.accountLabel),
164
+ ]),
165
+ h('nav', {
166
+ class: ['pa-shell-navigation', 'workspace-navigation'],
167
+ 'aria-label': props.mobileNavigationLabel,
168
+ }, renderNavigation(props.navigation, true)),
169
+ h('div', { class: ['pa-shell-mobile-commands', 'mobile-navigation-commands'] }, commands),
170
+ ])
171
+
172
+ return h(ShellFrame, { ...attrs, audience: props.audience }, {
173
+ header: () => h(ShellHeader, {}, () => [
174
+ h('button', {
175
+ type: 'button',
176
+ class: 'mobile-nav-trigger',
177
+ 'aria-label': props.openNavigationLabel,
178
+ 'aria-expanded': String(props.mobileOpen),
179
+ onClick: () => emit('update:mobileOpen', true),
180
+ }, [
181
+ h('span', { 'aria-hidden': 'true' }),
182
+ h('span', { 'aria-hidden': 'true' }),
183
+ h('span', { 'aria-hidden': 'true' }),
184
+ ]),
185
+ h('div', { class: ['pa-shell-brand', 'shell-brand'] }, [
186
+ h('span', { class: ['pa-shell-brand__mark', 'brand-mark'], 'aria-hidden': 'true' }, props.config?.brand.mark),
187
+ h('span', {}, [h('strong', props.config?.brand.name), h('small', audienceLabel)]),
188
+ ]),
189
+ identity === null ? null : h('div', {
190
+ class: ['pa-shell-identity', 'shell-context'],
191
+ 'aria-label': props.identityLabel,
192
+ }, [
193
+ h('span', identity.contextLabel),
194
+ h('strong', identity.actorLabel),
195
+ h('small', identity.accountLabel),
196
+ ]),
197
+ h('div', { class: ['pa-shell-commands', 'shell-commands'] }, commands),
198
+ ]),
199
+ sidebar: () => h(ShellSidebar, { collapsed: props.collapsed }, () => [
200
+ navigation,
201
+ h('button', {
202
+ type: 'button',
203
+ class: ['pa-shell__collapse', 'sidebar-collapse'],
204
+ 'aria-label': props.collapsed ? props.expandNavigationLabel : props.collapseNavigationLabel,
205
+ onClick: () => emit('update:collapsed', !props.collapsed),
206
+ }, props.collapsed ? props.expandNavigationLabel : props.collapseNavigationLabel),
207
+ ]),
208
+ breadcrumb: () => h(ShellBreadcrumb, { label: props.breadcrumbLabel }, () => props.breadcrumbs.map((item, index) => (
209
+ trustedLocalPath(item.path)
210
+ ? h('a', {
211
+ key: `${index}:${item.label}`,
212
+ href: item.path,
213
+ onClick: (event: MouseEvent) => {
214
+ event.preventDefault()
215
+ emit('navigate', item.path as string)
216
+ },
217
+ }, item.label)
218
+ : h('span', { key: `${index}:${item.label}`, 'aria-current': 'page' }, item.label)
219
+ ))),
220
+ default: () => [
221
+ slots.default?.(),
222
+ h(ElDrawer as unknown as Component, {
223
+ modelValue: props.mobileOpen,
224
+ 'onUpdate:modelValue': (open: boolean) => emit('update:mobileOpen', open),
225
+ title: props.config?.brand.name,
226
+ direction: 'ltr',
227
+ size: 'min(84vw, 320px)',
228
+ class: 'mobile-navigation-drawer',
229
+ closeOnPressEscape: true,
230
+ }, () => mobileNavigation),
231
+ ],
232
+ })
233
+ }
234
+ },
235
+ })
236
+
237
+ const createAudienceShell = (name: string, audience: 'tenant' | 'platform') => defineComponent({
238
+ name,
239
+ inheritAttrs: false,
240
+ props: shellProps,
241
+ emits: shellEmits,
242
+ setup(props, { attrs, emit, slots }) {
243
+ return () => h(WorkspaceShell as Component, {
244
+ ...attrs,
245
+ ...props,
246
+ audience,
247
+ onNavigate: (path: string) => emit('navigate', path),
248
+ 'onUpdate:collapsed': (collapsed: boolean) => emit('update:collapsed', collapsed),
249
+ 'onUpdate:mobileOpen': (open: boolean) => emit('update:mobileOpen', open),
250
+ onSwitchTenant: () => emit('switch-tenant'),
251
+ onLogout: () => emit('logout'),
252
+ } as Record<string, unknown>, slots)
253
+ },
254
+ })
255
+
256
+ export const AdminShell = createAudienceShell('AdminShell', 'tenant')
257
+
258
+ export const PlatformShell = createAudienceShell('PlatformShell', 'platform')
259
+
260
+ export const ShellHeader = defineComponent({
261
+ name: 'ShellHeader',
262
+ setup(_, { slots }) {
263
+ return () => h('header', { class: 'pa-shell-header' }, slots.default?.())
264
+ },
265
+ })
266
+
267
+ export const ShellSidebar = defineComponent({
268
+ name: 'ShellSidebar',
269
+ props: {
270
+ label: { type: String, default: 'Primary navigation' },
271
+ collapsed: { type: Boolean, default: false },
272
+ },
273
+ setup(props, { slots }) {
274
+ return () => h('aside', {
275
+ class: ['pa-shell-sidebar', { 'is-collapsed': props.collapsed }],
276
+ 'aria-label': props.label,
277
+ }, slots.default?.())
278
+ },
279
+ })
280
+
281
+ export const ShellBreadcrumb = defineComponent({
282
+ name: 'ShellBreadcrumb',
283
+ props: {
284
+ label: { type: String, default: 'Breadcrumb' },
285
+ },
286
+ setup(props, { slots }) {
287
+ return () => h('nav', { class: 'pa-shell-breadcrumb', 'aria-label': props.label }, slots.default?.())
288
+ },
289
+ })
290
+
291
+ export const ShellTabs = defineComponent({
292
+ name: 'ShellTabs',
293
+ props: {
294
+ label: { type: String, default: 'Open pages' },
295
+ },
296
+ setup(props, { slots }) {
297
+ return () => h('nav', { class: 'pa-shell-tabs', 'aria-label': props.label }, slots.default?.())
298
+ },
299
+ })
300
+
301
+ export const PageHeader = defineComponent({
302
+ name: 'PageHeader',
303
+ setup(_, { slots }) {
304
+ return () => h('header', { class: 'pa-page-header' }, [
305
+ h('div', { class: 'pa-page-header__title' }, slots.default?.()),
306
+ slots.actions === undefined
307
+ ? null
308
+ : h('div', { class: 'pa-page-header__actions' }, slots.actions()),
309
+ ])
310
+ },
311
+ })
312
+
313
+ export const PageToolbar = defineComponent({
314
+ name: 'PageToolbar',
315
+ props: {
316
+ label: { type: String, default: 'Page actions' },
317
+ },
318
+ setup(props, { slots }) {
319
+ return () => h('div', {
320
+ class: 'pa-page-toolbar',
321
+ role: 'toolbar',
322
+ 'aria-label': props.label,
323
+ }, slots.default?.())
324
+ },
325
+ })
326
+
327
+ export const PageContent = defineComponent({
328
+ name: 'PageContent',
329
+ setup(_, { slots }) {
330
+ return () => h('section', { class: 'pa-page-content' }, slots.default?.())
331
+ },
332
+ })