@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.
@@ -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
- links: {
4
- label: string
5
- href: string
6
- }[]
7
- copyright: {
8
- label: string
9
- href: string
10
- year: number
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
- const { links, copyright } = Astro.props as FooterProps
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 class="flex w-full items-center justify-between px-6 py-4 text-sm font-medium">
18
- <a href={copyright.href} target="_blank">
19
- © {copyright.label}, {copyright.year}
20
- </a>
21
- {links.map(({ label, href }) => (
22
- <a href={href}>
23
- {label}
24
- </a>
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>&copy; {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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levino/shipyard-base",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Core layouts, components, and configuration for shipyard - a composable page builder for Astro",
5
5
  "keywords": [
6
6
  "astro",
@@ -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.