@levino/shipyard-base 0.6.2 → 0.6.3-rc-20260113205743

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.
@@ -0,0 +1,104 @@
1
+ ---
2
+ /**
3
+ * Deprecation banner component that displays a warning when viewing deprecated docs.
4
+ * Shows a prominent alert with a link to the same page in the latest version.
5
+ * The banner can be dismissed but will reappear on navigation.
6
+ */
7
+
8
+ interface Props {
9
+ /** The deprecated version being viewed */
10
+ version: string
11
+ /** Human-readable label for the deprecated version */
12
+ label?: string
13
+ /** The current/latest version to link to */
14
+ currentVersion: string
15
+ /** Human-readable label for the current version */
16
+ currentVersionLabel?: string
17
+ /** URL to the same page in the latest version */
18
+ latestVersionUrl: string
19
+ /** Additional CSS classes */
20
+ class?: string
21
+ }
22
+
23
+ const {
24
+ version,
25
+ label,
26
+ currentVersion,
27
+ currentVersionLabel,
28
+ latestVersionUrl,
29
+ class: className,
30
+ } = Astro.props
31
+
32
+ const displayVersion = label ?? version
33
+ const displayCurrentVersion = currentVersionLabel ?? currentVersion
34
+ ---
35
+
36
+ <div
37
+ class:list={['deprecation-banner', className]}
38
+ data-testid="deprecation-banner"
39
+ role="alert"
40
+ >
41
+ <div class="alert alert-warning shadow-lg">
42
+ <svg
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ class="h-6 w-6 shrink-0 stroke-current"
45
+ fill="none"
46
+ viewBox="0 0 24 24"
47
+ >
48
+ <path
49
+ stroke-linecap="round"
50
+ stroke-linejoin="round"
51
+ stroke-width="2"
52
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
53
+ />
54
+ </svg>
55
+ <div class="flex-1">
56
+ <h3 class="font-bold">Outdated Documentation</h3>
57
+ <p class="text-sm">
58
+ You are viewing documentation for <strong>{displayVersion}</strong>, which is no longer
59
+ maintained.
60
+ <a href={latestVersionUrl} class="link link-hover font-medium">
61
+ View the latest version ({displayCurrentVersion})
62
+ </a>
63
+ </p>
64
+ </div>
65
+ <button
66
+ type="button"
67
+ class="btn btn-ghost btn-sm btn-circle"
68
+ aria-label="Dismiss banner"
69
+ data-dismiss-banner
70
+ >
71
+ <svg
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ class="h-5 w-5"
74
+ fill="none"
75
+ viewBox="0 0 24 24"
76
+ stroke="currentColor"
77
+ >
78
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
79
+ </svg>
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ <script>
85
+ // Handle dismiss button click
86
+ // The banner will reappear on navigation (page reload)
87
+ function initDeprecationBanner() {
88
+ const banners = document.querySelectorAll('.deprecation-banner')
89
+ banners.forEach((banner) => {
90
+ const dismissBtn = banner.querySelector('[data-dismiss-banner]')
91
+ if (dismissBtn) {
92
+ dismissBtn.addEventListener('click', () => {
93
+ banner.classList.add('hidden')
94
+ })
95
+ }
96
+ })
97
+ }
98
+
99
+ // Initialize on page load
100
+ initDeprecationBanner()
101
+
102
+ // Re-initialize on Astro view transitions
103
+ document.addEventListener('astro:page-load', initDeprecationBanner)
104
+ </script>
@@ -70,6 +70,7 @@ const { brand, navigation, showBrand = false } = Astro.props as Props
70
70
  )
71
71
  }
72
72
  </ul>
73
+ <slot name="navbarExtra" />
73
74
  <ThemeToggle class="btn btn-ghost btn-circle" />
74
75
  <LanguageSwitcher variant="dropdown" />
75
76
  </div>
@@ -36,6 +36,7 @@ const withLocale = (path: string) =>
36
36
  )
37
37
  }
38
38
  </li>
39
+ <slot name="sidebarExtra" />
39
40
  <LanguageSwitcher variant="list" />
