Skip to content

Commit 0c1bdcb

Browse files
authored
Merge pull request #23 from alexanderop/improve
Update header styling and layout improvements
2 parents f34d1b3 + e091003 commit 0c1bdcb

13 files changed

Lines changed: 289 additions & 165 deletions

File tree

components/BaseGridLayout.vue

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<script setup lang="ts">
2+
interface GridVariant {
3+
name: 'default' | 'with-sidebars' | 'left-sidebar' | 'right-sidebar' | 'centered' | 'full-width'
4+
gridClass: string
5+
mainClass: string
6+
showLeftSidebar: boolean
7+
showRightSidebar: boolean
8+
}
9+
10+
const { variant = 'default' } = defineProps<{
11+
/** Layout variant determining grid structure and sidebar visibility */
12+
variant?: GridVariant['name']
13+
}>()
14+
15+
const slots = useSlots()
16+
17+
const gridVariants: Record<GridVariant['name'], Omit<GridVariant, 'name'>> = {
18+
'centered': {
19+
gridClass: 'grid-cols-1',
20+
mainClass: 'px-4 max-w-prose mx-auto w-full',
21+
showLeftSidebar: false,
22+
showRightSidebar: false,
23+
},
24+
'default': {
25+
gridClass: 'grid-cols-1 sm:grid-cols-5',
26+
mainClass: 'px-4 col-span-1 sm:px-0 sm:col-start-2 sm:col-end-5',
27+
showLeftSidebar: false,
28+
showRightSidebar: false,
29+
},
30+
'full-width': {
31+
gridClass: 'grid-cols-1',
32+
mainClass: 'w-full',
33+
showLeftSidebar: false,
34+
showRightSidebar: false,
35+
},
36+
'left-sidebar': {
37+
gridClass: 'grid-cols-1 sm:grid-cols-5',
38+
mainClass: 'px-4 col-span-1 sm:px-0 sm:col-start-2 sm:col-end-5',
39+
showLeftSidebar: true,
40+
showRightSidebar: false,
41+
},
42+
'right-sidebar': {
43+
gridClass: 'grid-cols-1 sm:grid-cols-5',
44+
mainClass: 'px-4 col-span-1 sm:px-0 sm:col-start-2 sm:col-end-5',
45+
showLeftSidebar: false,
46+
showRightSidebar: true,
47+
},
48+
'with-sidebars': {
49+
gridClass: 'grid-cols-1 sm:grid-cols-5',
50+
mainClass: 'px-4 col-span-1 sm:px-0 sm:col-start-2 sm:col-end-5',
51+
showLeftSidebar: true,
52+
showRightSidebar: true,
53+
},
54+
}
55+
56+
const currentVariant = computed(() => gridVariants[variant])
57+
58+
const hasLeftSidebar = computed(() => {
59+
const hasSlot = !!slots['sidebar-left']
60+
const shouldShow = currentVariant.value.showLeftSidebar
61+
return shouldShow && hasSlot
62+
})
63+
64+
const hasRightSidebar = computed(() => {
65+
const hasSlot = !!slots['sidebar-right']
66+
const shouldShow = currentVariant.value.showRightSidebar
67+
return shouldShow && hasSlot
68+
})
69+
</script>
70+
71+
<template>
72+
<div
73+
class="grid"
74+
:class="currentVariant.gridClass"
75+
>
76+
<!-- Left Sidebar -->
77+
<aside
78+
v-if="hasLeftSidebar"
79+
id="left-sidebar"
80+
class="hidden sm:col-start-1 sm:col-end-2 sm:block"
81+
aria-label="Left sidebar"
82+
>
83+
<div
84+
id="left-sidebar-content"
85+
class="px-4 top-4 sticky"
86+
>
87+
<slot name="sidebar-left" />
88+
</div>
89+
</aside>
90+
91+
<!-- Main Content -->
92+
<main
93+
id="main-content"
94+
:class="currentVariant.mainClass"
95+
>
96+
<slot />
97+
</main>
98+
99+
<!-- Right Sidebar -->
100+
<aside
101+
v-if="hasRightSidebar"
102+
id="right-sidebar"
103+
class="hidden sm:col-start-5 sm:col-end-6 sm:block"
104+
aria-label="Right sidebar"
105+
>
106+
<div
107+
id="right-sidebar-content"
108+
class="px-4 top-4 sticky"
109+
>
110+
<slot name="sidebar-right" />
111+
</div>
112+
</aside>
113+
</div>
114+
</template>

