@levino/shipyard-base 0.5.9 → 0.5.10
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/README.md +1 -1
- package/astro/components/GlobalDesktopNavigation.astro +2 -0
- package/astro/components/LanguageSwitcher.astro +96 -0
- package/astro/components/SidebarNavigation.astro +5 -3
- package/astro/layouts/Markdown.astro +18 -0
- package/astro/layouts/Splash.astro +1 -1
- package/astro/layouts/index.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +15 -7
- package/src/tools/title.test.ts +12 -12
- package/src/virtual.d.ts +10 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { Config } from '../../src/schemas/config'
|
|
3
3
|
import { cn } from '../../src/tools/cn'
|
|
4
|
+
import LanguageSwitcher from './LanguageSwitcher.astro'
|
|
4
5
|
|
|
5
6
|
type Props = Pick<Config, 'brand' | 'navigation'> & { showBrand: boolean }
|
|
6
7
|
|
|
@@ -68,5 +69,6 @@ const { brand, navigation, showBrand = false } = Astro.props as Props
|
|
|
68
69
|
)
|
|
69
70
|
}
|
|
70
71
|
</ul>
|
|
72
|
+
<LanguageSwitcher variant="dropdown" />
|
|
71
73
|
</div>
|
|
72
74
|
</div>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Language switcher component that displays available locales.
|
|
4
|
+
* Reads locales from Astro's i18n config via virtual module.
|
|
5
|
+
*/
|
|
6
|
+
import { locales } from 'virtual:shipyard/locales'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Additional CSS classes */
|
|
10
|
+
class?: string
|
|
11
|
+
/** Variant: 'dropdown' for navbar, 'list' for sidebar */
|
|
12
|
+
variant?: 'dropdown' | 'list'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { class: className, variant = 'dropdown' } = Astro.props
|
|
16
|
+
|
|
17
|
+
const currentLocale = Astro.currentLocale ?? locales[0]
|
|
18
|
+
const currentPath = Astro.url.pathname
|
|
19
|
+
|
|
20
|
+
// Build the path for other locales by replacing the current locale
|
|
21
|
+
const getLocalePath = (locale: string) => {
|
|
22
|
+
if (!Astro.currentLocale) {
|
|
23
|
+
return `/${locale}${currentPath}`
|
|
24
|
+
}
|
|
25
|
+
return currentPath.replace(`/${currentLocale}`, `/${locale}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Locale display names
|
|
29
|
+
const localeNames: Record<string, string> = {
|
|
30
|
+
en: 'English',
|
|
31
|
+
de: 'Deutsch',
|
|
32
|
+
fr: 'Français',
|
|
33
|
+
es: 'Español',
|
|
34
|
+
it: 'Italiano',
|
|
35
|
+
pt: 'Português',
|
|
36
|
+
nl: 'Nederlands',
|
|
37
|
+
pl: 'Polski',
|
|
38
|
+
ru: 'Русский',
|
|
39
|
+
ja: '日本語',
|
|
40
|
+
zh: '中文',
|
|
41
|
+
ko: '한국어',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hasMultipleLocales = locales.length > 1
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{hasMultipleLocales && variant === 'dropdown' && (
|
|
48
|
+
<div class:list={["dropdown dropdown-end", className]} data-testid="language-switcher">
|
|
49
|
+
<div tabindex="0" role="button" class="btn btn-ghost gap-1">
|
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
52
|
+
</svg>
|
|
53
|
+
<span class="hidden sm:inline">{localeNames[currentLocale] ?? currentLocale}</span>
|
|
54
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
56
|
+
</svg>
|
|
57
|
+
</div>
|
|
58
|
+
<ul tabindex="0" class="dropdown-content menu rounded-box z-50 mt-3 w-40 bg-base-100 p-2 shadow">
|
|
59
|
+
{locales.map((locale) => (
|
|
60
|
+
<li>
|
|
61
|
+
<a
|
|
62
|
+
href={getLocalePath(locale)}
|
|
63
|
+
class:list={[{ "active": locale === currentLocale }]}
|
|
64
|
+
>
|
|
65
|
+
{localeNames[locale] ?? locale}
|
|
66
|
+
</a>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{hasMultipleLocales && variant === 'list' && (
|
|
74
|
+
<li class={className} data-testid="language-switcher">
|
|
75
|
+
<details>
|
|
76
|
+
<summary class="gap-2">
|
|
77
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
78
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
79
|
+
</svg>
|
|
80
|
+
{localeNames[currentLocale] ?? currentLocale}
|
|
81
|
+
</summary>
|
|
82
|
+
<ul>
|
|
83
|
+
{locales.map((locale) => (
|
|
84
|
+
<li>
|
|
85
|
+
<a
|
|
86
|
+
href={getLocalePath(locale)}
|
|
87
|
+
class:list={[{ "active": locale === currentLocale }]}
|
|
88
|
+
>
|
|
89
|
+
{localeNames[locale] ?? locale}
|
|
90
|
+
</a>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</details>
|
|
95
|
+
</li>
|
|
96
|
+
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { cn } from '../../src/tools/cn'
|
|
3
|
+
import LanguageSwitcher from './LanguageSwitcher.astro'
|
|
3
4
|
import SidebarElement from './SidebarElement.astro'
|
|
4
5
|
import type { Entry } from './types'
|
|
5
6
|
|
|
@@ -15,13 +16,13 @@ const withLocale = (path: string) =>
|
|
|
15
16
|
Astro.currentLocale ? `/${Astro.currentLocale}${path}` : path
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
<ul class={cn("menu min-h-screen w-56 bg-base-100", { "md:hidden": !local })}>
|
|
19
|
+
<ul class={cn("menu min-h-screen w-56 bg-base-100", { "md:hidden": !local })} data-testid="sidebar-navigation">
|
|
19
20
|
<div>
|
|
20
21
|
<a href={withLocale("/")} class="btn btn-ghost mb-2 text-xl">
|
|
21
22
|
{brand}
|
|
22
23
|
</a>
|
|
23
24
|
</div>
|
|
24
|
-
<div class="block md:hidden">
|
|
25
|
+
<div class="block md:hidden" data-testid="sidebar-global-nav">
|
|
25
26
|
<li>
|
|
26
27
|
{
|
|
27
28
|
local ? (
|
|
@@ -34,7 +35,8 @@ const withLocale = (path: string) =>
|
|
|
34
35
|
)
|
|
35
36
|
}
|
|
36
37
|
</li>
|
|
38
|
+
<LanguageSwitcher variant="list" />
|
|
37
39
|
<div class={cn("divider my-1 block md:hidden", { hidden: !local })}></div>
|
|
38
40
|
</div>
|
|
39
|
-
{local && <SidebarElement entry={local}
|
|
41
|
+
{local && <div data-testid="sidebar-local-nav"><SidebarElement entry={local} /></div>}
|
|
40
42
|
</ul>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavigationTree } from '../../src/schemas/config'
|
|
3
|
+
import Base from './Page.astro'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
frontmatter?: {
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
sidebarNavigation?: NavigationTree
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = Astro.props
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Base {...props}>
|
|
17
|
+
<div class="prose mx-auto"><slot /></div>
|
|
18
|
+
</Base>
|
package/astro/layouts/index.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,27 +7,35 @@ export { getTitle } from './tools/title'
|
|
|
7
7
|
export * from './types'
|
|
8
8
|
|
|
9
9
|
const shipyardConfigId = 'virtual:shipyard/config'
|
|
10
|
+
const shipyardLocalesId = 'virtual:shipyard/locales'
|
|
10
11
|
|
|
11
12
|
const resolveId: Record<string, string | undefined> = {
|
|
12
13
|
[shipyardConfigId]: `${shipyardConfigId}`,
|
|
14
|
+
[shipyardLocalesId]: `${shipyardLocalesId}`,
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const load = (config: Config) =>
|
|
16
|
-
({
|
|
17
|
-
[shipyardConfigId]: `export default ${JSON.stringify(config)}`,
|
|
18
|
-
}) as Record<string, string | undefined>
|
|
19
|
-
|
|
20
17
|
export default (config: Config): AstroIntegration => ({
|
|
21
18
|
name: 'shipyard',
|
|
22
19
|
hooks: {
|
|
23
|
-
'astro:config:setup': ({ updateConfig }) => {
|
|
20
|
+
'astro:config:setup': ({ updateConfig, config: astroConfig }) => {
|
|
21
|
+
// Extract locales from Astro's i18n config
|
|
22
|
+
const locales = astroConfig.i18n?.locales ?? []
|
|
23
|
+
const localeList = locales.map((locale) =>
|
|
24
|
+
typeof locale === 'string' ? locale : locale.path,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const load = {
|
|
28
|
+
[shipyardConfigId]: `export default ${JSON.stringify(config)}`,
|
|
29
|
+
[shipyardLocalesId]: `export const locales = ${JSON.stringify(localeList)}; export default ${JSON.stringify(localeList)};`,
|
|
30
|
+
} as Record<string, string | undefined>
|
|
31
|
+
|
|
24
32
|
updateConfig({
|
|
25
33
|
vite: {
|
|
26
34
|
plugins: [
|
|
27
35
|
{
|
|
28
36
|
name: 'shipyard',
|
|
29
37
|
resolveId: (id: string) => resolveId[id],
|
|
30
|
-
load: (id: string) => load
|
|
38
|
+
load: (id: string) => load[id],
|
|
31
39
|
},
|
|
32
40
|
],
|
|
33
41
|
},
|
package/src/tools/title.test.ts
CHANGED
|
@@ -2,45 +2,45 @@ import { describe, expect, test } from 'vitest'
|
|
|
2
2
|
import { getTitle } from './title'
|
|
3
3
|
|
|
4
4
|
describe('getTitle', () => {
|
|
5
|
-
const siteTitle = '
|
|
5
|
+
const siteTitle = 'shipyard'
|
|
6
6
|
|
|
7
7
|
test('returns site title when page title is undefined', () => {
|
|
8
|
-
expect(getTitle(siteTitle, undefined)).toBe('
|
|
8
|
+
expect(getTitle(siteTitle, undefined)).toBe('shipyard')
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
test('returns site title when page title is null', () => {
|
|
12
|
-
expect(getTitle(siteTitle, null)).toBe('
|
|
12
|
+
expect(getTitle(siteTitle, null)).toBe('shipyard')
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
test('returns site title when page title is empty', () => {
|
|
16
|
-
expect(getTitle(siteTitle, '')).toBe('
|
|
16
|
+
expect(getTitle(siteTitle, '')).toBe('shipyard')
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('returns site title when page title equals site title', () => {
|
|
20
|
-
expect(getTitle(siteTitle, '
|
|
20
|
+
expect(getTitle(siteTitle, 'shipyard')).toBe('shipyard')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
test('returns combined title when page title differs', () => {
|
|
24
|
-
expect(getTitle(siteTitle, 'Blog')).toBe('
|
|
24
|
+
expect(getTitle(siteTitle, 'Blog')).toBe('shipyard - Blog')
|
|
25
25
|
expect(getTitle(siteTitle, 'First Blog Post')).toBe(
|
|
26
|
-
'
|
|
26
|
+
'shipyard - First Blog Post',
|
|
27
27
|
)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
test('handles whitespace-only page titles', () => {
|
|
31
|
-
expect(getTitle(siteTitle, ' ')).toBe('
|
|
31
|
+
expect(getTitle(siteTitle, ' ')).toBe('shipyard')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('trims page title before comparison', () => {
|
|
35
|
-
expect(getTitle(siteTitle, '
|
|
36
|
-
expect(getTitle(siteTitle, ' Blog ')).toBe('
|
|
35
|
+
expect(getTitle(siteTitle, ' shipyard ')).toBe('shipyard')
|
|
36
|
+
expect(getTitle(siteTitle, ' Blog ')).toBe('shipyard - Blog')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
test('is case sensitive', () => {
|
|
40
|
-
expect(getTitle(siteTitle, '
|
|
40
|
+
expect(getTitle(siteTitle, 'SHIPYARD')).toBe('shipyard - SHIPYARD')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
test('handles special characters', () => {
|
|
44
|
-
expect(getTitle(siteTitle, 'FAQ & Help')).toBe('
|
|
44
|
+
expect(getTitle(siteTitle, 'FAQ & Help')).toBe('shipyard - FAQ & Help')
|
|
45
45
|
})
|
|
46
46
|
})
|
package/src/virtual.d.ts
ADDED