@levino/shipyard-base 0.6.1 → 0.6.2
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/astro/components/Footer.astro +173 -19
- package/astro/layouts/Footer.astro +2 -11
- package/package.json +1 -1
- package/src/schemas/config.ts +110 -0
|
@@ -1,26 +1,180 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { i18n } from 'astro:config/server'
|
|
3
|
+
import type {
|
|
4
|
+
FooterConfig,
|
|
5
|
+
FooterLink,
|
|
6
|
+
FooterLinkColumn,
|
|
7
|
+
} from '../../src/schemas/config'
|
|
8
|
+
|
|
2
9
|
interface FooterProps {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
config?: FooterConfig
|
|
11
|
+
hideBranding?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { config, hideBranding = false } = Astro.props as FooterProps
|
|
15
|
+
|
|
16
|
+
const withLocale = (path: string) => {
|
|
17
|
+
if (!i18n || !Astro.currentLocale) return path
|
|
18
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
19
|
+
return `/${Astro.currentLocale}${normalizedPath}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Type guard to check if a link item is a column
|
|
23
|
+
function isColumn(
|
|
24
|
+
item: FooterLink | FooterLinkColumn,
|
|
25
|
+
): item is FooterLinkColumn {
|
|
26
|
+
return 'items' in item && Array.isArray(item.items)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if we have multi-column layout
|
|
30
|
+
const hasColumns = config?.links?.some(isColumn) ?? false
|
|
31
|
+
|
|
32
|
+
// Get link href with locale support
|
|
33
|
+
function getLinkHref(link: FooterLink): string {
|
|
34
|
+
if (link.href) return link.href
|
|
35
|
+
if (link.to) return withLocale(link.to)
|
|
36
|
+
return '#'
|
|
12
37
|
}
|
|
13
38
|
|
|
14
|
-
|
|
39
|
+
// Check if link is external
|
|
40
|
+
function isExternal(link: FooterLink): boolean {
|
|
41
|
+
return (
|
|
42
|
+
!!link.href &&
|
|
43
|
+
(link.href.startsWith('http://') || link.href.startsWith('https://'))
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if a URL is external
|
|
48
|
+
function isExternalUrl(url: string): boolean {
|
|
49
|
+
return url.startsWith('http://') || url.startsWith('https://')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get logo href with locale support for internal links
|
|
53
|
+
function getLogoHref(href: string): string {
|
|
54
|
+
if (isExternalUrl(href)) return href
|
|
55
|
+
return withLocale(href)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isDark = config?.style === 'dark'
|
|
15
59
|
---
|
|
16
60
|
|
|
17
|
-
<footer
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
61
|
+
<footer
|
|
62
|
+
class:list={[
|
|
63
|
+
'w-full px-6 py-8 text-sm',
|
|
64
|
+
isDark ? 'bg-neutral text-neutral-content' : 'bg-base-200 text-base-content',
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
<div class="mx-auto max-w-7xl">
|
|
68
|
+
{/* Logo */}
|
|
69
|
+
{config?.logo && (
|
|
70
|
+
<div class="mb-6">
|
|
71
|
+
{config.logo.href ? (
|
|
72
|
+
<a
|
|
73
|
+
href={getLogoHref(config.logo.href)}
|
|
74
|
+
{...(isExternalUrl(config.logo.href) ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
75
|
+
>
|
|
76
|
+
{config.logo.srcDark ? (
|
|
77
|
+
<picture>
|
|
78
|
+
<source srcset={config.logo.srcDark} media="(prefers-color-scheme: dark)" />
|
|
79
|
+
<img
|
|
80
|
+
src={config.logo.src}
|
|
81
|
+
alt={config.logo.alt}
|
|
82
|
+
width={config.logo.width}
|
|
83
|
+
height={config.logo.height}
|
|
84
|
+
class="h-10 w-auto"
|
|
85
|
+
/>
|
|
86
|
+
</picture>
|
|
87
|
+
) : (
|
|
88
|
+
<img
|
|
89
|
+
src={config.logo.src}
|
|
90
|
+
alt={config.logo.alt}
|
|
91
|
+
width={config.logo.width}
|
|
92
|
+
height={config.logo.height}
|
|
93
|
+
class="h-10 w-auto"
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</a>
|
|
97
|
+
) : config.logo.srcDark ? (
|
|
98
|
+
<picture>
|
|
99
|
+
<source srcset={config.logo.srcDark} media="(prefers-color-scheme: dark)" />
|
|
100
|
+
<img
|
|
101
|
+
src={config.logo.src}
|
|
102
|
+
alt={config.logo.alt}
|
|
103
|
+
width={config.logo.width}
|
|
104
|
+
height={config.logo.height}
|
|
105
|
+
class="h-10 w-auto"
|
|
106
|
+
/>
|
|
107
|
+
</picture>
|
|
108
|
+
) : (
|
|
109
|
+
<img
|
|
110
|
+
src={config.logo.src}
|
|
111
|
+
alt={config.logo.alt}
|
|
112
|
+
width={config.logo.width}
|
|
113
|
+
height={config.logo.height}
|
|
114
|
+
class="h-10 w-auto"
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Links */}
|
|
121
|
+
{config?.links && config.links.length > 0 && (
|
|
122
|
+
hasColumns ? (
|
|
123
|
+
<div class="mb-8 grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-4">
|
|
124
|
+
{config.links.filter(isColumn).map((item) => (
|
|
125
|
+
<div>
|
|
126
|
+
<h3 class="mb-4 font-semibold">{item.title}</h3>
|
|
127
|
+
<ul class="space-y-2">
|
|
128
|
+
{item.items.map((link) => (
|
|
129
|
+
<li>
|
|
130
|
+
<a
|
|
131
|
+
href={getLinkHref(link)}
|
|
132
|
+
class="opacity-80 transition-opacity hover:opacity-100"
|
|
133
|
+
{...(isExternal(link) ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
134
|
+
>
|
|
135
|
+
{link.label}
|
|
136
|
+
</a>
|
|
137
|
+
</li>
|
|
138
|
+
))}
|
|
139
|
+
</ul>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div class="mb-6 flex flex-wrap gap-x-6 gap-y-2">
|
|
145
|
+
{config.links.map((item) =>
|
|
146
|
+
!isColumn(item) && (
|
|
147
|
+
<a
|
|
148
|
+
href={getLinkHref(item)}
|
|
149
|
+
class="opacity-80 transition-opacity hover:opacity-100"
|
|
150
|
+
{...(isExternal(item) ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
151
|
+
>
|
|
152
|
+
{item.label}
|
|
153
|
+
</a>
|
|
154
|
+
)
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Bottom bar: copyright and branding */}
|
|
161
|
+
<div class="flex flex-wrap items-center justify-between gap-4 border-t border-current/10 pt-6">
|
|
162
|
+
{config?.copyright ? (
|
|
163
|
+
<div set:html={config.copyright} />
|
|
164
|
+
) : (
|
|
165
|
+
<div>© {new Date().getFullYear()}</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{!hideBranding && (
|
|
169
|
+
<a
|
|
170
|
+
href="https://github.com/levino/shipyard"
|
|
171
|
+
target="_blank"
|
|
172
|
+
rel="noopener noreferrer"
|
|
173
|
+
class="opacity-60 transition-opacity hover:opacity-100"
|
|
174
|
+
>
|
|
175
|
+
Built with Shipyard
|
|
176
|
+
</a>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
26
180
|
</footer>
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import config from 'virtual:shipyard/config'
|
|
2
3
|
import { Footer as FooterComponent } from '../components'
|
|
3
|
-
|
|
4
|
-
const withLocale = (path: string) =>
|
|
5
|
-
Astro.currentLocale ? `/${Astro.currentLocale}${path}` : path
|
|
6
4
|
---
|
|
7
5
|
|
|
8
|
-
<FooterComponent
|
|
9
|
-
links={[]}
|
|
10
|
-
copyright={{
|
|
11
|
-
href: "https://github.com/levino",
|
|
12
|
-
label: "Levin Keller",
|
|
13
|
-
year: 2025,
|
|
14
|
-
}}
|
|
15
|
-
/>
|
|
6
|
+
<FooterComponent config={config.footer} hideBranding={config.hideBranding} />
|
package/package.json
CHANGED
package/src/schemas/config.ts
CHANGED
|
@@ -14,6 +14,105 @@ export type NavigationTree = Record<string, NavigationEntry>
|
|
|
14
14
|
|
|
15
15
|
export type Script = string | astroHTML.JSX.IntrinsicElements['script']
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A single footer link.
|
|
19
|
+
* Exactly one of `to` (internal link) or `href` (external link) must be provided.
|
|
20
|
+
*/
|
|
21
|
+
export type FooterLink =
|
|
22
|
+
| {
|
|
23
|
+
/**
|
|
24
|
+
* Display text for the link.
|
|
25
|
+
*/
|
|
26
|
+
label: string
|
|
27
|
+
/**
|
|
28
|
+
* Client-side routing path. Use for internal links.
|
|
29
|
+
*/
|
|
30
|
+
to: string
|
|
31
|
+
href?: never
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
/**
|
|
35
|
+
* Display text for the link.
|
|
36
|
+
*/
|
|
37
|
+
label: string
|
|
38
|
+
/**
|
|
39
|
+
* Full URL for external links.
|
|
40
|
+
*/
|
|
41
|
+
href: string
|
|
42
|
+
to?: never
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A column of footer links with a title.
|
|
47
|
+
* Used for multi-column footer layouts.
|
|
48
|
+
*/
|
|
49
|
+
export interface FooterLinkColumn {
|
|
50
|
+
/**
|
|
51
|
+
* Column title displayed above the links.
|
|
52
|
+
*/
|
|
53
|
+
title: string
|
|
54
|
+
/**
|
|
55
|
+
* Links within this column.
|
|
56
|
+
*/
|
|
57
|
+
items: FooterLink[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Footer logo configuration.
|
|
62
|
+
*/
|
|
63
|
+
export interface FooterLogo {
|
|
64
|
+
/**
|
|
65
|
+
* Alt text for the logo image.
|
|
66
|
+
*/
|
|
67
|
+
alt: string
|
|
68
|
+
/**
|
|
69
|
+
* Logo image source URL.
|
|
70
|
+
*/
|
|
71
|
+
src: string
|
|
72
|
+
/**
|
|
73
|
+
* Optional dark mode logo source.
|
|
74
|
+
*/
|
|
75
|
+
srcDark?: string
|
|
76
|
+
/**
|
|
77
|
+
* Link URL when clicking the logo.
|
|
78
|
+
*/
|
|
79
|
+
href?: string
|
|
80
|
+
/**
|
|
81
|
+
* Logo width in pixels.
|
|
82
|
+
*/
|
|
83
|
+
width?: number
|
|
84
|
+
/**
|
|
85
|
+
* Logo height in pixels.
|
|
86
|
+
*/
|
|
87
|
+
height?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Footer configuration.
|
|
92
|
+
* Supports both simple (single row of links) and multi-column layouts.
|
|
93
|
+
*/
|
|
94
|
+
export interface FooterConfig {
|
|
95
|
+
/**
|
|
96
|
+
* Footer color theme.
|
|
97
|
+
* @default 'light'
|
|
98
|
+
*/
|
|
99
|
+
style?: 'light' | 'dark'
|
|
100
|
+
/**
|
|
101
|
+
* Footer links. Can be a flat array of links (simple footer)
|
|
102
|
+
* or an array of columns with titles (multi-column footer).
|
|
103
|
+
*/
|
|
104
|
+
links?: (FooterLink | FooterLinkColumn)[]
|
|
105
|
+
/**
|
|
106
|
+
* Copyright notice displayed at the bottom of the footer.
|
|
107
|
+
* Supports HTML for links and formatting.
|
|
108
|
+
*/
|
|
109
|
+
copyright?: string
|
|
110
|
+
/**
|
|
111
|
+
* Optional footer logo.
|
|
112
|
+
*/
|
|
113
|
+
logo?: FooterLogo
|
|
114
|
+
}
|
|
115
|
+
|
|
17
116
|
/**
|
|
18
117
|
* Announcement bar configuration.
|
|
19
118
|
* Displays a dismissible banner above the navbar.
|
|
@@ -89,6 +188,17 @@ export interface Config {
|
|
|
89
188
|
* Shows a dismissible banner at the top of the page.
|
|
90
189
|
*/
|
|
91
190
|
announcementBar?: AnnouncementBar
|
|
191
|
+
/**
|
|
192
|
+
* Footer configuration.
|
|
193
|
+
* Customize footer links, copyright notice, and styling.
|
|
194
|
+
*/
|
|
195
|
+
footer?: FooterConfig
|
|
196
|
+
/**
|
|
197
|
+
* Hide the "Built with Shipyard" branding in the footer.
|
|
198
|
+
* Set to true to remove the branding link.
|
|
199
|
+
* @default false
|
|
200
|
+
*/
|
|
201
|
+
hideBranding?: boolean
|
|
92
202
|
/**
|
|
93
203
|
* The behavior of Shipyard when it detects any broken internal link.
|
|
94
204
|
* By default, it logs a warning. Set to 'throw' to fail the build on broken links.
|