@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koehler8/cms",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- <img :src="logoSrc" :alt="siteName" class="logo-flash-image" />
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
- <div v-if="hasHeaderActions || showLocaleLinks" class="site-header__actions">
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 logoSrc = computed(() => resolveAsset('img/logo.png'));
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;