40
41
  <li class="flex flex-row items-center gap-2 px-4 py-2">
41
42
  <span class="text-sm opacity-70">Theme</span>
@@ -0,0 +1,116 @@
1
+ ---
2
+ /**
3
+ * Version badge component that displays version status inline.
4
+ * Shows badges for stable, deprecated, latest, or unreleased versions.
5
+ * Can be used to indicate the version status at the top of docs pages.
6
+ */
7
+
8
+ interface Props {
9
+ /** The version identifier (e.g., "v1.0", "v2.0") */
10
+ version: string
11
+ /** Human-readable label for display (defaults to version) */
12
+ label?: string
13
+ /** The stable version for comparison */
14
+ stableVersion?: string
15
+ /** The current/latest version for comparison */
16
+ currentVersion?: string
17
+ /** List of deprecated versions */
18
+ deprecatedVersions?: string[]
19
+ /** Banner type for this version (from config) */
20
+ banner?: 'unreleased' | 'unmaintained'
21
+ /** Whether this is the latest alias */
22
+ isLatestAlias?: boolean
23
+ /** Additional CSS classes */
24
+ class?: string
25
+ /** Size variant: 'sm' for small, 'md' for medium, 'lg' for large */
26
+ size?: 'sm' | 'md' | 'lg'
27
+ }
28
+
29
+ const {
30
+ version,
31
+ label,
32
+ stableVersion,
33
+ currentVersion,
34
+ deprecatedVersions = [],
35
+ banner,
36
+ isLatestAlias = false,
37
+ class: className,
38
+ size = 'md',
39
+ } = Astro.props
40
+
41
+ // Determine badge type based on version status
42
+ type BadgeType = 'stable' | 'latest' | 'deprecated' | 'unreleased' | null
43
+
44
+ const getBadgeType = (): BadgeType => {
45
+ // Unreleased takes precedence
46
+ if (banner === 'unreleased') {
47
+ return 'unreleased'
48
+ }
49
+ // Deprecated/unmaintained
50
+ if (banner === 'unmaintained' || deprecatedVersions.includes(version)) {
51
+ return 'deprecated'
52
+ }
53
+ // Stable version
54
+ if (stableVersion && version === stableVersion) {
55
+ return 'stable'
56
+ }
57
+ // Latest/current version (and not stable, so show as latest)
58
+ if (
59
+ currentVersion &&
60
+ version === currentVersion &&
61
+ version !== stableVersion
62
+ ) {
63
+ return 'latest'
64
+ }
65
+ // If viewing via latest alias
66
+ if (isLatestAlias) {
67
+ return 'latest'
68
+ }
69
+ return null
70
+ }
71
+
72
+ const badgeType = getBadgeType()
73
+ const displayLabel = label ?? version
74
+
75
+ // Badge styling based on type
76
+ const getBadgeClasses = () => {
77
+ const sizeClass = {
78
+ sm: 'badge-sm',
79
+ md: '',
80
+ lg: 'badge-lg',
81
+ }[size]
82
+
83
+ const typeClass = {
84
+ stable: 'badge-success',
85
+ latest: 'badge-primary',
86
+ deprecated: 'badge-warning',
87
+ unreleased: 'badge-info',
88
+ }[badgeType ?? 'stable']
89
+
90
+ return ['badge', sizeClass, typeClass, className].filter(Boolean).join(' ')
91
+ }
92
+
93
+ const getBadgeLabel = () => {
94
+ switch (badgeType) {
95
+ case 'stable':
96
+ return 'Stable'
97
+ case 'latest':
98
+ return 'Latest'
99
+ case 'deprecated':
100
+ return 'Deprecated'
101
+ case 'unreleased':
102
+ return 'Unreleased'
103
+ default:
104
+ return null
105
+ }
106
+ }
107
+
108
+ const badgeLabel = getBadgeLabel()
109
+ ---
110
+
111
+ {badgeLabel && (
112
+ <span class={getBadgeClasses()} data-testid="version-badge">
113
+ <span class="sr-only">Version {displayLabel} - </span>
114
+ {badgeLabel}
115
+ </span>
116
+ )}
@@ -0,0 +1,207 @@
1
+ ---
2
+ /**
3
+ * Version selector component that displays available documentation versions.
4
+ * Allows users to switch between different versions of the documentation.
5
+ */
6
+
7
+ interface VersionInfo {
8
+ /** The version identifier (e.g., "v1.0", "v2.0") */
9
+ version: string
10
+ /** Human-readable label for display */
11
+ label?: string
12
+ /** Path segment used in URLs */
13
+ path?: string
14
+ /** Banner type for this version */
15
+ banner?: 'unreleased' | 'unmaintained'
16
+ }
17
+
18
+ interface Props {
19
+ /** All available versions */
20
+ versions: VersionInfo[]
21
+ /** The current version being viewed */
22
+ currentVersion: string
23
+ /** The stable version (for badge display) */
24
+ stableVersion?: string
25
+ /** List of deprecated version identifiers */
26
+ deprecatedVersions?: string[]
27
+ /** Additional CSS classes */
28
+ class?: string
29
+ /** Variant: 'dropdown' for navbar, 'list' for sidebar */
30
+ variant?: 'dropdown' | 'list'
31
+ }
32
+
33
+ const {
34
+ versions,
35
+ currentVersion,
36
+ stableVersion,
37
+ deprecatedVersions = [],
38
+ class: className,
39
+ variant = 'dropdown',
40
+ } = Astro.props
41
+
42
+ const currentPath = Astro.url.pathname
43
+
44
+ // Get display label for current version
45
+ const currentVersionInfo = versions.find(
46
+ (v) => v.version === currentVersion || v.path === currentVersion,
47
+ )
48
+ const currentLabel = currentVersionInfo?.label ?? currentVersion
49
+
50
+ // Build the path for other versions by replacing the current version in the URL
51
+ const getVersionPath = (targetVersion: VersionInfo) => {
52
+ const targetPath = targetVersion.path ?? targetVersion.version
53
+ const currentVersionPath = currentVersionInfo?.path ?? currentVersion
54
+
55
+ // Replace the version segment in the current URL
56
+ // This handles both /docs/v1/page and /en/docs/v1/page patterns
57
+ return currentPath.replace(
58
+ new RegExp(`/${escapeRegex(currentVersionPath)}/`),
59
+ `/${targetPath}/`,
60
+ )
61
+ }
62
+
63
+ // Escape special regex characters
64
+ function escapeRegex(str: string): string {
65
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
66
+ }
67
+
68
+ // Helper to get badge info for a version
69
+ function getVersionBadge(version: VersionInfo): {
70
+ type: 'stable' | 'deprecated' | 'unreleased' | null
71
+ label: string
72
+ } | null {
73
+ if (version.banner === 'unreleased') {
74
+ return { type: 'unreleased', label: 'Unreleased' }
75
+ }
76
+ if (
77
+ version.banner === 'unmaintained' ||
78
+ deprecatedVersions.includes(version.version)
79
+ ) {
80
+ return { type: 'deprecated', label: 'Deprecated' }
81
+ }
82
+ if (stableVersion && version.version === stableVersion) {
83
+ return { type: 'stable', label: 'Stable' }
84
+ }
85
+ return null
86
+ }
87
+
88
+ const hasMultipleVersions = versions.length > 1
89
+ ---
90
+
91
+ {hasMultipleVersions && variant === 'dropdown' && (
92
+ <div class:list={['dropdown dropdown-end', className]} data-testid="version-selector">
93
+ <div tabindex="0" role="button" class="btn btn-ghost gap-1">
94
+ <svg
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ class="h-5 w-5"
97
+ fill="none"
98
+ viewBox="0 0 24 24"
99
+ stroke="currentColor"
100
+ >
101
+ <path
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ stroke-width="2"
105
+ d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A2 2 0 013 12V7a4 4 0 014-4z"
106
+ />
107
+ </svg>
108
+ <span class="hidden sm:inline">{currentLabel}</span>
109
+ <svg
110
+ xmlns="http://www.w3.org/2000/svg"
111
+ class="h-4 w-4"
112
+ fill="none"
113
+ viewBox="0 0 24 24"
114
+ stroke="currentColor"
115
+ >
116
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
117
+ </svg>
118
+ </div>
119
+ <ul
120
+ tabindex="0"
121
+ class="dropdown-content menu rounded-box z-50 mt-3 w-52 bg-base-100 p-2 shadow"
122
+ >
123
+ {versions.map((version) => {
124
+ const badge = getVersionBadge(version)
125
+ const isActive = version.version === currentVersion || version.path === currentVersion
126
+ return (
127
+ <li>
128
+ <a
129
+ href={getVersionPath(version)}
130
+ class:list={[{ active: isActive }]}
131
+ >
132
+ <span class="flex-1">{version.label ?? version.version}</span>
133
+ {badge && (
134
+ <span
135
+ class:list={[
136
+ 'badge badge-sm',
137
+ {
138
+ 'badge-success': badge.type === 'stable',
139
+ 'badge-warning': badge.type === 'deprecated',
140
+ 'badge-info': badge.type === 'unreleased',
141
+ },
142
+ ]}
143
+ >
144
+ {badge.label}
145
+ </span>
146
+ )}
147
+ </a>
148
+ </li>
149
+ )
150
+ })}
151
+ </ul>
152
+ </div>
153
+ )}
154
+
155
+ {hasMultipleVersions && variant === 'list' && (
156
+ <li class={className} data-testid="version-selector">
157
+ <details>
158
+ <summary class="gap-2">
159
+ <svg
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ class="h-5 w-5"
162
+ fill="none"
163
+ viewBox="0 0 24 24"
164
+ stroke="currentColor"
165
+ >
166
+ <path
167
+ stroke-linecap="round"
168
+ stroke-linejoin="round"
169
+ stroke-width="2"
170
+ d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A2 2 0 013 12V7a4 4 0 014-4z"
171
+ />
172
+ </svg>
173
+ {currentLabel}
174
+ </summary>
175
+ <ul>
176
+ {versions.map((version) => {
177
+ const badge = getVersionBadge(version)
178
+ const isActive = version.version === currentVersion || version.path === currentVersion
179
+ return (
180
+ <li>
181
+ <a
182
+ href={getVersionPath(version)}
183
+ class:list={[{ active: isActive }]}
184
+ >
185
+ <span class="flex-1">{version.label ?? version.version}</span>
186
+ {badge && (
187
+ <span
188
+ class:list={[
189
+ 'badge badge-sm',
190
+ {
191
+ 'badge-success': badge.type === 'stable',
192
+ 'badge-warning': badge.type === 'deprecated',
193
+ 'badge-info': badge.type === 'unreleased',
194
+ },
195
+ ]}
196
+ >
197
+ {badge.label}
198
+ </span>
199
+ )}
200
+ </a>
201
+ </li>
202
+ )
203
+ })}
204
+ </ul>
205
+ </details>
206
+ </li>
207
+ )}
@@ -1,8 +1,10 @@
1
1
  export { default as Admonition } from './Admonition.astro'
