@nuxt-customer-portal/ui 0.3.3 → 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/CHANGELOG.md +19 -0
- package/app/app.config.ts +5 -0
- package/app/components/AppHeader.vue +43 -7
- package/app/components/ConfirmationModal.vue +1 -1
- package/app/components/InvitationActions.vue +37 -8
- package/app/components/PortalListToolbar.vue +8 -4
- package/app/composables/useInvitationManagement.ts +1 -0
- package/app/composables/useModuleNavigation.ts +36 -7
- package/app/composables/useNavigationLinks.ts +18 -3
- package/app/layouts/default.vue +29 -8
- package/package.json +2 -2
- package/portal.manifest.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @nuxt-customer-portal/ui
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 59c8921: Add reusable appearance presets, validated typography and surface overrides, a light/dark live preview and optional full header logos. Apply saved branding reactively and preserve existing portals through backward-compatible defaults.
|
|
8
|
+
- 59c8921: Configure supported store content and SaaS interface languages using the shared bundled language list. Preserve disabled translations and switch unavailable UI locales to an enabled language.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [59c8921]
|
|
13
|
+
- Updated dependencies [59c8921]
|
|
14
|
+
- Updated dependencies [59c8921]
|
|
15
|
+
- Updated dependencies [59c8921]
|
|
16
|
+
- Updated dependencies [59c8921]
|
|
17
|
+
- Updated dependencies [59c8921]
|
|
18
|
+
- Updated dependencies [59c8921]
|
|
19
|
+
- Updated dependencies [59c8921]
|
|
20
|
+
- @nuxt-customer-portal/core@0.4.0
|
|
21
|
+
|
|
3
22
|
## 0.3.3
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/app/app.config.ts
CHANGED
|
@@ -18,9 +18,23 @@ const props = withDefaults(
|
|
|
18
18
|
const { t, locale, setLocale } = useI18n()
|
|
19
19
|
const toast = useToast()
|
|
20
20
|
const portalRuntimeSettings = useState<{
|
|
21
|
-
branding?: {
|
|
22
|
-
|
|
21
|
+
branding?: {
|
|
22
|
+
portalName?: string
|
|
23
|
+
tagline?: string
|
|
24
|
+
markLight?: string
|
|
25
|
+
markDark?: string
|
|
26
|
+
logoLight?: string
|
|
27
|
+
logoDark?: string
|
|
28
|
+
}
|
|
29
|
+
appearance?: { colorMode?: string; headerBranding?: string }
|
|
30
|
+
languages?: string[]
|
|
23
31
|
} | null>('portal-runtime-settings', () => null)
|
|
32
|
+
const availableLanguages = computed(() =>
|
|
33
|
+
[en, nl].filter(
|
|
34
|
+
(language) =>
|
|
35
|
+
!portalRuntimeSettings.value?.languages || portalRuntimeSettings.value.languages.includes(language.code)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
24
38
|
const colorMode = useColorMode()
|
|
25
39
|
const runtimeBrandName = computed(() => portalRuntimeSettings.value?.branding?.portalName || props.brandName)
|
|
26
40
|
const runtimeTagline = computed(() => portalRuntimeSettings.value?.branding?.tagline || props.brandTagline)
|
|
@@ -33,6 +47,15 @@ const runtimeMark = computed(() => {
|
|
|
33
47
|
? branding.markDark || branding.markLight || ''
|
|
34
48
|
: branding.markLight || branding.markDark || ''
|
|
35
49
|
})
|
|
50
|
+
const runtimeFullLogo = computed(() => {
|
|
51
|
+
if (portalRuntimeSettings.value?.appearance?.headerBranding !== 'full-logo') {
|
|
52
|
+
return ''
|
|
53
|
+
}
|
|
54
|
+
const branding = portalRuntimeSettings.value.branding
|
|
55
|
+
return colorMode.value === 'dark'
|
|
56
|
+
? branding?.logoDark || branding?.logoLight || ''
|
|
57
|
+
: branding?.logoLight || branding?.logoDark || ''
|
|
58
|
+
})
|
|
36
59
|
const showColorModeControl = computed(
|
|
37
60
|
() => !portalRuntimeSettings.value || portalRuntimeSettings.value.appearance?.colorMode === 'user-choice'
|
|
38
61
|
)
|
|
@@ -173,7 +196,13 @@ const stopImpersonating = async () => {
|
|
|
173
196
|
class="flex items-center gap-3 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"
|
|
174
197
|
:aria-label="runtimeBrandName"
|
|
175
198
|
>
|
|
176
|
-
<
|
|
199
|
+
<img
|
|
200
|
+
v-if="runtimeFullLogo"
|
|
201
|
+
:src="runtimeFullLogo"
|
|
202
|
+
:alt="runtimeBrandName"
|
|
203
|
+
class="h-12 w-auto max-w-44 object-contain"
|
|
204
|
+
/>
|
|
205
|
+
<div v-else class="relative">
|
|
177
206
|
<img v-if="runtimeMark" :src="runtimeMark" alt="" class="size-10 rounded-lg object-contain" />
|
|
178
207
|
<svg
|
|
179
208
|
v-else
|
|
@@ -199,7 +228,7 @@ const stopImpersonating = async () => {
|
|
|
199
228
|
</div>
|
|
200
229
|
|
|
201
230
|
<!-- Neutral fallback wordmark; host shells can replace the layout entirely. -->
|
|
202
|
-
<div class="hidden flex-col sm:flex">
|
|
231
|
+
<div v-if="!runtimeFullLogo" class="hidden flex-col sm:flex">
|
|
203
232
|
<span class="portal-wordmark text-2xl font-bold text-gray-900 dark:text-white leading-tight">
|
|
204
233
|
{{ runtimeBrandName }}
|
|
205
234
|
</span>
|
|
@@ -245,8 +274,9 @@ const stopImpersonating = async () => {
|
|
|
245
274
|
<div class="hidden lg:flex items-center gap-3 ml-auto">
|
|
246
275
|
<UButton icon="i-lucide-search" color="neutral" variant="ghost" size="sm" square @click="searchOpen = true" />
|
|
247
276
|
<ULocaleSelect
|
|
277
|
+
v-if="availableLanguages.length > 1"
|
|
248
278
|
v-model="currentLocale"
|
|
249
|
-
:locales="
|
|
279
|
+
:locales="availableLanguages"
|
|
250
280
|
:ui="{ content: 'w-max min-w-40', itemLabel: 'whitespace-nowrap' }"
|
|
251
281
|
/>
|
|
252
282
|
<UColorModeButton v-if="showColorModeControl" />
|
|
@@ -256,7 +286,12 @@ const stopImpersonating = async () => {
|
|
|
256
286
|
</div>
|
|
257
287
|
|
|
258
288
|
<!-- Organization Switcher Modal -->
|
|
259
|
-
<UModal
|
|
289
|
+
<UModal
|
|
290
|
+
v-if="showOrgSwitcherModal"
|
|
291
|
+
v-model:open="showOrgSwitcherModal"
|
|
292
|
+
:title="t('menu.switchOrganization')"
|
|
293
|
+
:ui="{ footer: 'justify-end' }"
|
|
294
|
+
>
|
|
260
295
|
<template #body>
|
|
261
296
|
<OrganizationSwitcher
|
|
262
297
|
v-if="isAuthenticated"
|
|
@@ -283,8 +318,9 @@ const stopImpersonating = async () => {
|
|
|
283
318
|
@click="searchOpen = true"
|
|
284
319
|
/>
|
|
285
320
|
<ULocaleSelect
|
|
321
|
+
v-if="availableLanguages.length > 1"
|
|
286
322
|
v-model="currentLocale"
|
|
287
|
-
:locales="
|
|
323
|
+
:locales="availableLanguages"
|
|
288
324
|
class="w-32"
|
|
289
325
|
:ui="{ content: 'w-max min-w-40', itemLabel: 'whitespace-nowrap' }"
|
|
290
326
|
/>
|
|
@@ -53,7 +53,7 @@ const getText = (key: string | undefined, fallback: string): string => {
|
|
|
53
53
|
</script>
|
|
54
54
|
|
|
55
55
|
<template>
|
|
56
|
-
<UModal v-model:open="open" :title="getText(title, title)" :ui="{ footer: 'justify-end' }">
|
|
56
|
+
<UModal v-if="open" v-model:open="open" :title="getText(title, title)" :ui="{ footer: 'justify-end' }">
|
|
57
57
|
<template #body>
|
|
58
58
|
<div class="space-y-4">
|
|
59
59
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
@@ -6,6 +6,7 @@ const props = defineProps<{
|
|
|
6
6
|
email: string
|
|
7
7
|
role: string | null
|
|
8
8
|
canEdit?: boolean
|
|
9
|
+
canResend?: boolean
|
|
9
10
|
canRevoke?: boolean
|
|
10
11
|
}>()
|
|
11
12
|
const emit = defineEmits<{ refresh: [] }>()
|
|
@@ -46,6 +47,18 @@ const revoke = async () => {
|
|
|
46
47
|
busy.value = false
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
const resend = async () => {
|
|
51
|
+
busy.value = true
|
|
52
|
+
try {
|
|
53
|
+
await api.resend(props.endpoint)
|
|
54
|
+
toast.add({ title: t('invitationManagement.resent'), color: 'success' })
|
|
55
|
+
emit('refresh')
|
|
56
|
+
} catch {
|
|
57
|
+
toast.add({ title: t('invitationManagement.resendFailed'), color: 'error' })
|
|
58
|
+
} finally {
|
|
59
|
+
busy.value = false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
</script>
|
|
50
63
|
|
|
51
64
|
<template>
|
|
@@ -54,23 +67,36 @@ const revoke = async () => {
|
|
|
54
67
|
v-if="canEdit"
|
|
55
68
|
icon="i-lucide-pencil"
|
|
56
69
|
color="neutral"
|
|
57
|
-
variant="
|
|
58
|
-
size="
|
|
70
|
+
variant="outline"
|
|
71
|
+
size="xs"
|
|
59
72
|
:disabled="busy"
|
|
60
73
|
:aria-label="t('invitationManagement.edit')"
|
|
61
74
|
@click="openEditor"
|
|
62
|
-
|
|
75
|
+
>{{ t('invitationManagement.edit') }}</UButton
|
|
76
|
+
>
|
|
77
|
+
<UButton
|
|
78
|
+
v-if="canResend"
|
|
79
|
+
icon="i-lucide-send"
|
|
80
|
+
color="neutral"
|
|
81
|
+
variant="outline"
|
|
82
|
+
size="xs"
|
|
83
|
+
:loading="busy"
|
|
84
|
+
:aria-label="t('invitationManagement.resend')"
|
|
85
|
+
@click="resend"
|
|
86
|
+
>{{ t('invitationManagement.resend') }}</UButton
|
|
87
|
+
>
|
|
63
88
|
<UButton
|
|
64
89
|
v-if="canRevoke"
|
|
65
90
|
icon="i-lucide-x"
|
|
66
91
|
color="error"
|
|
67
|
-
variant="
|
|
68
|
-
size="
|
|
92
|
+
variant="outline"
|
|
93
|
+
size="xs"
|
|
69
94
|
:disabled="busy"
|
|
70
95
|
:aria-label="t('invitationManagement.revoke')"
|
|
71
96
|
@click="revoking = true"
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
>{{ t('invitationManagement.revoke') }}</UButton
|
|
98
|
+
>
|
|
99
|
+
<UModal v-if="editing" v-model:open="editing" :title="t('invitationManagement.edit')">
|
|
74
100
|
<template #body>
|
|
75
101
|
<UForm :state="state" :schema="invitationRoleSchema" novalidate class="space-y-4" @submit="save">
|
|
76
102
|
<p>{{ email }}</p>
|
|
@@ -79,12 +105,15 @@ const revoke = async () => {
|
|
|
79
105
|
</UFormField>
|
|
80
106
|
<div class="flex justify-end gap-2">
|
|
81
107
|
<UButton color="neutral" variant="outline" @click="editing = false">{{ t('common.cancel') }}</UButton>
|
|
82
|
-
<UButton type="submit" :loading="busy">{{
|
|
108
|
+
<UButton type="submit" icon="i-lucide-save" class="ml-auto flex min-w-28 justify-center" :loading="busy">{{
|
|
109
|
+
t('invitationManagement.save')
|
|
110
|
+
}}</UButton>
|
|
83
111
|
</div>
|
|
84
112
|
</UForm>
|
|
85
113
|
</template>
|
|
86
114
|
</UModal>
|
|
87
115
|
<ConfirmationModal
|
|
116
|
+
v-if="revoking"
|
|
88
117
|
v-model:open="revoking"
|
|
89
118
|
title="invitationManagement.revoke"
|
|
90
119
|
message="invitationManagement.confirmRevoke"
|
|
@@ -19,7 +19,7 @@ const showFilters = ref(false)
|
|
|
19
19
|
const showSort = ref(false)
|
|
20
20
|
const toolbar = ref<HTMLElement | null>(null)
|
|
21
21
|
const { width } = useElementSize(toolbar)
|
|
22
|
-
const isMobile = computed(() => width.value < Math.max(660, 160 + props.filters.length *
|
|
22
|
+
const isMobile = computed(() => width.value < Math.max(660, 160 + props.filters.length * 208 + 232))
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
25
|
<template>
|
|
@@ -40,7 +40,11 @@ const isMobile = computed(() => width.value < Math.max(660, 160 + props.filters.
|
|
|
40
40
|
value-key="value"
|
|
41
41
|
:placeholder="filter.placeholder"
|
|
42
42
|
:aria-label="filter.placeholder"
|
|
43
|
-
class="w-
|
|
43
|
+
class="w-52 shrink-0"
|
|
44
|
+
:ui="{
|
|
45
|
+
content: 'w-max min-w-[var(--reka-select-trigger-width)]',
|
|
46
|
+
itemLabel: 'whitespace-nowrap'
|
|
47
|
+
}"
|
|
44
48
|
@update:model-value="emit('filter', filter.key, $event)"
|
|
45
49
|
/>
|
|
46
50
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
|
@@ -80,7 +84,7 @@ const isMobile = computed(() => width.value < Math.max(660, 160 + props.filters.
|
|
|
80
84
|
/>
|
|
81
85
|
</template>
|
|
82
86
|
</div>
|
|
83
|
-
<UModal v-model:open="showFilters" :title="t('common.filters')">
|
|
87
|
+
<UModal v-if="showFilters" v-model:open="showFilters" :title="t('common.filters')">
|
|
84
88
|
<template #body>
|
|
85
89
|
<div class="space-y-4">
|
|
86
90
|
<UFormField v-for="filter in filters" :key="filter.key" :label="filter.placeholder">
|
|
@@ -95,7 +99,7 @@ const isMobile = computed(() => width.value < Math.max(660, 160 + props.filters.
|
|
|
95
99
|
</div>
|
|
96
100
|
</template>
|
|
97
101
|
</UModal>
|
|
98
|
-
<UModal v-model:open="showSort" :title="t('common.sort')">
|
|
102
|
+
<UModal v-if="showSort" v-model:open="showSort" :title="t('common.sort')">
|
|
99
103
|
<template #body>
|
|
100
104
|
<div class="space-y-4">
|
|
101
105
|
<USelect
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const useInvitationManagement = () => ({
|
|
2
2
|
changeRole: (endpoint: string, role: string) => $fetch(endpoint, { method: 'PATCH', body: { role } }),
|
|
3
|
+
resend: (endpoint: string) => $fetch(`${endpoint}/resend`, { method: 'POST' }),
|
|
3
4
|
revoke: (endpoint: string) => $fetch(endpoint, { method: 'PATCH', body: { status: 'canceled' } })
|
|
4
5
|
})
|
|
@@ -6,7 +6,8 @@ const hasAudience = (
|
|
|
6
6
|
isAuthenticated: boolean,
|
|
7
7
|
isAdmin: boolean,
|
|
8
8
|
organizationRole: string | null,
|
|
9
|
-
organizationType: 'PROVIDER' | 'CLIENT' | null
|
|
9
|
+
organizationType: 'PROVIDER' | 'CLIENT' | null,
|
|
10
|
+
isPersonalClient: boolean
|
|
10
11
|
) =>
|
|
11
12
|
audiences.some((audience) => {
|
|
12
13
|
if (audience === 'public') {
|
|
@@ -30,6 +31,13 @@ const hasAudience = (
|
|
|
30
31
|
if (audience === 'clientAdmin') {
|
|
31
32
|
return organizationType === 'CLIENT' && (organizationRole === 'owner' || organizationRole === 'admin')
|
|
32
33
|
}
|
|
34
|
+
if (audience === 'clientOrganizationAdmin') {
|
|
35
|
+
return (
|
|
36
|
+
organizationType === 'CLIENT' &&
|
|
37
|
+
!isPersonalClient &&
|
|
38
|
+
(organizationRole === 'owner' || organizationRole === 'admin')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
33
41
|
return organizationType === 'PROVIDER' && (organizationRole === 'owner' || organizationRole === 'admin')
|
|
34
42
|
})
|
|
35
43
|
|
|
@@ -37,9 +45,15 @@ export const useModuleNavigation = (sidebarOpen?: Ref<boolean>) => {
|
|
|
37
45
|
const { t } = useI18n()
|
|
38
46
|
const route = useRoute()
|
|
39
47
|
const { modules: registeredModules } = usePortalFeatures()
|
|
40
|
-
const {
|
|
48
|
+
const {
|
|
49
|
+
isAuthenticated,
|
|
50
|
+
isSystemAdmin,
|
|
51
|
+
activeOrganizationRole,
|
|
52
|
+
activeOrganizationType,
|
|
53
|
+
activeOrganizationIsPersonal
|
|
54
|
+
} = usePortalSession()
|
|
41
55
|
|
|
42
|
-
const
|
|
56
|
+
const accessibleModules = computed(() =>
|
|
43
57
|
registeredModules.value
|
|
44
58
|
.filter((module) =>
|
|
45
59
|
hasAudience(
|
|
@@ -47,15 +61,28 @@ export const useModuleNavigation = (sidebarOpen?: Ref<boolean>) => {
|
|
|
47
61
|
isAuthenticated.value,
|
|
48
62
|
isSystemAdmin.value,
|
|
49
63
|
activeOrganizationRole.value,
|
|
50
|
-
activeOrganizationType.value
|
|
64
|
+
activeOrganizationType.value,
|
|
65
|
+
activeOrganizationIsPersonal.value
|
|
51
66
|
)
|
|
52
67
|
)
|
|
53
68
|
.map((module) => ({ ...module, label: t(module.labelKey) }))
|
|
54
69
|
)
|
|
70
|
+
const modules = computed(() =>
|
|
71
|
+
accessibleModules.value.filter((module) =>
|
|
72
|
+
hasAudience(
|
|
73
|
+
module.navigationAudiences ?? module.audiences,
|
|
74
|
+
isAuthenticated.value,
|
|
75
|
+
isSystemAdmin.value,
|
|
76
|
+
activeOrganizationRole.value,
|
|
77
|
+
activeOrganizationType.value,
|
|
78
|
+
activeOrganizationIsPersonal.value
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
55
82
|
|
|
56
83
|
const activeModule = computed(
|
|
57
84
|
() =>
|
|
58
|
-
|
|
85
|
+
accessibleModules.value
|
|
59
86
|
.map((module) => ({
|
|
60
87
|
module,
|
|
61
88
|
matchLength: Math.max(
|
|
@@ -78,7 +105,8 @@ export const useModuleNavigation = (sidebarOpen?: Ref<boolean>) => {
|
|
|
78
105
|
isAuthenticated.value,
|
|
79
106
|
isSystemAdmin.value,
|
|
80
107
|
activeOrganizationRole.value,
|
|
81
|
-
activeOrganizationType.value
|
|
108
|
+
activeOrganizationType.value,
|
|
109
|
+
activeOrganizationIsPersonal.value
|
|
82
110
|
)
|
|
83
111
|
)
|
|
84
112
|
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
|
|
@@ -104,7 +132,8 @@ export const useModuleNavigation = (sidebarOpen?: Ref<boolean>) => {
|
|
|
104
132
|
isAuthenticated.value,
|
|
105
133
|
isSystemAdmin.value,
|
|
106
134
|
activeOrganizationRole.value,
|
|
107
|
-
activeOrganizationType.value
|
|
135
|
+
activeOrganizationType.value,
|
|
136
|
+
activeOrganizationIsPersonal.value
|
|
108
137
|
)
|
|
109
138
|
)
|
|
110
139
|
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
|
|
@@ -6,7 +6,8 @@ const hasAudience = (
|
|
|
6
6
|
isAuthenticated: boolean,
|
|
7
7
|
isAdmin: boolean,
|
|
8
8
|
organizationRole: string | null,
|
|
9
|
-
organizationType: 'PROVIDER' | 'CLIENT' | null
|
|
9
|
+
organizationType: 'PROVIDER' | 'CLIENT' | null,
|
|
10
|
+
isPersonalClient: boolean
|
|
10
11
|
) =>
|
|
11
12
|
audiences.some((audience) => {
|
|
12
13
|
if (audience === 'public') {
|
|
@@ -30,6 +31,13 @@ const hasAudience = (
|
|
|
30
31
|
if (audience === 'clientAdmin') {
|
|
31
32
|
return organizationType === 'CLIENT' && (organizationRole === 'owner' || organizationRole === 'admin')
|
|
32
33
|
}
|
|
34
|
+
if (audience === 'clientOrganizationAdmin') {
|
|
35
|
+
return (
|
|
36
|
+
organizationType === 'CLIENT' &&
|
|
37
|
+
!isPersonalClient &&
|
|
38
|
+
(organizationRole === 'owner' || organizationRole === 'admin')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
33
41
|
return organizationType === 'PROVIDER' && (organizationRole === 'owner' || organizationRole === 'admin')
|
|
34
42
|
})
|
|
35
43
|
|
|
@@ -37,7 +45,13 @@ export const useNavigationLinks = (sidebarOpen: Ref<boolean>) => {
|
|
|
37
45
|
const { t } = useI18n()
|
|
38
46
|
const route = useRoute()
|
|
39
47
|
const { navigation } = usePortalFeatures()
|
|
40
|
-
const {
|
|
48
|
+
const {
|
|
49
|
+
isAuthenticated,
|
|
50
|
+
isSystemAdmin,
|
|
51
|
+
activeOrganizationRole,
|
|
52
|
+
activeOrganizationType,
|
|
53
|
+
activeOrganizationIsPersonal
|
|
54
|
+
} = usePortalSession()
|
|
41
55
|
|
|
42
56
|
const visibleItems = computed(() =>
|
|
43
57
|
navigation.value.filter(
|
|
@@ -47,7 +61,8 @@ export const useNavigationLinks = (sidebarOpen: Ref<boolean>) => {
|
|
|
47
61
|
isAuthenticated.value,
|
|
48
62
|
isSystemAdmin.value,
|
|
49
63
|
activeOrganizationRole.value,
|
|
50
|
-
activeOrganizationType.value
|
|
64
|
+
activeOrganizationType.value,
|
|
65
|
+
activeOrganizationIsPersonal.value
|
|
51
66
|
) &&
|
|
52
67
|
(route.path === '/' || !item.audiences.every((audience) => audience === 'public'))
|
|
53
68
|
)
|
package/app/layouts/default.vue
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
<script setup lang="ts">
|
|
3
3
|
const route = useRoute()
|
|
4
4
|
const toast = useToast()
|
|
5
|
+
const { t } = useI18n()
|
|
5
6
|
const open = ref(false)
|
|
7
|
+
const sidebarCollapsed = ref(false)
|
|
6
8
|
const showFooter = computed(() => route.meta?.public === true)
|
|
7
9
|
|
|
8
10
|
const { activeModule, activeModuleMenuItems } = useModuleNavigation(open)
|
|
@@ -39,7 +41,7 @@ onMounted(async () => {
|
|
|
39
41
|
</script>
|
|
40
42
|
|
|
41
43
|
<template>
|
|
42
|
-
<div class="dashboard-layout relative min-h-screen">
|
|
44
|
+
<div class="dashboard-layout relative min-h-screen" :data-sidebar-collapsed="sidebarCollapsed">
|
|
43
45
|
<!-- AppHeader - fixed at top -->
|
|
44
46
|
<div class="fixed left-0 right-0 z-50" style="top: var(--portal-top-bar-height, 0px)">
|
|
45
47
|
<div class="mx-auto w-full max-w-[1600px] px-4">
|
|
@@ -49,30 +51,43 @@ onMounted(async () => {
|
|
|
49
51
|
|
|
50
52
|
<UDashboardGroup
|
|
51
53
|
unit="rem"
|
|
54
|
+
storage-key="portal-dashboard-v2"
|
|
52
55
|
class="px-4 mx-auto max-w-[1600px]! w-full!"
|
|
53
56
|
style="top: var(--portal-top-bar-height, 0px)"
|
|
54
57
|
>
|
|
55
58
|
<UDashboardSidebar
|
|
56
59
|
id="default"
|
|
57
60
|
v-model:open="open"
|
|
61
|
+
v-model:collapsed="sidebarCollapsed"
|
|
58
62
|
collapsible
|
|
59
63
|
resizable
|
|
64
|
+
:collapsed-size="4"
|
|
60
65
|
:default-size="18"
|
|
61
66
|
:min-size="18"
|
|
62
67
|
:max-size="24"
|
|
63
|
-
class="min-w-72 bg-elevated/25 lg:pb-12 data-[collapsed=true]:min-w-
|
|
68
|
+
class="min-w-72 bg-elevated/25 lg:pb-12 data-[collapsed=true]:min-w-16"
|
|
64
69
|
:ui="{ footer: 'lg:border-t lg:border-default' }"
|
|
65
70
|
>
|
|
66
|
-
<template #header>
|
|
67
|
-
<div v-if="activeModule" class="flex min-w-0 items-center gap-2 px-2 text-sm font-semibold">
|
|
71
|
+
<template #header="{ collapsed }">
|
|
72
|
+
<div v-if="activeModule && !collapsed" class="flex min-w-0 items-center gap-2 px-2 text-sm font-semibold">
|
|
68
73
|
<UIcon v-if="activeModule.icon" :name="activeModule.icon" class="size-4 shrink-0 text-primary" />
|
|
69
74
|
<span class="truncate">{{ activeModule.label }}</span>
|
|
70
75
|
</div>
|
|
71
76
|
</template>
|
|
72
|
-
<template #default="{ collapsed }">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
<template #default="{ collapsed, collapse }">
|
|
78
|
+
<div class="flex items-center pt-3 pb-1" :class="collapsed ? 'justify-center' : 'justify-between px-2.5'">
|
|
79
|
+
<span v-if="!collapsed && activeModule" class="text-xs font-semibold uppercase text-muted">
|
|
80
|
+
{{ activeModule.label }}
|
|
81
|
+
</span>
|
|
82
|
+
<UButton
|
|
83
|
+
:icon="collapsed ? 'i-lucide-panel-left-open' : 'i-lucide-panel-left-close'"
|
|
84
|
+
color="neutral"
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="xs"
|
|
87
|
+
:aria-label="t(collapsed ? 'common.expandSidebar' : 'common.collapseSidebar')"
|
|
88
|
+
:title="t(collapsed ? 'common.expandSidebar' : 'common.collapseSidebar')"
|
|
89
|
+
@click="collapse(!collapsed)"
|
|
90
|
+
/>
|
|
76
91
|
</div>
|
|
77
92
|
<UNavigationMenu
|
|
78
93
|
:collapsed="collapsed"
|
|
@@ -93,3 +108,9 @@ onMounted(async () => {
|
|
|
93
108
|
<AppFooter v-if="showFooter" />
|
|
94
109
|
</div>
|
|
95
110
|
</template>
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.dashboard-layout[data-sidebar-collapsed='true'] main [class~='max-w-(--ui-container)'] {
|
|
114
|
+
max-width: none;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxt-customer-portal/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://nuxt-customer-portal.com",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@nuxtjs/i18n": "^10.6.0",
|
|
30
30
|
"@pinia/nuxt": "^1.0.1",
|
|
31
31
|
"pinia": "^4.0.2",
|
|
32
|
-
"@nuxt-customer-portal/core": "^0.
|
|
32
|
+
"@nuxt-customer-portal/core": "^0.4.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"nuxt": "^4.5.1",
|