@levino/shipyard-base 0.0.0-rc-20251122114952
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/Breadcrumbs.astro +63 -0
- package/astro/components/Footer.astro +26 -0
- package/astro/components/GlobalDesktopNavigation.astro +72 -0
- package/astro/components/LocalNavigation.astro +12 -0
- package/astro/components/SidebarElement.astro +45 -0
- package/astro/components/SidebarNavigation.astro +40 -0
- package/astro/components/TableOfContents.astro +84 -0
- package/astro/components/index.ts +7 -0
- package/astro/components/types.ts +10 -0
- package/astro/layouts/Footer.astro +15 -0
- package/astro/layouts/Page.astro +103 -0
- package/astro/layouts/Splash.astro +18 -0
- package/astro/layouts/index.ts +3 -0
- package/package.json +41 -0
- package/src/astro.ts +2 -0
- package/src/globals.css +5 -0
- package/src/index.ts +37 -0
- package/src/schemas/config.ts +18 -0
- package/src/tools/cn.ts +4 -0
- package/src/tools/title.test.ts +46 -0
- package/src/tools/title.ts +11 -0
- package/src/types.ts +34 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Entry } from './types'
|
|
3
|
+
|
|
4
|
+
interface BreadcrumbItem {
|
|
5
|
+
label: string
|
|
6
|
+
href?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
navigation: Entry
|
|
11
|
+
currentPath?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { navigation, currentPath = Astro.url.pathname } = Astro.props
|
|
15
|
+
|
|
16
|
+
// Normalize path by removing trailing slash for comparison
|
|
17
|
+
const normalizePath = (path: string) => path.replace(/\/$/, '') || '/'
|
|
18
|
+
|
|
19
|
+
// Find the path to the current page in the navigation tree
|
|
20
|
+
const findBreadcrumbs = (
|
|
21
|
+
entry: Entry,
|
|
22
|
+
targetPath: string,
|
|
23
|
+
path: BreadcrumbItem[] = [],
|
|
24
|
+
): BreadcrumbItem[] | null => {
|
|
25
|
+
for (const [key, item] of Object.entries(entry)) {
|
|
26
|
+
const label = item.label ?? key
|
|
27
|
+
const currentItem: BreadcrumbItem = { label, href: item.href }
|
|
28
|
+
|
|
29
|
+
if (item.href && normalizePath(item.href) === normalizePath(targetPath)) {
|
|
30
|
+
return [...path, currentItem]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (item.subEntry) {
|
|
34
|
+
const result = findBreadcrumbs(item.subEntry, targetPath, [
|
|
35
|
+
...path,
|
|
36
|
+
currentItem,
|
|
37
|
+
])
|
|
38
|
+
if (result) return result
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const breadcrumbs = findBreadcrumbs(navigation, currentPath) ?? []
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
breadcrumbs.length > 0 && (
|
|
49
|
+
<div class="breadcrumbs text-sm">
|
|
50
|
+
<ul>
|
|
51
|
+
{breadcrumbs.map((item, index) => (
|
|
52
|
+
<li>
|
|
53
|
+
{item.href && index < breadcrumbs.length - 1 ? (
|
|
54
|
+
<a href={item.href}>{item.label}</a>
|
|
55
|
+
) : (
|
|
56
|
+
<span>{item.label}</span>
|
|
57
|
+
)}
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface FooterProps {
|
|
3
|
+
links: {
|
|
4
|
+
label: string
|
|
5
|
+
href: string
|
|
6
|
+
}[]
|
|
7
|
+
copyright: {
|
|
8
|
+
label: string
|
|
9
|
+
href: string
|
|
10
|
+
year: number
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { links, copyright } = Astro.props as FooterProps
|
|
15
|
+
---
|
|
16
|
+
|
|
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
|
+
))}
|
|
26
|
+
</footer>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Config } from '../../src/schemas/config'
|
|
3
|
+
import { cn } from '../../src/tools/cn'
|
|
4
|
+
|
|
5
|
+
type Props = Pick<Config, 'brand' | 'navigation'> & { showBrand: boolean }
|
|
6
|
+
|
|
7
|
+
const { brand, navigation, showBrand = false } = Astro.props as Props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class="navbar z-10 bg-base-100">
|
|
11
|
+
<span
|
|
12
|
+
class="tooltip tooltip-bottom before:text-xs before:content-[attr(data-tip)]"
|
|
13
|
+
data-tip="Menu"
|
|
14
|
+
>
|
|
15
|
+
<label
|
|
16
|
+
aria-label="Open menu"
|
|
17
|
+
for="drawer"
|
|
18
|
+
class="btn btn-square btn-ghost drawer-button lg:hidden"
|
|
19
|
+
>
|
|
20
|
+
<svg
|
|
21
|
+
width="20"
|
|
22
|
+
height="20"
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
fill="none"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
class="inline-block h-5 w-5 stroke-current md:h-6 md:w-6"
|
|
27
|
+
>
|
|
28
|
+
<path
|
|
29
|
+
stroke-linecap="round"
|
|
30
|
+
stroke-linejoin="round"
|
|
31
|
+
stroke-width="2"
|
|
32
|
+
d="M4 6h16M4 12h16M4 18h16"></path>
|
|
33
|
+
</svg>
|
|
34
|
+
</label>
|
|
35
|
+
</span>
|
|
36
|
+
<div class="flex-1">
|
|
37
|
+
<a
|
|
38
|
+
href="/"
|
|
39
|
+
class={cn("btn btn-ghost text-xl", {
|
|
40
|
+
"md:hidden": !showBrand,
|
|
41
|
+
})}
|
|
42
|
+
>
|
|
43
|
+
{brand}
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="hidden flex-none lg:flex">
|
|
47
|
+
<ul class="menu menu-horizontal px-1">
|
|
48
|
+
{
|
|
49
|
+
Object.entries(navigation).map(([key, entry]) =>
|
|
50
|
+
entry.subEntry ? (
|
|
51
|
+
<li>
|
|
52
|
+
<details>
|
|
53
|
+
<summary>{entry.label ?? key}</summary>
|
|
54
|
+
<ul class="rounded-t-none bg-base-100 p-2">
|
|
55
|
+
{Object.entries(entry.subEntry).map(([key, entry]) => (
|
|
56
|
+
<li>
|
|
57
|
+
<a href={entry.href}>{entry.label ?? key}</a>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</details>
|
|
62
|
+
</li>
|
|
63
|
+
) : (
|
|
64
|
+
<li>
|
|
65
|
+
<a href={entry.href}>{entry.label ?? key}</a>
|
|
66
|
+
</li>
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../src/tools/cn'
|
|
3
|
+
import type { Entry } from './types'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
entry: Entry
|
|
7
|
+
currentPath?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { entry, currentPath = Astro.url.pathname } = Astro.props
|
|
11
|
+
|
|
12
|
+
// Normalize path by removing trailing slash for comparison
|
|
13
|
+
const normalizePath = (path: string) => path.replace(/\/$/, '') || '/'
|
|
14
|
+
|
|
15
|
+
// Check if entry or any of its children are active
|
|
16
|
+
const isActiveOrHasActiveChild = (entryValue: Entry[string]): boolean => {
|
|
17
|
+
if (
|
|
18
|
+
entryValue.href &&
|
|
19
|
+
normalizePath(entryValue.href) === normalizePath(currentPath)
|
|
20
|
+
)
|
|
21
|
+
return true
|
|
22
|
+
if (entryValue.subEntry) {
|
|
23
|
+
return Object.values(entryValue.subEntry).some(isActiveOrHasActiveChild)
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
{Object.entries(entry).map(([key, entryValue]) => {
|
|
30
|
+
const label = entryValue.label ?? key;
|
|
31
|
+
const isActive = entryValue.href && normalizePath(entryValue.href) === normalizePath(currentPath);
|
|
32
|
+
const hasActiveChild = entryValue.subEntry && Object.values(entryValue.subEntry).some(isActiveOrHasActiveChild);
|
|
33
|
+
return (
|
|
34
|
+
<li class={entryValue.className}>
|
|
35
|
+
{entryValue.href
|
|
36
|
+
? <a href={entryValue.href} class={cn({ 'bg-base-200/50 font-medium text-primary': isActive })}>{label}</a>
|
|
37
|
+
: <span class='menu-title'>{label}</span>}
|
|
38
|
+
{entryValue.subEntry ? (
|
|
39
|
+
<ul>
|
|
40
|
+
<Astro.self entry={entryValue.subEntry} currentPath={currentPath} />
|
|
41
|
+
</ul>
|
|
42
|
+
) : null}
|
|
43
|
+
</li>
|
|
44
|
+
)
|
|
45
|
+
})}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../src/tools/cn'
|
|
3
|
+
import SidebarElement from './SidebarElement.astro'
|
|
4
|
+
import type { Entry } from './types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
local: Entry | undefined
|
|
8
|
+
global: Entry
|
|
9
|
+
brand: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { local, global, brand } = Astro.props
|
|
13
|
+
|
|
14
|
+
const withLocale = (path: string) =>
|
|
15
|
+
Astro.currentLocale ? `/${Astro.currentLocale}${path}` : path
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<ul class={cn("menu min-h-screen w-56 bg-base-100", { "md:hidden": !local })}>
|
|
19
|
+
<div>
|
|
20
|
+
<a href={withLocale("/")} class="btn btn-ghost mb-2 text-xl">
|
|
21
|
+
{brand}
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="block md:hidden">
|
|
25
|
+
<li>
|
|
26
|
+
{
|
|
27
|
+
local ? (
|
|
28
|
+
<details>
|
|
29
|
+
<summary>Main menu</summary>
|
|
30
|
+
<SidebarElement entry={global} />
|
|
31
|
+
</details>
|
|
32
|
+
) : (
|
|
33
|
+
<SidebarElement entry={global} />
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
</li>
|
|
37
|
+
<div class={cn("divider my-1 block md:hidden", { hidden: !local })}></div>
|
|
38
|
+
</div>
|
|
39
|
+
{local && <SidebarElement entry={local} />}
|
|
40
|
+
</ul>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../src/tools/cn'
|
|
3
|
+
|
|
4
|
+
interface Link {
|
|
5
|
+
depth: number
|
|
6
|
+
text: string
|
|
7
|
+
slug: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
links: Link[]
|
|
12
|
+
label?: string
|
|
13
|
+
class?: string
|
|
14
|
+
desktopOnly?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
links,
|
|
19
|
+
label = 'On this page',
|
|
20
|
+
class: className,
|
|
21
|
+
desktopOnly = false,
|
|
22
|
+
} = Astro.props
|
|
23
|
+
|
|
24
|
+
// Filter to only include h2 and h3 headings (depth 2 and 3)
|
|
25
|
+
const filteredLinks = links.filter((link) => link.depth >= 2 && link.depth <= 3)
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
filteredLinks.length > 0 && (
|
|
30
|
+
<div class={className}>
|
|
31
|
+
{/* Mobile: Collapsible dropdown */}
|
|
32
|
+
{!desktopOnly && (
|
|
33
|
+
<div class="xl:hidden mb-4">
|
|
34
|
+
<details class="collapse collapse-arrow bg-base-200">
|
|
35
|
+
<summary class="collapse-title min-h-0 py-2 font-medium">
|
|
36
|
+
{label}
|
|
37
|
+
</summary>
|
|
38
|
+
<div class="collapse-content">
|
|
39
|
+
<ul class="menu menu-sm">
|
|
40
|
+
{filteredLinks.map((link) => (
|
|
41
|
+
<li>
|
|
42
|
+
<a
|
|
43
|
+
href={`#${link.slug}`}
|
|
44
|
+
class={cn({
|
|
45
|
+
'pl-4': link.depth === 2,
|
|
46
|
+
'pl-8': link.depth === 3,
|
|
47
|
+
})}
|
|
48
|
+
>
|
|
49
|
+
{link.text}
|
|
50
|
+
</a>
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
</div>
|
|
55
|
+
</details>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{/* Desktop: Sidebar within the grid column */}
|
|
60
|
+
{desktopOnly && (
|
|
61
|
+
<div class="border-l border-base-300 pl-4">
|
|
62
|
+
<h4 class="font-medium text-sm mb-2 text-base-content/60">
|
|
63
|
+
{label}
|
|
64
|
+
</h4>
|
|
65
|
+
<ul class="menu menu-xs p-0">
|
|
66
|
+
{filteredLinks.map((link) => (
|
|
67
|
+
<li>
|
|
68
|
+
<a
|
|
69
|
+
href={`#${link.slug}`}
|
|
70
|
+
class={cn('text-base-content/70 hover:text-primary', {
|
|
71
|
+
'pl-0': link.depth === 2,
|
|
72
|
+
'pl-4': link.depth === 3,
|
|
73
|
+
})}
|
|
74
|
+
>
|
|
75
|
+
{link.text}
|
|
76
|
+
</a>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Breadcrumbs } from './Breadcrumbs.astro'
|
|
2
|
+
export { default as Footer } from './Footer.astro'
|
|
3
|
+
export { default as GlobalDesktopNavigation } from './GlobalDesktopNavigation.astro'
|
|
4
|
+
export { default as LocalNavigation } from './LocalNavigation.astro'
|
|
5
|
+
export { default as SidebarElement } from './SidebarElement.astro'
|
|
6
|
+
export { default as SidebarNavigation } from './SidebarNavigation.astro'
|
|
7
|
+
export { default as TableOfContents } from './TableOfContents.astro'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Footer as FooterComponent } from '../components'
|
|
3
|
+
|
|
4
|
+
const withLocale = (path: string) =>
|
|
5
|
+
Astro.currentLocale ? `/${Astro.currentLocale}${path}` : path
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<FooterComponent
|
|
9
|
+
links={[{ href: withLocale("/imprint"), label: "Impressum" }]}
|
|
10
|
+
copyright={{
|
|
11
|
+
href: "https://github.com/levino",
|
|
12
|
+
label: "Levin Keller",
|
|
13
|
+
year: 2025,
|
|
14
|
+
}}
|
|
15
|
+
/>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Footer from './Footer.astro'
|
|
3
|
+
import '../../src/globals.css'
|
|
4
|
+
import { i18n } from 'astro:config/server'
|
|
5
|
+
import config from 'virtual:shipyard/config'
|
|
6
|
+
import { mapObjIndexed } from 'ramda'
|
|
7
|
+
import type {
|
|
8
|
+
NavigationEntry,
|
|
9
|
+
NavigationTree,
|
|
10
|
+
Script,
|
|
11
|
+
} from '../../src/schemas/config'
|
|
12
|
+
import { getTitle } from '../../src/tools/title'
|
|
13
|
+
import { GlobalDesktopNavigation, SidebarNavigation } from '../components'
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
frontmatter?: {
|
|
17
|
+
title?: string
|
|
18
|
+
description?: string
|
|
19
|
+
sidebarNavigation?: NavigationTree
|
|
20
|
+
}
|
|
21
|
+
} & {
|
|
22
|
+
title?: string
|
|
23
|
+
description?: string
|
|
24
|
+
sidebarNavigation?: NavigationTree
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const currentPath = Astro.url.pathname
|
|
28
|
+
const props = Astro.props.frontmatter || Astro.props
|
|
29
|
+
|
|
30
|
+
const withLocale = (path: string) =>
|
|
31
|
+
i18n ? `/${Astro.currentLocale}${path}` : path
|
|
32
|
+
const applyLocaleAndSetActive: (navigation: NavigationTree) => NavigationTree =
|
|
33
|
+
mapObjIndexed((entry: NavigationEntry) => ({
|
|
34
|
+
...entry,
|
|
35
|
+
...(entry.href ? { href: withLocale(entry.href) } : {}),
|
|
36
|
+
active: entry.href === currentPath,
|
|
37
|
+
...(entry.subEntry
|
|
38
|
+
? { subEntry: applyLocaleAndSetActive(entry.subEntry) }
|
|
39
|
+
: {}),
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
const navigation = applyLocaleAndSetActive(config.navigation)
|
|
43
|
+
const title = getTitle(config.title, props.title)
|
|
44
|
+
|
|
45
|
+
// Helper function to render script attributes
|
|
46
|
+
const renderScriptAttributes = (script: Script) => {
|
|
47
|
+
if (typeof script === 'string') {
|
|
48
|
+
return { src: script }
|
|
49
|
+
}
|
|
50
|
+
return script
|
|
51
|
+
}
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<html>
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="utf-8" />
|
|
57
|
+
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
58
|
+
<title>
|
|
59
|
+
{title}
|
|
60
|
+
</title>
|
|
61
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
62
|
+
<meta name="description" content={props.description} />
|
|
63
|
+
<meta property="og:title" content={title} />
|
|
64
|
+
<meta property="og:description" content={props.description} />
|
|
65
|
+
|
|
66
|
+
{config.scripts?.map((script) => {
|
|
67
|
+
const attrs = renderScriptAttributes(script)
|
|
68
|
+
return <script is:inline {...attrs}></script>
|
|
69
|
+
})}
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div class="drawer lg:drawer-open">
|
|
73
|
+
<input id="drawer" type="checkbox" class="drawer-toggle" />
|
|
74
|
+
<div class="drawer-content">
|
|
75
|
+
<div class="flex min-h-screen flex-col">
|
|
76
|
+
<GlobalDesktopNavigation
|
|
77
|
+
showBrand={!props.sidebarNavigation}
|
|
78
|
+
brand={config.brand}
|
|
79
|
+
navigation={navigation}
|
|
80
|
+
/>
|
|
81
|
+
<div class="grow">
|
|
82
|
+
<div class="mx-auto px-4">
|
|
83
|
+
<slot />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<Footer />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="drawer-side z-40">
|
|
91
|
+
<label for="drawer" aria-label="close sidebar" class="drawer-overlay"
|
|
92
|
+
></label>
|
|
93
|
+
<div>
|
|
94
|
+
<SidebarNavigation
|
|
95
|
+
brand={config.brand}
|
|
96
|
+
global={navigation}
|
|
97
|
+
local={props.sidebarNavigation}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
@@ -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/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@levino/shipyard-base",
|
|
3
|
+
"version": "0.0.0-rc-20251122114952",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./astro": "./src/astro.ts",
|
|
9
|
+
"./layouts": "./astro/layouts/index.ts",
|
|
10
|
+
"./layouts/*": "./astro/layouts/*",
|
|
11
|
+
"./components": "./astro/components/index.ts",
|
|
12
|
+
"./components/*": "./astro/components/*"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"astro": "^5.7",
|
|
16
|
+
"daisyui": "^4",
|
|
17
|
+
"tailwindcss": "^3",
|
|
18
|
+
"@tailwindcss/typography": "^0.5.10"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"astro",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/levino/shipyard"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tailwindcss/typography": "^0.5.10",
|
|
30
|
+
"@types/ramda": "^0.31",
|
|
31
|
+
"daisyui": "^4.6.0",
|
|
32
|
+
"tailwindcss": "^3.4.0",
|
|
33
|
+
"typescript": "^5.2.2",
|
|
34
|
+
"vite": "^4"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"clsx": "^2.1.0",
|
|
38
|
+
"ramda": "^0.31",
|
|
39
|
+
"tailwind-merge": "^2.2.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/astro.ts
ADDED
package/src/globals.css
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro'
|
|
2
|
+
import type { Config } from './schemas/config'
|
|
3
|
+
|
|
4
|
+
export type { Entry } from '../astro/components/types'
|
|
5
|
+
export type * from './schemas/config'
|
|
6
|
+
export { getTitle } from './tools/title'
|
|
7
|
+
export * from './types'
|
|
8
|
+
|
|
9
|
+
const shipyardConfigId = 'virtual:shipyard/config'
|
|
10
|
+
|
|
11
|
+
const resolveId: Record<string, string | undefined> = {
|
|
12
|
+
[shipyardConfigId]: `${shipyardConfigId}`,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const load = (config: Config) =>
|
|
16
|
+
({
|
|
17
|
+
[shipyardConfigId]: `export default ${JSON.stringify(config)}`,
|
|
18
|
+
}) as Record<string, string | undefined>
|
|
19
|
+
|
|
20
|
+
export default (config: Config): AstroIntegration => ({
|
|
21
|
+
name: 'shipyard',
|
|
22
|
+
hooks: {
|
|
23
|
+
'astro:config:setup': ({ updateConfig }) => {
|
|
24
|
+
updateConfig({
|
|
25
|
+
vite: {
|
|
26
|
+
plugins: [
|
|
27
|
+
{
|
|
28
|
+
name: 'shipyard',
|
|
29
|
+
resolveId: (id: string) => resolveId[id],
|
|
30
|
+
load: (id: string) => load(config)[id],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface NavigationEntry {
|
|
2
|
+
label?: string
|
|
3
|
+
href?: string
|
|
4
|
+
subEntry?: NavigationTree
|
|
5
|
+
active?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type NavigationTree = Record<string, NavigationEntry>
|
|
9
|
+
|
|
10
|
+
export type Script = string | astroHTML.JSX.IntrinsicElements['script']
|
|
11
|
+
|
|
12
|
+
export interface Config {
|
|
13
|
+
brand: string
|
|
14
|
+
navigation: NavigationTree
|
|
15
|
+
title: string
|
|
16
|
+
tagline: string
|
|
17
|
+
scripts?: Script[]
|
|
18
|
+
}
|
package/src/tools/cn.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { getTitle } from './title'
|
|
3
|
+
|
|
4
|
+
describe('getTitle', () => {
|
|
5
|
+
const siteTitle = 'Shipyard'
|
|
6
|
+
|
|
7
|
+
test('returns site title when page title is undefined', () => {
|
|
8
|
+
expect(getTitle(siteTitle, undefined)).toBe('Shipyard')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('returns site title when page title is null', () => {
|
|
12
|
+
expect(getTitle(siteTitle, null)).toBe('Shipyard')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('returns site title when page title is empty', () => {
|
|
16
|
+
expect(getTitle(siteTitle, '')).toBe('Shipyard')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('returns site title when page title equals site title', () => {
|
|
20
|
+
expect(getTitle(siteTitle, 'Shipyard')).toBe('Shipyard')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('returns combined title when page title differs', () => {
|
|
24
|
+
expect(getTitle(siteTitle, 'Blog')).toBe('Shipyard - Blog')
|
|
25
|
+
expect(getTitle(siteTitle, 'First Blog Post')).toBe(
|
|
26
|
+
'Shipyard - First Blog Post',
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('handles whitespace-only page titles', () => {
|
|
31
|
+
expect(getTitle(siteTitle, ' ')).toBe('Shipyard')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('trims page title before comparison', () => {
|
|
35
|
+
expect(getTitle(siteTitle, ' Shipyard ')).toBe('Shipyard')
|
|
36
|
+
expect(getTitle(siteTitle, ' Blog ')).toBe('Shipyard - Blog')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('is case sensitive', () => {
|
|
40
|
+
expect(getTitle(siteTitle, 'shipyard')).toBe('Shipyard - shipyard')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('handles special characters', () => {
|
|
44
|
+
expect(getTitle(siteTitle, 'FAQ & Help')).toBe('Shipyard - FAQ & Help')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const getTitle = (
|
|
2
|
+
siteTitle: string,
|
|
3
|
+
pageTitle?: string | null,
|
|
4
|
+
): string => {
|
|
5
|
+
if (!pageTitle) return siteTitle
|
|
6
|
+
|
|
7
|
+
const trimmedPageTitle = pageTitle.trim()
|
|
8
|
+
if (!trimmedPageTitle || trimmedPageTitle === siteTitle) return siteTitle
|
|
9
|
+
|
|
10
|
+
return `${siteTitle} - ${trimmedPageTitle}`
|
|
11
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface LinkData {
|
|
2
|
+
href: string
|
|
3
|
+
label: string
|
|
4
|
+
active: boolean
|
|
5
|
+
}
|
|
6
|
+
export const MONTHS_EN = [
|
|
7
|
+
'january',
|
|
8
|
+
'february',
|
|
9
|
+
'march',
|
|
10
|
+
'april',
|
|
11
|
+
'may',
|
|
12
|
+
'june',
|
|
13
|
+
'july',
|
|
14
|
+
'august',
|
|
15
|
+
'september',
|
|
16
|
+
'october',
|
|
17
|
+
'november',
|
|
18
|
+
'december',
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export const MONTHS_DE = [
|
|
22
|
+
'Januar',
|
|
23
|
+
'Februar',
|
|
24
|
+
'März',
|
|
25
|
+
'April',
|
|
26
|
+
'Mai',
|
|
27
|
+
'Juni',
|
|
28
|
+
'Juli',
|
|
29
|
+
'August',
|
|
30
|
+
'September',
|
|
31
|
+
'Oktober',
|
|
32
|
+
'November',
|
|
33
|
+
'Dezember',
|
|
34
|
+
] as const
|