2
2
  export { default as AnnouncementBar } from './AnnouncementBar.astro'
3
3
  export { default as Breadcrumbs } from './Breadcrumbs.astro'
4
+ export { default as DeprecationBanner } from './DeprecationBanner.astro'
4
5
  export { default as Footer } from './Footer.astro'
5
6
  export { default as GlobalDesktopNavigation } from './GlobalDesktopNavigation.astro'
7
+ export { default as LanguageSwitcher } from './LanguageSwitcher.astro'
6
8
  export { default as LocalNavigation } from './LocalNavigation.astro'
7
9
  export { default as Npm2YarnScript } from './Npm2YarnScript.astro'
8
10
  export { default as SidebarElement } from './SidebarElement.astro'
@@ -11,3 +13,5 @@ export { default as TabItem } from './TabItem.astro'
11
13
  export { default as TableOfContents } from './TableOfContents.astro'
12
14
  export { default as Tabs } from './Tabs.astro'
13
15
  export { default as ThemeToggle } from './ThemeToggle.astro'
16
+ export { default as VersionBadge } from './VersionBadge.astro'
17
+ export { default as VersionSelector } from './VersionSelector.astro'
@@ -116,7 +116,9 @@ const renderScriptAttributes = (script: Script) => {
116
116
  showBrand={!props.sidebarNavigation}
117
117
  brand={config.brand}
118
118
  navigation={navigation}
119
- />
119
+ >
120
+ <slot name="navbarExtra" slot="navbarExtra" />
121
+ </GlobalDesktopNavigation>
120
122
  <div class="grow">
121
123
  <div class="mx-auto px-4">
122
124
  <slot />
@@ -134,7 +136,9 @@ const renderScriptAttributes = (script: Script) => {
134
136
  brand={config.brand}
135
137
  global={navigation}
136
138
  local={props.sidebarNavigation}
137
- />
139
+ >
140
+ <slot name="sidebarExtra" slot="sidebarExtra" />
141
+ </SidebarNavigation>
138
142
  </div>
139
143
  </div>
140
144
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levino/shipyard-base",
3
- "version": "0.6.2",
3
+ "version": "0.6.3-rc-20260113205743",
4
4
  "description": "Core layouts, components, and configuration for shipyard - a composable page builder for Astro",
5
5
  "keywords": [
6
6
  "astro",
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
  import type { AstroIntegration } from 'astro'
3
+ import remarkDirective from 'remark-directive'
4
+ import { remarkAdmonitions } from './remark/remarkAdmonitions'
5
+ import { remarkNpm2Yarn } from './remark/remarkNpm2Yarn'
3
6
  import type { Config } from './schemas/config'
4
7
  import { checkLinks, reportBrokenLinks } from './tools/linkChecker'
5
8
 
@@ -39,6 +42,9 @@ export default (config: Config): AstroIntegration => {
39
42
  } as Record<string, string | undefined>
40
43
 
41
44
  updateConfig({
45
+ markdown: {
46
+ remarkPlugins: [remarkDirective, remarkAdmonitions, remarkNpm2Yarn],
47
+ },
42
48
  vite: {
43
49
  plugins: [
44
50
  {
@@ -1,17 +1,9 @@
1
1
  /**
2
2
  * Remark plugins for shipyard markdown processing.
3
3
  *
4
- * @example
5
- * ```ts
6
- * import remarkDirective from 'remark-directive'
7
- * import { remarkAdmonitions } from '@levino/shipyard-base/remark'
8
- *
9
- * export default defineConfig({
10
- * markdown: {
11
- * remarkPlugins: [remarkDirective, remarkAdmonitions],
12
- * },
13
- * })
14
- * ```
4
+ * Note: These plugins are automatically registered by the shipyard integration.
5
+ * You don't need to configure them manually. They are exported here for
6
+ * advanced use cases where you need direct access to the plugins.
15
7
  */
16
8
 
17
9
  // Re-export remark-directive for convenience
@@ -41,7 +41,10 @@ const getDefaultTitle = (type: AdmonitionType): string => {
41
41
  /**
42
42
  * Remark plugin to transform container directives into admonitions.
43
43
  *
44
- * This plugin works with remark-directive to transform:
44
+ * This plugin is automatically registered by the shipyard integration.
45
+ * You don't need to configure it manually.
46
+ *
47
+ * It works with remark-directive to transform:
45
48
  * ```markdown
46
49
  * :::note
47
50
  * This is a note
@@ -53,18 +56,6 @@ const getDefaultTitle = (type: AdmonitionType): string => {
53
56
  * ```
54
57
  *
55
58
  * Into properly structured admonition HTML.
56
- *
57
- * @example
58
- * ```ts
59
- * import remarkDirective from 'remark-directive'
60
- * import { remarkAdmonitions } from '@levino/shipyard-base/remark'
61
- *
62
- * export default defineConfig({
63
- * markdown: {
64
- * remarkPlugins: [remarkDirective, remarkAdmonitions],
65
- * },
66
- * })
67
- * ```
68
59
  */
69
60
  export const remarkAdmonitions: Plugin<[], Root> = () => {
70
61
  return (tree: Root) => {