@nuxt-customer-portal/ui 0.3.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,7 @@
1
+ # @nuxt-customer-portal/ui
2
+
3
+ ## 0.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @nuxt-customer-portal/core@0.3.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nuxt Customer Portal contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Nuxt Customer Portal UI
2
+
3
+ Neutral shell primitives and fallback layouts for Nuxt Customer Portal. Hosts
4
+ can override every layout and branded component while retaining the shared
5
+ feature registry, session state, and navigation contracts from core.
@@ -0,0 +1,29 @@
1
+ const dropdownContent = 'bg-default ring-1 ring-accented shadow-xl shadow-black/15 dark:shadow-black/40'
2
+ const selectItem =
3
+ 'data-highlighted:not-data-disabled:before:bg-accented data-[state=checked]:before:bg-elevated data-[state=checked]:font-medium'
4
+
5
+ export default defineAppConfig({
6
+ ui: {
7
+ button: {
8
+ compoundVariants: [
9
+ {
10
+ color: 'primary',
11
+ variant: 'solid',
12
+ class: 'text-[color:var(--portal-on-primary,var(--ui-text-inverted))] hover:bg-primary active:bg-primary'
13
+ }
14
+ ]
15
+ },
16
+ select: {
17
+ slots: { content: dropdownContent, item: selectItem }
18
+ },
19
+ selectMenu: {
20
+ slots: { content: dropdownContent, item: selectItem }
21
+ },
22
+ dropdownMenu: {
23
+ slots: {
24
+ content: dropdownContent,
25
+ item: 'data-highlighted:before:bg-accented data-[state=open]:before:bg-accented'
26
+ }
27
+ }
28
+ }
29
+ })
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title: string
4
+ description?: string
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <UCard>
10
+ <template #header>
11
+ <div>
12
+ <h2 class="text-xl font-semibold text-gray-900 dark:text-white">
13
+ {{ title }}
14
+ </h2>
15
+ <h6 v-if="description" class="text-sm text-gray-600 dark:text-gray-400">
16
+ {{ description }}
17
+ </h6>
18
+ </div>
19
+ </template>
20
+ <slot />
21
+ </UCard>
22
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ const { t } = useI18n()
3
+ </script>
4
+
5
+ <template>
6
+ <USeparator class="h-px" />
7
+
8
+ <UFooter
9
+ class="portal-footer"
10
+ :ui="{ top: 'border-b border-default', container: 'w-full max-w-none px-0 sm:px-0 lg:px-0' }"
11
+ >
12
+ <template #left>
13
+ <p class="text-muted text-sm">© {{ new Date().getFullYear() }}. {{ t('footer.copyright') }}</p>
14
+ </template>
15
+ <template #right>
16
+ <nav class="flex gap-4 text-sm" aria-label="Legal">
17
+ <NuxtLink class="text-muted hover:text-highlighted" to="/terms">Terms</NuxtLink
18
+ ><NuxtLink class="text-muted hover:text-highlighted" to="/privacy">Privacy</NuxtLink>
19
+ </nav>
20
+ </template>
21
+ </UFooter>
22
+ </template>
@@ -0,0 +1,398 @@
1
+ <script setup lang="ts">
2
+ import { en, nl } from '@nuxt/ui/locale'
3
+ import { authClient } from '@nuxt-customer-portal/core/app/utils/auth-client'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ showNavigation?: boolean
8
+ brandName?: string
9
+ brandTagline?: string
10
+ }>(),
11
+ {
12
+ showNavigation: true,
13
+ brandName: 'Nuxt Customer Portal',
14
+ brandTagline: 'Customer workspace'
15
+ }
16
+ )
17
+
18
+ const { t, locale, setLocale } = useI18n()
19
+ const toast = useToast()
20
+ const portalRuntimeSettings = useState<{
21
+ branding?: { portalName?: string; tagline?: string; markLight?: string; markDark?: string }
22
+ appearance?: { colorMode?: string }
23
+ } | null>('portal-runtime-settings', () => null)
24
+ const colorMode = useColorMode()
25
+ const runtimeBrandName = computed(() => portalRuntimeSettings.value?.branding?.portalName || props.brandName)
26
+ const runtimeTagline = computed(() => portalRuntimeSettings.value?.branding?.tagline || props.brandTagline)
27
+ const runtimeMark = computed(() => {
28
+ const branding = portalRuntimeSettings.value?.branding
29
+ if (!branding) {
30
+ return ''
31
+ }
32
+ return colorMode.value === 'dark'
33
+ ? branding.markDark || branding.markLight || ''
34
+ : branding.markLight || branding.markDark || ''
35
+ })
36
+ const showColorModeControl = computed(
37
+ () => !portalRuntimeSettings.value || portalRuntimeSettings.value.appearance?.colorMode === 'user-choice'
38
+ )
39
+ // User store
40
+ const userStore = useUserStore()
41
+ const { currentUser, isAuthenticated, currentSession, myOrganizations, activeOrganization, loadingOrganization } =
42
+ storeToRefs(userStore)
43
+
44
+ const showOrgSwitcherModal = ref(false)
45
+ const searchOpen = ref(false)
46
+ const headerMenuOpen = ref(false)
47
+
48
+ const hasMultipleOrganizations = computed(() => {
49
+ return myOrganizations.value && myOrganizations.value.length > 1
50
+ })
51
+
52
+ // Dummy ref for sidebarOpen (not used in header, but required by composable)
53
+ const sidebarOpen = ref(false)
54
+
55
+ // Get navigation links for search groups
56
+ const { searchGroups } = useNavigationLinks(sidebarOpen)
57
+
58
+ const { modules, moduleNavigationGroups, activeModuleId, activeModule } = useModuleNavigation(headerMenuOpen)
59
+ const expandedMobileModuleId = ref('')
60
+
61
+ watch(
62
+ activeModuleId,
63
+ (moduleId) => {
64
+ expandedMobileModuleId.value = moduleId
65
+ },
66
+ { immediate: true }
67
+ )
68
+
69
+ const toggleMobileModule = (moduleId: string) => {
70
+ expandedMobileModuleId.value = expandedMobileModuleId.value === moduleId ? '' : moduleId
71
+ }
72
+
73
+ const moduleItems = computed(() => {
74
+ return modules.value.map((module) => ({
75
+ ...module,
76
+ active: module.id === activeModuleId.value
77
+ }))
78
+ })
79
+
80
+ const moduleBadgeProps = (badge: (typeof modules.value)[number]['badge']) =>
81
+ typeof badge === 'object' ? badge : { label: badge }
82
+
83
+ // Create a reactive locale ref that's properly initialized
84
+ const currentLocale = ref(locale.value)
85
+
86
+ // Watch for locale changes and handle them properly
87
+ watch(
88
+ locale,
89
+ (newLocale) => {
90
+ currentLocale.value = newLocale
91
+ setLocale(newLocale)
92
+ console.log('Locale changed to:', newLocale)
93
+ // No URL change needed with no_prefix strategy
94
+ },
95
+ { immediate: false }
96
+ )
97
+
98
+ // Watch currentLocale changes to update the global locale
99
+ watch(currentLocale, (newLocale) => {
100
+ setLocale(newLocale)
101
+ // No URL change needed with no_prefix strategy
102
+ })
103
+
104
+ // Impersonation state
105
+ const isImpersonating = computed(() => !!currentSession.value?.impersonatedBy)
106
+
107
+ // Stop impersonating function
108
+ const stopImpersonating = async () => {
109
+ try {
110
+ const { error } = await authClient.admin.stopImpersonating()
111
+ if (error) {
112
+ throw new Error(error.message || t('admin.user.impersonate.stopError'))
113
+ }
114
+ toast.add({
115
+ title: t('common.success'),
116
+ description: t('admin.user.impersonate.stopSuccess'),
117
+ color: 'success'
118
+ })
119
+ // Reload with the restored admin session before opening user management.
120
+ window.location.href = '/admin/users'
121
+ } catch (err: unknown) {
122
+ const errorMessage = err instanceof Error ? err.message : t('admin.user.impersonate.stopError')
123
+ toast.add({
124
+ title: t('common.error'),
125
+ description: errorMessage,
126
+ color: 'error'
127
+ })
128
+ }
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <!-- Impersonation Banner -->
134
+ <div v-if="isImpersonating" class="relative z-50 shrink-0">
135
+ <UAlert
136
+ color="warning"
137
+ orientation="horizontal"
138
+ :title="t('admin.user.impersonate.indicator')"
139
+ :ui="{
140
+ root: 'rounded-none bg-warning-50 px-4 py-2 dark:bg-warning-950',
141
+ wrapper: 'flex-1',
142
+ title: 'text-sm font-medium',
143
+ actions: 'ml-auto'
144
+ }"
145
+ variant="outline"
146
+ >
147
+ <template #actions>
148
+ <UButton color="warning" variant="solid" size="sm" @click="stopImpersonating">
149
+ {{ t('admin.user.impersonate.stop') }}
150
+ </UButton>
151
+ </template>
152
+ </UAlert>
153
+ </div>
154
+
155
+ <UHeader
156
+ v-model:open="headerMenuOpen"
157
+ class="portal-header"
158
+ :title="activeModule?.label || t('menu.module')"
159
+ mode="slideover"
160
+ :menu="{ side: 'right' }"
161
+ :ui="{
162
+ root: 'relative top-auto',
163
+ container: 'w-full max-w-none px-0',
164
+ content: 'w-full max-w-sm',
165
+ body: 'flex h-full flex-col overflow-y-auto p-0 sm:p-0'
166
+ }"
167
+ >
168
+ <template #left>
169
+ <div class="flex items-center gap-3">
170
+ <!-- Logo Icon -->
171
+ <NuxtLink to="/" class="shrink-0" :aria-label="runtimeBrandName">
172
+ <div class="relative">
173
+ <img v-if="runtimeMark" :src="runtimeMark" alt="" class="size-10 rounded-lg object-contain" />
174
+ <svg
175
+ v-else
176
+ class="portal-logo-mark"
177
+ width="40"
178
+ height="40"
179
+ viewBox="0 0 40 40"
180
+ fill="none"
181
+ xmlns="http://www.w3.org/2000/svg"
182
+ aria-hidden="true"
183
+ >
184
+ <!-- Background circle -->
185
+ <circle cx="20" cy="20" r="20" fill="var(--portal-logo-surface)" />
186
+ <!-- Building/facility icon -->
187
+ <rect x="12" y="15" width="4" height="10" fill="var(--portal-logo-ink)" rx="1" />
188
+ <rect x="18" y="12" width="4" height="13" fill="var(--portal-logo-ink)" rx="1" />
189
+ <rect x="24" y="18" width="4" height="7" fill="var(--portal-logo-ink)" rx="1" />
190
+ <!-- Roof/peak -->
191
+ <path d="M10 15 L20 8 L30 15 L28 15 L20 10 L12 15 Z" fill="var(--portal-logo-ink)" />
192
+ <!-- Door -->
193
+ <rect x="18" y="20" width="2" height="5" fill="var(--portal-logo-surface)" />
194
+ </svg>
195
+ </div>
196
+ </NuxtLink>
197
+
198
+ <!-- Neutral fallback wordmark; host shells can replace the layout entirely. -->
199
+ <div class="hidden flex-col sm:flex">
200
+ <span class="portal-wordmark text-2xl font-bold text-gray-900 dark:text-white leading-tight">
201
+ {{ runtimeBrandName }}
202
+ </span>
203
+ <div class="-mt-1 flex items-center gap-1">
204
+ <span class="text-sm leading-tight text-gray-600 dark:text-gray-400">
205
+ {{ runtimeTagline }}
206
+ </span>
207
+ <UButton
208
+ v-if="isAuthenticated && activeOrganization && hasMultipleOrganizations"
209
+ :aria-label="t('menu.switchOrganization')"
210
+ :title="t('menu.switchOrganization')"
211
+ icon="i-lucide-arrow-left-right"
212
+ color="neutral"
213
+ variant="ghost"
214
+ size="xs"
215
+ square
216
+ class="hidden size-6 text-muted hover:text-highlighted lg:inline-flex"
217
+ @click="showOrgSwitcherModal = true"
218
+ />
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </template>
223
+
224
+ <nav v-if="showNavigation" class="hidden lg:flex flex-1 items-center justify-center gap-6">
225
+ <template v-for="module in moduleItems" :key="module.id">
226
+ <NuxtLink
227
+ :to="module.to"
228
+ :class="[
229
+ 'text-sm font-medium transition-colors flex items-center gap-1.5',
230
+ module.active ? 'text-primary font-semibold' : 'text-muted hover:text-highlighted'
231
+ ]"
232
+ >
233
+ <UIcon v-if="module.icon" :name="module.icon" class="w-4 h-4" />
234
+ {{ module.label }}
235
+ <UBadge v-if="module.badge" v-bind="moduleBadgeProps(module.badge)" />
236
+ </NuxtLink>
237
+ </template>
238
+ </nav>
239
+
240
+ <template #right>
241
+ <div class="hidden lg:flex items-center gap-3 ml-auto">
242
+ <UButton icon="i-lucide-search" color="neutral" variant="ghost" size="sm" square @click="searchOpen = true" />
243
+ <ULocaleSelect
244
+ v-model="currentLocale"
245
+ :locales="[en, nl]"
246
+ :ui="{ content: 'w-max min-w-40', itemLabel: 'whitespace-nowrap' }"
247
+ />
248
+ <UColorModeButton v-if="showColorModeControl" />
249
+
250
+ <!-- User Avatar Dropdown (only show when user is logged in) -->
251
+ <AppUserMenu size="sm" />
252
+ </div>
253
+
254
+ <!-- Organization Switcher Modal -->
255
+ <UModal v-model:open="showOrgSwitcherModal" :title="t('menu.switchOrganization')" :ui="{ footer: 'justify-end' }">
256
+ <template #body>
257
+ <OrganizationSwitcher
258
+ v-if="isAuthenticated"
259
+ :show-create-button="false"
260
+ @switched="showOrgSwitcherModal = false"
261
+ />
262
+ </template>
263
+ <template #footer="{ close }">
264
+ <UButton label="Close" color="neutral" variant="outline" @click="close" />
265
+ </template>
266
+ </UModal>
267
+ </template>
268
+
269
+ <template #body>
270
+ <div class="border-b border-default px-4 py-2">
271
+ <div class="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2">
272
+ <UButton
273
+ icon="i-lucide-search"
274
+ :label="t('menu.search')"
275
+ color="neutral"
276
+ variant="outline"
277
+ size="md"
278
+ class="min-h-11 justify-start"
279
+ @click="searchOpen = true"
280
+ />
281
+ <ULocaleSelect
282
+ v-model="currentLocale"
283
+ :locales="[en, nl]"
284
+ class="w-32"
285
+ :ui="{ content: 'w-max min-w-40', itemLabel: 'whitespace-nowrap' }"
286
+ />
287
+ <UColorModeButton v-if="showColorModeControl" size="md" class="min-h-11 min-w-11" />
288
+ </div>
289
+ </div>
290
+
291
+ <nav v-if="showNavigation" :aria-label="t('menu.moduleNavigation')" class="space-y-5 p-5 pt-4">
292
+ <section v-for="module in moduleNavigationGroups" :key="module.id" class="space-y-1">
293
+ <button
294
+ v-if="module.menuItems.length > 1"
295
+ type="button"
296
+ class="flex min-h-11 w-full items-center gap-3 rounded-md px-3 text-left text-base font-bold text-highlighted transition-colors hover:bg-elevated"
297
+ :class="module.id === activeModuleId ? 'bg-primary/10 text-primary' : ''"
298
+ :aria-expanded="expandedMobileModuleId === module.id"
299
+ :aria-controls="`mobile-module-${module.id}`"
300
+ @click="toggleMobileModule(module.id)"
301
+ >
302
+ <UIcon v-if="module.icon" :name="module.icon" class="size-5 shrink-0" />
303
+ <span class="min-w-0 flex-1 truncate">{{ module.label }}</span>
304
+ <UBadge v-if="module.badge" v-bind="moduleBadgeProps(module.badge)" />
305
+ <UIcon
306
+ name="i-lucide-chevron-down"
307
+ class="size-4 shrink-0 transition-transform"
308
+ :class="expandedMobileModuleId === module.id ? 'rotate-180' : ''"
309
+ />
310
+ </button>
311
+ <NuxtLink
312
+ v-else
313
+ :to="module.menuItems[0]?.to ?? module.to"
314
+ class="flex min-h-11 items-center gap-3 rounded-md px-3 text-base font-bold text-highlighted transition-colors hover:bg-elevated"
315
+ :class="module.id === activeModuleId ? 'bg-primary/10 text-primary' : ''"
316
+ @click="headerMenuOpen = false"
317
+ >
318
+ <UIcon
319
+ v-if="module.menuItems[0]?.icon ?? module.icon"
320
+ :name="module.menuItems[0]?.icon ?? module.icon"
321
+ class="size-5 shrink-0"
322
+ />
323
+ <span class="min-w-0 flex-1 truncate">{{ module.menuItems[0]?.label ?? module.label }}</span>
324
+ <UBadge
325
+ v-if="module.menuItems[0]?.badge ?? module.badge"
326
+ v-bind="moduleBadgeProps(module.menuItems[0]?.badge ?? module.badge)"
327
+ />
328
+ </NuxtLink>
329
+ <UNavigationMenu
330
+ v-if="module.menuItems.length > 1 && expandedMobileModuleId === module.id"
331
+ :id="`mobile-module-${module.id}`"
332
+ :items="module.menuItems"
333
+ orientation="vertical"
334
+ class="w-full pl-5"
335
+ :ui="{
336
+ list: 'gap-1 border-l border-default pl-3',
337
+ link: 'min-h-10 rounded-md px-3 text-sm font-medium'
338
+ }"
339
+ />
340
+ </section>
341
+ </nav>
342
+
343
+ <div class="mt-auto border-t border-default p-4">
344
+ <!-- Active Organization Display for Mobile -->
345
+ <UButton
346
+ v-if="isAuthenticated && loadingOrganization"
347
+ variant="ghost"
348
+ size="md"
349
+ disabled
350
+ block
351
+ class="min-h-11 justify-start text-muted"
352
+ >
353
+ <UIcon name="i-lucide-loader-2" class="w-4 h-4 animate-spin" />
354
+ </UButton>
355
+ <div v-else-if="isAuthenticated && activeOrganization" class="flex items-center gap-1">
356
+ <UButton
357
+ variant="ghost"
358
+ size="md"
359
+ icon="i-lucide-building-2"
360
+ class="min-h-11 min-w-0 flex-1 justify-start text-muted hover:text-highlighted"
361
+ @click="showOrgSwitcherModal = true"
362
+ >
363
+ <span class="truncate">{{ activeOrganization.name }}</span>
364
+ </UButton>
365
+ <UButton
366
+ v-if="hasMultipleOrganizations"
367
+ :aria-label="t('menu.switchOrganization')"
368
+ :title="t('menu.switchOrganization')"
369
+ icon="i-lucide-arrow-left-right"
370
+ color="neutral"
371
+ variant="ghost"
372
+ size="md"
373
+ square
374
+ class="min-h-11 min-w-11 shrink-0 text-muted hover:text-highlighted"
375
+ @click="showOrgSwitcherModal = true"
376
+ />
377
+ </div>
378
+
379
+ <div v-if="currentUser" class="mt-2 border-t border-default pt-2">
380
+ <AppUserMenu inline size="md" @navigate="headerMenuOpen = false" />
381
+ </div>
382
+ </div>
383
+ </template>
384
+ </UHeader>
385
+
386
+ <!-- Dashboard Search Modal -->
387
+ <UDashboardSearch
388
+ v-model:open="searchOpen"
389
+ :groups="searchGroups"
390
+ :fuse="{
391
+ fuseOptions: {
392
+ ignoreLocation: true,
393
+ threshold: 0.1,
394
+ keys: ['label', 'suffix', '_searchText']
395
+ }
396
+ }"
397
+ />
398
+ </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ name?: string
5
+ tagline?: string
6
+ }>(),
7
+ {
8
+ name: 'Nuxt Customer Portal',
9
+ tagline: 'Customer workspace'
10
+ }
11
+ )
12
+
13
+ const portalRuntimeSettings = useState<{
14
+ branding?: { portalName?: string; tagline?: string; markLight?: string; markDark?: string }
15
+ } | null>('portal-runtime-settings', () => null)
16
+ const colorMode = useColorMode()
17
+ const runtimeName = computed(() => portalRuntimeSettings.value?.branding?.portalName || props.name)
18
+ const runtimeTagline = computed(() => portalRuntimeSettings.value?.branding?.tagline || props.tagline)
19
+ const runtimeMark = computed(() => {
20
+ const branding = portalRuntimeSettings.value?.branding
21
+ return colorMode.value === 'dark'
22
+ ? branding?.markDark || branding?.markLight || ''
23
+ : branding?.markLight || branding?.markDark || ''
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <div class="flex items-center gap-3">
29
+ <!-- Logo Icon -->
30
+ <div class="relative">
31
+ <img v-if="runtimeMark" :src="runtimeMark" alt="" class="size-10 rounded-lg object-contain" />
32
+ <svg
33
+ v-else
34
+ class="portal-logo-mark"
35
+ width="40"
36
+ height="40"
37
+ viewBox="0 0 40 40"
38
+ fill="none"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ aria-hidden="true"
41
+ >
42
+ <!-- Background circle -->
43
+ <circle cx="20" cy="20" r="20" fill="var(--portal-logo-surface)" />
44
+ <!-- Building/facility icon -->
45
+ <rect x="12" y="15" width="4" height="10" fill="var(--portal-logo-ink)" rx="1" />
46
+ <rect x="18" y="12" width="4" height="13" fill="var(--portal-logo-ink)" rx="1" />
47
+ <rect x="24" y="18" width="4" height="7" fill="var(--portal-logo-ink)" rx="1" />
48
+ <!-- Roof/peak -->
49
+ <path d="M10 15 L20 8 L30 15 L28 15 L20 10 L12 15 Z" fill="var(--portal-logo-ink)" />
50
+ <!-- Door -->
51
+ <rect x="18" y="20" width="2" height="5" fill="var(--portal-logo-surface)" />
52
+ </svg>
53
+ </div>
54
+
55
+ <!-- Company Name -->
56
+ <div class="flex flex-col">
57
+ <span class="portal-wordmark text-2xl font-bold text-gray-900 dark:text-white leading-tight">
58
+ {{ runtimeName }}
59
+ </span>
60
+ <span class="text-sm text-gray-600 dark:text-gray-400 leading-tight -mt-1">
61
+ {{ runtimeTagline }}
62
+ </span>
63
+ </div>
64
+ </div>
65
+ </template>