components/TableOfContents.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { tocKey } from './tocKey'
55
66
const { links = [] } = defineProps<{
77
/** Array of table of contents links with hierarchy */
8-
links?: TocLink[]
8+
links?: readonly TocLink[]
99
}>()
1010
1111
const { activeId, scrollToHeading } = useTableOfContents()

components/TheHeader.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ watch(
3232
<header>
3333
<BaseSkipLink />
3434

35-
<div class="bg-[var(--color-header-bg)] w-full">
35+
<div class="bg-[var(--color-background)] w-full">
3636
<div
3737
id="nav-container"
3838
class="px-4 py-4 flex w-full items-center justify-between relative sm:px-6 sm:py-6"
@@ -62,7 +62,7 @@ watch(
6262
menuItemStyles,
6363
// Mobile styles: toggle between 'grid' and 'hidden'
6464
menuOpen
65-
? 'grid mt-4 w-44 grid-cols-2 place-content-center gap-2 absolute top-full left-1/2 -translate-x-1/2 bg-[var(--color-header-bg)] rounded-lg shadow-lg z-50'
65+
? 'grid mt-4 w-44 grid-cols-2 place-content-center gap-2 absolute top-full left-1/2 -translate-x-1/2 bg-[var(--color-background)] rounded-lg shadow-lg z-50'
6666
: 'hidden',
6767
]"
6868
>

composables/useLayoutConfig.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export interface LayoutConfig {
2+
showLeftSidebar: boolean
3+
showRightSidebar: boolean
4+
leftSidebarContent?: any
5+
rightSidebarContent?: any
6+
variant?: 'default' | 'with-sidebars' | 'left-sidebar' | 'right-sidebar' | 'centered' | 'full-width'
7+
}
8+
9+
const layoutConfigKey: InjectionKey<LayoutConfig> = Symbol('layoutConfig')
10+
11+
export function useLayoutConfig() {
12+
const config = inject(layoutConfigKey, {
13+
showLeftSidebar: false,
14+
showRightSidebar: false,
15+
variant: 'default',
16+
})
17+
18+
const setLayoutConfig = (newConfig: Partial<LayoutConfig>) => {
19+
Object.assign(config, newConfig)
20+
}
21+
22+
return {
23+
config: readonly(config),
24+
setLayoutConfig,
25+
}
26+
}
27+
28+
export function provideLayoutConfig(config: LayoutConfig) {
29+
provide(layoutConfigKey, config)
30+
}

composables/useSeo.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function useCanonicalURL(path?: string) {
2222
return `${cleanUrl}${basePath}`
2323
}
2424

25-
export function useEnhancedSeoMeta(options: {
25+
interface SeoMetaOptions {
2626
title?: string
2727
description?: string
2828
image?: string
@@ -34,19 +34,33 @@ export function useEnhancedSeoMeta(options: {
3434
modifiedTime?: string
3535
tags?: string[]
3636
path?: string
37-
}) {
38-
const appConfig = useAppConfig()
39-
const canonicalUrl = useCanonicalURL(options.path)
37+
}
4038

39+
interface AppConfig {
40+
site: {
41+
title: string
42+
desc: string
43+
ogImage?: string
44+
lang: string
45+
author: string
46+
}
47+
socials?: Social[]
48+
}
49+
50+
function buildBaseSeoMeta(
51+
options: SeoMetaOptions,
52+
appConfig: AppConfig,
53+
canonicalUrl: string,
54+
): SeoMetaData {
4155
const metaTitle = options.title ?? appConfig.site.title
4256
const metaDescription = options.description ?? appConfig.site.desc
4357
const metaImage = options.image ?? `/${appConfig.site.ogImage}`
44-
45-
// Default image dimensions for better social media preview
4658
const imageWidth = options.imageWidth ?? 1200
4759
const imageHeight = options.imageHeight ?? 630
4860

49-
const seoMeta: SeoMetaData = {
61+
const twitterHandle = appConfig.socials?.find((s: Social) => s.name === 'Twitter')?.href?.split('/').pop()
62+
63+
return {
5064
author: options.author ?? appConfig.site.author,
5165
description: metaDescription,
5266
ogDescription: metaDescription,
@@ -64,29 +78,42 @@ export function useEnhancedSeoMeta(options: {
6478
twitterDescription: metaDescription,
6579
twitterImage: metaImage,
6680
twitterImageAlt: metaTitle,
67-
twitterSite: appConfig.socials?.find((s: Social) => s.name === 'Twitter')?.href?.split('/').pop(),
81+
twitterSite: twitterHandle,
6882
twitterTitle: metaTitle,
6983
}
84+
}
7085

71-
if (canonicalUrl !== '' && canonicalUrl.length > 0) {
86+
function addArticleMeta(seoMeta: SeoMetaData, options: SeoMetaOptions): void {
87+
if (options.type !== 'article')
88+
return
89+
90+
if (options.author != null)
91+
seoMeta.articleAuthor = [options.author]
92+
if (options.publishedTime != null)
93+
seoMeta.articlePublishedTime = options.publishedTime
94+
if (options.modifiedTime != null)
95+
seoMeta.articleModifiedTime = options.modifiedTime
96+
if (options.tags != null && options.tags.length > 0)
97+
seoMeta.articleTag = options.tags
98+
seoMeta.articleSection = 'Technology'
99+
}
100+
101+
export function useEnhancedSeoMeta(options: SeoMetaOptions) {
102+
const appConfig = useAppConfig()
103+
const canonicalUrl = useCanonicalURL(options.path)
104+
105+
const seoMeta = buildBaseSeoMeta(options, appConfig, canonicalUrl)
106+
107+
const hasCanonicalUrl = canonicalUrl !== '' && canonicalUrl.length > 0
108+
if (hasCanonicalUrl) {
72109
seoMeta.canonical = canonicalUrl
73110
}
74111

75-
if (options.type === 'article') {
76-
if (options.author != null)
77-
seoMeta.articleAuthor = [options.author]
78-
if (options.publishedTime != null)
79-
seoMeta.articlePublishedTime = options.publishedTime
80-
if (options.modifiedTime != null)
81-
seoMeta.articleModifiedTime = options.modifiedTime
82-
if (options.tags != null && options.tags.length > 0)
83-
seoMeta.articleTag = options.tags
84-
seoMeta.articleSection = 'Technology'
85-
}
112+
addArticleMeta(seoMeta, options)
86113

87114
useSeoMeta(seoMeta)
88115

89-
if (canonicalUrl !== '' && canonicalUrl.length > 0) {
116+
if (hasCanonicalUrl) {
90117
useHead({
91118
link: [
92119
{ href: canonicalUrl, rel: 'canonical' },

eslint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default antfu(
1616
},
1717
rules: {
1818
'arrow-spacing': 'error',
19+
'complexity': ['warn', { max: 15 }],
1920
'dot-notation': 'off',
2021
'max-params': 'off',
2122
'no-array-constructor': 'error',
@@ -143,6 +144,7 @@ export default antfu(
143144
'vue/new-line-between-multi-line-property': ['error', {
144145
minLineOfMultilineProperty: 2,
145146
}],
147+
'vue/no-empty-component-block': 'error',
146148
'vue/no-mutating-props': 'error',
147149
'vue/no-ref-object-reactivity-loss': 'error',
148150
'vue/no-required-prop-with-default': ['error', {

layouts/default.vue

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,20 @@
11
<script setup lang="ts">
2-
// Default layout with 5x5 grid structure
2+
// Default layout component providing the standard page structure
3+
// with header, main content area, and footer
34
</script>
45

56
<template>
6-
<div class="font-mono grid grid-rows-[auto_1fr_1fr_1fr_auto] grid-cols-1 min-h-screen sm:grid-cols-5">
7-
<!-- Header - spans full width on mobile, columns 2-4 on desktop -->
8-
<TheHeader class="col-span-1 row-start-1 row-end-2 sm:col-start-2 sm:col-end-5" />
7+
<div class="font-mono flex flex-col min-h-screen">
8+
<div class="grid grid-cols-1 sm:grid-cols-5">
9+
<TheHeader class="col-span-1 sm:col-start-2 sm:col-end-5" />
10+
</div>
911

10-
<!-- Left Sidebar - hidden on mobile, spans row 2-4 in column 1 on desktop -->
11-
<aside
12-
id="left-sidebar"
13-
class="hidden sm:col-start-1 sm:row-start-2 sm:col-end-2 sm:row-end-5 sm:block"
14-
>
15-
<div
16-
id="left-sidebar-content"
17-
class="px-4 top-4 sticky"
18-
/>
19-
</aside>
20-
21-
<!-- Main Content - full width on mobile, spans columns 2-4 on desktop -->
22-
<main
23-
id="main-content"
24-
class="px-4 col-span-1 row-start-2 row-end-5 sm:px-0 sm:col-start-2 sm:col-end-5"
25-
>
12+
<main class="flex-1 w-full">
2613
<slot />
2714
</main>
2815

29-
<!-- Right Sidebar - hidden on mobile, spans row 2-4 in column 5 on desktop -->
30-
<div
31-
id="right-sidebar"
32-
class="hidden sm:col-start-5 sm:row-start-2 sm:col-end-6 sm:row-end-5 sm:block"
33-
/>
34-
35-
<!-- Footer - full width on mobile, spans columns 2-4 on desktop -->
36-
<TheFooter class="col-span-1 row-start-5 row-end-6 sm:col-start-2 sm:col-end-5" />
16+
<div class="grid grid-cols-1 sm:grid-cols-5">
17+
<TheFooter class="col-span-1 sm:col-start-2 sm:col-end-5" />
18+
</div>
3719
</div>
3820
</template>

pages/(home).vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ useStaggeredAnimation()
3737
</script>
3838

3939
<template>
40-
<div>
40+
<BaseGridLayout variant="default">
4141
<article
4242
v-if="page"
4343
class="animate prose-lg max-w-none prose dark:prose-invert"
4444
>
4545
<ContentRenderer :value="page" />
4646
</article>
47-
</div>
47+
</BaseGridLayout>
4848
</template>

pages/[...path].vue

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,20 @@ if (route.path !== '/') {
5353
</script>
5454

5555
<template>
56-
<div v-if="page">
57-
<Breadcrumbs
58-
v-if="route.path !== '/'"
59-
:items="[
60-
{ name: 'Home', url: '/' },
61-
{ name: page?.title || '' },
62-
]"
63-
class="mb-8"
64-
/>
56+
<BaseGridLayout variant="default">
57+
<div v-if="page">
58+
<Breadcrumbs
59+
v-if="route.path !== '/'"
60+
:items="[
61+
{ name: 'Home', url: '/' },
62+
{ name: page?.title || '' },
63+
]"
64+
class="mb-8"
65+
/>
6566

66-
<article class="prose-lg max-w-none prose dark:prose-invert">
67-
<ContentRenderer :value="page" />
68-
</article>
69-
</div>
67+
<article class="prose-lg max-w-none prose dark:prose-invert">
68+
<ContentRenderer :value="page" />
69+
</article>
70+
</div>
71+
</BaseGridLayout>
7072
</template>

0 commit comments

Comments
 (0)