@koehler8/cms 1.0.0-beta.12 → 1.0.0-beta.13
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/package.json +1 -1
- package/src/components/Header.vue +113 -4
package/package.json
CHANGED
|
@@ -12,9 +12,42 @@
|
|
|
12
12
|
<div class="container">
|
|
13
13
|
<div class="site-header__inner">
|
|
14
14
|
<a href="/" ref="logoLinkRef" class="site-header__logo logo-flash-trigger">
|
|
15
|
-
|
|
15
|
+
<!--
|
|
16
|
+
Logo slot. Default rendering: <img> if site/assets/img/logo.png
|
|
17
|
+
exists or `content.header.logoSrc` is set, otherwise text from
|
|
18
|
+
`content.header.logoText` or the site title. Sites that wrap
|
|
19
|
+
Header in a parent component can override entirely via
|
|
20
|
+
<template #logo>.
|
|
21
|
+
-->
|
|
22
|
+
<slot name="logo" :src="logoSrc" :text="logoText" :site-name="siteName">
|
|
23
|
+
<img
|
|
24
|
+
v-if="logoSrc"
|
|
25
|
+
:src="logoSrc"
|
|
26
|
+
:alt="siteName"
|
|
27
|
+
class="logo-flash-image"
|
|
28
|
+
/>
|
|
29
|
+
<span v-else class="site-header__logo-text">{{ logoText || siteName }}</span>
|
|
30
|
+
</slot>
|
|
16
31
|
</a>
|
|
17
|
-
|
|
32
|
+
<!--
|
|
33
|
+
Nav slot. Defaults to a horizontal <ul> of links built from
|
|
34
|
+
`content.header.navItems[]` (each entry: { text, href, target? }).
|
|
35
|
+
No items, no nav.
|
|
36
|
+
-->
|
|
37
|
+
<slot name="nav" :items="navItems">
|
|
38
|
+
<ul v-if="navItems.length" class="site-header__nav-list">
|
|
39
|
+
<li v-for="(item, idx) in navItems" :key="`${item.href || item.text}-${idx}`">
|
|
40
|
+
<a
|
|
41
|
+
:href="item.href"
|
|
42
|
+
:target="item.target || null"
|
|
43
|
+
:rel="item.target === '_blank' ? 'noopener noreferrer' : null"
|
|
44
|
+
>{{ item.text }}</a>
|
|
45
|
+
</li>
|
|
46
|
+
</ul>
|
|
47
|
+
</slot>
|
|
48
|
+
<div v-if="hasHeaderActions || showLocaleLinks || hasActionsSlot" class="site-header__actions">
|
|
49
|
+
<!-- Site-provided extra actions, rendered before the locale dropdown. -->
|
|
50
|
+
<slot name="actions" />
|
|
18
51
|
<component
|
|
19
52
|
v-if="headerActionsComponent"
|
|
20
53
|
:is="headerActionsComponent"
|
|
@@ -80,14 +113,15 @@
|
|
|
80
113
|
</template>
|
|
81
114
|
|
|
82
115
|
<script setup>
|
|
83
|
-
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
116
|
+
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue';
|
|
84
117
|
import { useRoute } from 'vue-router';
|
|
85
118
|
import { resolveAsset } from '../utils/assetResolver.js';
|
|
86
119
|
import { trackEvent } from '../utils/analytics.js';
|
|
87
120
|
import { getExtensionComponent } from '../extensions/extensionLoader.js';
|
|
88
121
|
import { availableLocales as siteLocales } from '../utils/loadConfig.js';
|
|
89
122
|
|
|
90
|
-
const
|
|
123
|
+
const slots = useSlots();
|
|
124
|
+
const hasActionsSlot = computed(() => Boolean(slots.actions));
|
|
91
125
|
|
|
92
126
|
const isLangOpen = ref(false);
|
|
93
127
|
const isHeaderCompact = ref(false);
|
|
@@ -231,6 +265,40 @@ const updateLocaleMenuPosition = () => {
|
|
|
231
265
|
|
|
232
266
|
const siteName = computed(() => injectedSiteData.value?.site?.title || '');
|
|
233
267
|
|
|
268
|
+
// Header content (merged from shared.json + per-page overrides). Three
|
|
269
|
+
// optional knobs sites can use to customise the bundled Header without
|
|
270
|
+
// wrapping it in a parent component:
|
|
271
|
+
// - content.header.logoText → text shown in place of the site title
|
|
272
|
+
// - content.header.logoSrc → full URL to use as the logo image
|
|
273
|
+
// (overrides the site/assets/img/logo.png
|
|
274
|
+
// asset resolution)
|
|
275
|
+
// - content.header.navItems → array of { text, href, target? } to
|
|
276
|
+
// render as a top-level nav menu
|
|
277
|
+
const headerContent = computed(() => {
|
|
278
|
+
const fromPage = pageContent.value?.header;
|
|
279
|
+
if (fromPage && typeof fromPage === 'object') return fromPage;
|
|
280
|
+
const fromSite = injectedSiteData.value?.header;
|
|
281
|
+
return (fromSite && typeof fromSite === 'object') ? fromSite : {};
|
|
282
|
+
});
|
|
283
|
+
const logoText = computed(() => {
|
|
284
|
+
const t = headerContent.value?.logoText;
|
|
285
|
+
return typeof t === 'string' && t.trim() ? t.trim() : '';
|
|
286
|
+
});
|
|
287
|
+
const logoSrc = computed(() => {
|
|
288
|
+
const customSrc = headerContent.value?.logoSrc;
|
|
289
|
+
if (typeof customSrc === 'string' && customSrc.trim()) {
|
|
290
|
+
return customSrc.trim();
|
|
291
|
+
}
|
|
292
|
+
// Fall back to the conventional site/assets/img/logo.png. resolveAsset
|
|
293
|
+
// returns '' when the asset doesn't exist; we treat that as "no image"
|
|
294
|
+
// and the template renders the text fallback instead of a broken <img>.
|
|
295
|
+
return resolveAsset('img/logo.png') || '';
|
|
296
|
+
});
|
|
297
|
+
const navItems = computed(() => {
|
|
298
|
+
const items = headerContent.value?.navItems;
|
|
299
|
+
return Array.isArray(items) ? items.filter((it) => it && typeof it === 'object') : [];
|
|
300
|
+
});
|
|
301
|
+
|
|
234
302
|
const resolveHeaderSettings = () => {
|
|
235
303
|
const pageHeader = pageContent.value?.header;
|
|
236
304
|
if (pageHeader && typeof pageHeader === 'object') {
|
|
@@ -396,6 +464,47 @@ const updateLocaleMenuPosition = () => {
|
|
|
396
464
|
width: auto;
|
|
397
465
|
}
|
|
398
466
|
|
|
467
|
+
/* Text fallback when no logo asset / logoSrc is provided. Uses theme tokens
|
|
468
|
+
so the active theme drives appearance; consumers can override via scoped
|
|
469
|
+
CSS in a wrapping component or a brand-var override. */
|
|
470
|
+
.site-header__logo-text {
|
|
471
|
+
font-family: var(--ui-font-display, var(--ui-font-body, inherit));
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
|
474
|
+
letter-spacing: 0.02em;
|
|
475
|
+
color: var(--brand-header-text, var(--brand-fg-100, inherit));
|
|
476
|
+
white-space: nowrap;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Nav menu rendered from content.header.navItems[]. Sits between the logo
|
|
480
|
+
and the actions cluster. Hidden on narrow viewports — sites that need
|
|
481
|
+
responsive nav should override the slot. */
|
|
482
|
+
.site-header__nav-list {
|
|
483
|
+
display: none;
|
|
484
|
+
list-style: none;
|
|
485
|
+
margin: 0;
|
|
486
|
+
padding: 0;
|
|
487
|
+
gap: clamp(0.75rem, 2vw, 1.75rem);
|
|
488
|
+
align-items: center;
|
|
489
|
+
}
|
|
490
|
+
@media (min-width: 720px) {
|
|
491
|
+
.site-header__nav-list {
|
|
492
|
+
display: flex;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
.site-header__nav-list a {
|
|
496
|
+
color: var(--brand-header-text, var(--brand-fg-100, inherit));
|
|
497
|
+
text-decoration: none;
|
|
498
|
+
font-weight: 500;
|
|
499
|
+
font-size: 0.95rem;
|
|
500
|
+
transition: opacity 0.15s ease;
|
|
501
|
+
}
|
|
502
|
+
.site-header__nav-list a:hover,
|
|
503
|
+
.site-header__nav-list a:focus-visible {
|
|
504
|
+
opacity: 0.7;
|
|
505
|
+
text-decoration: underline;
|
|
506
|
+
}
|
|
507
|
+
|
|
399
508
|
.site-header__actions {
|
|
400
509
|
display: inline-flex;
|
|
401
510
|
align-items: center;
|