@lupinum/ginko-docs 0.2.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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/app/app.config.ts +49 -0
  4. package/app/app.vue +71 -0
  5. package/app/assets/css/prose.css +1154 -0
  6. package/app/assets/css/tailwind.css +365 -0
  7. package/app/components/OgImage/GinkoDocs.satori.vue +117 -0
  8. package/app/components/content/DocumentPageShell.vue +55 -0
  9. package/app/components/content/Feedback.vue +97 -0
  10. package/app/components/content/PageMarkdownCopy.vue +102 -0
  11. package/app/components/mdc/MdcAccordion.vue +64 -0
  12. package/app/components/mdc/MdcAccordionItem.vue +51 -0
  13. package/app/components/mdc/MdcAlert.vue +34 -0
  14. package/app/components/mdc/MdcAside.vue +27 -0
  15. package/app/components/mdc/MdcCallout.vue +62 -0
  16. package/app/components/mdc/MdcCard.vue +186 -0
  17. package/app/components/mdc/MdcCardGroup.vue +47 -0
  18. package/app/components/mdc/MdcCenter.vue +71 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +193 -0
  20. package/app/components/mdc/MdcColumn.vue +70 -0
  21. package/app/components/mdc/MdcDocImg.vue +25 -0
  22. package/app/components/mdc/MdcDropcap.vue +84 -0
  23. package/app/components/mdc/MdcError.vue +16 -0
  24. package/app/components/mdc/MdcField.vue +62 -0
  25. package/app/components/mdc/MdcFieldGroup.vue +18 -0
  26. package/app/components/mdc/MdcFigure.vue +51 -0
  27. package/app/components/mdc/MdcIdea.vue +16 -0
  28. package/app/components/mdc/MdcInfo.vue +16 -0
  29. package/app/components/mdc/MdcLayout.vue +57 -0
  30. package/app/components/mdc/MdcNote.vue +16 -0
  31. package/app/components/mdc/MdcPassage.vue +32 -0
  32. package/app/components/mdc/MdcQuiz.vue +281 -0
  33. package/app/components/mdc/MdcQuizOption.vue +91 -0
  34. package/app/components/mdc/MdcQuizQuestion.vue +226 -0
  35. package/app/components/mdc/MdcReadMore.vue +90 -0
  36. package/app/components/mdc/MdcReadMoreGroup.vue +18 -0
  37. package/app/components/mdc/MdcShortcut.vue +46 -0
  38. package/app/components/mdc/MdcStep.vue +42 -0
  39. package/app/components/mdc/MdcSteps.vue +25 -0
  40. package/app/components/mdc/MdcSuccess.vue +16 -0
  41. package/app/components/mdc/MdcTab.vue +17 -0
  42. package/app/components/mdc/MdcTabs.vue +132 -0
  43. package/app/components/mdc/MdcTimeline.vue +14 -0
  44. package/app/components/mdc/MdcTimelineItem.vue +49 -0
  45. package/app/components/mdc/MdcWarning.vue +16 -0
  46. package/app/components/mdc/icons.ts +10 -0
  47. package/app/components/prose/ProseA.vue +27 -0
  48. package/app/components/prose/ProseBlockquote.vue +14 -0
  49. package/app/components/prose/ProseCode.vue +14 -0
  50. package/app/components/prose/ProseEm.vue +14 -0
  51. package/app/components/prose/ProseH1.vue +18 -0
  52. package/app/components/prose/ProseH2.vue +24 -0
  53. package/app/components/prose/ProseH3.vue +24 -0
  54. package/app/components/prose/ProseH4.vue +14 -0
  55. package/app/components/prose/ProseHr.vue +12 -0
  56. package/app/components/prose/ProseImg.vue +120 -0
  57. package/app/components/prose/ProseLi.vue +14 -0
  58. package/app/components/prose/ProseOl.vue +14 -0
  59. package/app/components/prose/ProseP.vue +14 -0
  60. package/app/components/prose/ProsePre.vue +217 -0
  61. package/app/components/prose/ProseStrong.vue +14 -0
  62. package/app/components/prose/ProseTable.vue +16 -0
  63. package/app/components/prose/ProseTbody.vue +14 -0
  64. package/app/components/prose/ProseTd.vue +14 -0
  65. package/app/components/prose/ProseTh.vue +14 -0
  66. package/app/components/prose/ProseThead.vue +14 -0
  67. package/app/components/prose/ProseTr.vue +14 -0
  68. package/app/components/prose/ProseUl.vue +14 -0
  69. package/app/components/site/ModeToggle.vue +123 -0
  70. package/app/components/site/SiteBanner.vue +56 -0
  71. package/app/components/site/SiteFooter.vue +80 -0
  72. package/app/components/site/SiteHeader.vue +250 -0
  73. package/app/components/site/SiteInteractionLayer.vue +35 -0
  74. package/app/components/site/SiteLocaleSwitcher.vue +268 -0
  75. package/app/components/site/SiteLogoMark.vue +12 -0
  76. package/app/components/site/SiteSkipLink.vue +14 -0
  77. package/app/components/ui/accordion/Accordion.vue +15 -0
  78. package/app/components/ui/accordion/AccordionContent.vue +23 -0
  79. package/app/components/ui/accordion/AccordionItem.vue +24 -0
  80. package/app/components/ui/accordion/AccordionTrigger.vue +35 -0
  81. package/app/components/ui/accordion/index.ts +4 -0
  82. package/app/components/ui/alert/Alert.vue +21 -0
  83. package/app/components/ui/alert/AlertDescription.vue +22 -0
  84. package/app/components/ui/alert/AlertTitle.vue +22 -0
  85. package/app/components/ui/alert/index.ts +34 -0
  86. package/app/components/ui/badge/Badge.vue +28 -0
  87. package/app/components/ui/badge/index.ts +24 -0
  88. package/app/components/ui/button/Button.vue +31 -0
  89. package/app/components/ui/button/index.ts +35 -0
  90. package/app/components/ui/card/Card.vue +16 -0
  91. package/app/components/ui/card/CardAction.vue +17 -0
  92. package/app/components/ui/card/CardContent.vue +14 -0
  93. package/app/components/ui/card/CardDescription.vue +14 -0
  94. package/app/components/ui/card/CardFooter.vue +14 -0
  95. package/app/components/ui/card/CardHeader.vue +22 -0
  96. package/app/components/ui/card/CardTitle.vue +14 -0
  97. package/app/components/ui/card/card-variants.ts +15 -0
  98. package/app/components/ui/card/index.ts +8 -0
  99. package/app/components/ui/checkbox/Checkbox.vue +35 -0
  100. package/app/components/ui/checkbox/index.ts +1 -0
  101. package/app/components/ui/collapsible/Collapsible.vue +15 -0
  102. package/app/components/ui/collapsible/CollapsibleContent.vue +25 -0
  103. package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  104. package/app/components/ui/collapsible/index.ts +3 -0
  105. package/app/components/ui/dialog/Dialog.vue +15 -0
  106. package/app/components/ui/dialog/DialogClose.vue +12 -0
  107. package/app/components/ui/dialog/DialogContent.vue +69 -0
  108. package/app/components/ui/dialog/DialogDescription.vue +21 -0
  109. package/app/components/ui/dialog/DialogFooter.vue +15 -0
  110. package/app/components/ui/dialog/DialogHeader.vue +12 -0
  111. package/app/components/ui/dialog/DialogOverlay.vue +26 -0
  112. package/app/components/ui/dialog/DialogPortal.vue +12 -0
  113. package/app/components/ui/dialog/DialogTitle.vue +21 -0
  114. package/app/components/ui/dialog/DialogTrigger.vue +12 -0
  115. package/app/components/ui/dialog/index.ts +10 -0
  116. package/app/components/ui/dropdown-menu/DropdownMenu.vue +15 -0
  117. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +36 -0
  118. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -0
  119. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  120. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  121. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +25 -0
  122. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +15 -0
  123. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +37 -0
  124. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  125. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  126. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +15 -0
  127. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +29 -0
  128. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  129. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +14 -0
  130. package/app/components/ui/dropdown-menu/index.ts +16 -0
  131. package/app/components/ui/input/Input.vue +23 -0
  132. package/app/components/ui/input/index.ts +1 -0
  133. package/app/components/ui/kbd/Kbd.vue +23 -0
  134. package/app/components/ui/kbd/KbdGroup.vue +14 -0
  135. package/app/components/ui/kbd/index.ts +22 -0
  136. package/app/components/ui/label/Label.vue +14 -0
  137. package/app/components/ui/label/index.ts +1 -0
  138. package/app/components/ui/scroll-area/ScrollArea.vue +39 -0
  139. package/app/components/ui/scroll-area/ScrollBar.vue +33 -0
  140. package/app/components/ui/scroll-area/index.ts +2 -0
  141. package/app/components/ui/separator/Separator.vue +27 -0
  142. package/app/components/ui/separator/index.ts +1 -0
  143. package/app/components/ui/sheet/Sheet.vue +15 -0
  144. package/app/components/ui/sheet/SheetClose.vue +12 -0
  145. package/app/components/ui/sheet/SheetContent.vue +59 -0
  146. package/app/components/ui/sheet/SheetDescription.vue +21 -0
  147. package/app/components/ui/sheet/SheetFooter.vue +12 -0
  148. package/app/components/ui/sheet/SheetHeader.vue +12 -0
  149. package/app/components/ui/sheet/SheetOverlay.vue +26 -0
  150. package/app/components/ui/sheet/SheetTitle.vue +21 -0
  151. package/app/components/ui/sheet/SheetTrigger.vue +12 -0
  152. package/app/components/ui/sheet/index.ts +8 -0
  153. package/app/components/ui/switch/Switch.vue +39 -0
  154. package/app/components/ui/switch/index.ts +1 -0
  155. package/app/components/ui/tabs/Tabs.vue +24 -0
  156. package/app/components/ui/tabs/TabsContent.vue +21 -0
  157. package/app/components/ui/tabs/TabsList.vue +26 -0
  158. package/app/components/ui/tabs/TabsTrigger.vue +28 -0
  159. package/app/components/ui/tabs/index.ts +4 -0
  160. package/app/components/ui/textarea/Textarea.vue +31 -0
  161. package/app/components/ui/textarea/index.ts +1 -0
  162. package/app/composables/site-navigation.utils.ts +80 -0
  163. package/app/composables/useCanonicalUrl.ts +9 -0
  164. package/app/composables/useContentRouteAlternates.ts +43 -0
  165. package/app/composables/useGinkoAnalytics.ts +37 -0
  166. package/app/composables/useGinkoDocsConfig.ts +6 -0
  167. package/app/composables/useGinkoOgImage.ts +20 -0
  168. package/app/composables/useLocalizedPath.ts +12 -0
  169. package/app/composables/useLocalizedRouteSwitch.ts +31 -0
  170. package/app/composables/usePageMarkdownActions.ts +79 -0
  171. package/app/composables/useSchemaJsonLd.ts +25 -0
  172. package/app/composables/useSiteNavigation.ts +86 -0
  173. package/app/config/site.utils.ts +36 -0
  174. package/app/error.vue +125 -0
  175. package/app/features/docs/components/DocsContributeLinks.vue +65 -0
  176. package/app/features/docs/components/DocsMobileToc.vue +104 -0
  177. package/app/features/docs/components/DocsPageContent.vue +188 -0
  178. package/app/features/docs/components/DocsPageNav.vue +57 -0
  179. package/app/features/docs/components/DocsSidebar.vue +129 -0
  180. package/app/features/docs/components/DocsSidebarDropdown.vue +70 -0
  181. package/app/features/docs/components/DocsSidebarItem.vue +169 -0
  182. package/app/features/docs/components/DocsSidebarList.vue +35 -0
  183. package/app/features/docs/components/DocsSidebarTabs.vue +35 -0
  184. package/app/features/docs/components/DocsToc.vue +60 -0
  185. package/app/features/docs/composables/useDocsEntryPath.ts +13 -0
  186. package/app/features/docs/composables/useDocsNavigation.ts +29 -0
  187. package/app/features/docs/composables/useDocsNavigationData.ts +15 -0
  188. package/app/features/docs/composables/useScrollspy.ts +67 -0
  189. package/app/features/docs/docs-navigation.ts +170 -0
  190. package/app/features/search/command-center.ts +156 -0
  191. package/app/features/search/components/SiteCommandCenter.vue +323 -0
  192. package/app/features/search/components/SiteSearchPageHighlight.vue +124 -0
  193. package/app/features/search/useCommandCenter.ts +300 -0
  194. package/app/layouts/blog.vue +15 -0
  195. package/app/layouts/default.vue +12 -0
  196. package/app/layouts/docs.vue +20 -0
  197. package/app/lib/errors.ts +84 -0
  198. package/app/lib/schema-org.ts +38 -0
  199. package/app/lib/utils.ts +7 -0
  200. package/app/pages/blog/[slug].vue +211 -0
  201. package/app/pages/blog/index.vue +106 -0
  202. package/app/pages/docs/[...slug].vue +10 -0
  203. package/app/pages/docs/index.vue +23 -0
  204. package/app/pages/index.vue +140 -0
  205. package/app/plugins/ssr-width.ts +6 -0
  206. package/app/utils/content.ts +56 -0
  207. package/app/utils/index.ts +1 -0
  208. package/components.ts +9 -0
  209. package/content.js +175 -0
  210. package/content.ts +148 -0
  211. package/i18n/i18n.config.ts +10 -0
  212. package/i18n/locales.ts +81 -0
  213. package/i18n/messages/global/banner.ts +8 -0
  214. package/i18n/messages/global/blog.ts +14 -0
  215. package/i18n/messages/global/command.ts +44 -0
  216. package/i18n/messages/global/docs.ts +32 -0
  217. package/i18n/messages/global/errors.ts +36 -0
  218. package/i18n/messages/global/feedback.ts +11 -0
  219. package/i18n/messages/global/index.ts +21 -0
  220. package/i18n/messages/global/nav.ts +18 -0
  221. package/i18n/messages/global/site.ts +4 -0
  222. package/i18n/messages/global/theme.ts +7 -0
  223. package/i18n/messages/index.ts +20 -0
  224. package/i18n/messages/localize.ts +35 -0
  225. package/i18n/messages/source.ts +5 -0
  226. package/i18n/routes.ts +33 -0
  227. package/icon-bundle.ts +141 -0
  228. package/index.d.ts +13 -0
  229. package/modules/feature-routing.ts +32 -0
  230. package/nuxt.config.ts +194 -0
  231. package/package.json +91 -0
  232. package/runtime/markdown/inline-code-highlight.ts +155 -0
  233. package/runtime/server/agent-markdown.ts +21 -0
  234. package/runtime/utils.ts +7 -0
  235. package/server/mcp/tools/get-page.ts +14 -0
  236. package/server/mcp/tools/list-pages.ts +10 -0
  237. package/server/plugins/agent-markdown.ts +6 -0
  238. package/shared/route-slugs.ts +5 -0
  239. package/shared/types/app-config.ts +100 -0
  240. package/tags.ts +204 -0
@@ -0,0 +1,31 @@
1
+ import { useRoute, useSwitchLocalePath } from "#imports";
2
+ import type { RouteLocationRaw } from "vue-router";
3
+ import { useContentRouteAlternates } from "./useContentRouteAlternates";
4
+ import { isLocaleCode } from "../../i18n/locales";
5
+
6
+ export function useLocalizedRouteSwitch() {
7
+ const route = useRoute();
8
+ const switchLocalePath = useSwitchLocalePath();
9
+ const alternates = useContentRouteAlternates();
10
+
11
+ function switchPathname(targetLocale: string) {
12
+ if (!isLocaleCode(targetLocale)) {
13
+ return route.path;
14
+ }
15
+
16
+ const configuredLocale = targetLocale as Parameters<typeof switchLocalePath>[0];
17
+ return (
18
+ alternates.value.find((alternate) => alternate.locale === targetLocale)?.path ||
19
+ switchLocalePath(configuredLocale)
20
+ );
21
+ }
22
+
23
+ function switchPath(targetLocale: string): RouteLocationRaw {
24
+ return { path: switchPathname(targetLocale), query: route.query, hash: route.hash };
25
+ }
26
+
27
+ return {
28
+ switchPath,
29
+ switchPathname,
30
+ };
31
+ }
@@ -0,0 +1,79 @@
1
+ import { agentRawPathForRoute } from "@lupinum/ginko-content/agent-paths";
2
+ import { useClipboard } from "@vueuse/core";
3
+ import { computed, onBeforeUnmount, ref } from "vue";
4
+ import { useRoute } from "#imports";
5
+ import { useGinkoDocsConfig } from "./useGinkoDocsConfig";
6
+
7
+ export type PageMarkdownAction = "markdown" | "link" | "mcp";
8
+ export type PageMarkdownActionResult = "success" | "error";
9
+
10
+ export function usePageMarkdownActions() {
11
+ const route = useRoute();
12
+ const config = useGinkoDocsConfig();
13
+ const { copy } = useClipboard({ legacy: true });
14
+ const result = ref<{ action: PageMarkdownAction; status: PageMarkdownActionResult } | null>(null);
15
+ let resetTimer: ReturnType<typeof setTimeout> | undefined;
16
+
17
+ const rawPath = computed(() => agentRawPathForRoute(route.path));
18
+ const rawUrl = computed(() => new URL(rawPath.value, config.site.url).toString());
19
+ const mcpUrl = computed(() => new URL("/mcp", config.site.url).toString());
20
+ const chatGptUrl = computed(
21
+ () =>
22
+ `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${rawUrl.value} so I can ask questions about it.`)}`,
23
+ );
24
+ const claudeUrl = computed(
25
+ () =>
26
+ `https://claude.ai/new?q=${encodeURIComponent(`Read ${rawUrl.value} so I can ask questions about it.`)}`,
27
+ );
28
+ const actions = computed(() => ({
29
+ chatGpt: config.markdownActions?.chatGpt !== false,
30
+ claude: config.markdownActions?.claude !== false,
31
+ mcp: config.markdownActions?.mcp !== false,
32
+ }));
33
+
34
+ function setResult(action: PageMarkdownAction, status: PageMarkdownActionResult) {
35
+ if (resetTimer) clearTimeout(resetTimer);
36
+ result.value = { action, status };
37
+ resetTimer = setTimeout(
38
+ () => {
39
+ result.value = null;
40
+ },
41
+ status === "success" ? 1800 : 4000,
42
+ );
43
+ }
44
+
45
+ async function run(action: PageMarkdownAction, operation: () => Promise<void>) {
46
+ try {
47
+ await operation();
48
+ setResult(action, "success");
49
+ } catch {
50
+ setResult(action, "error");
51
+ }
52
+ }
53
+
54
+ const copyValue = (value: string, action: Exclude<PageMarkdownAction, "markdown">) =>
55
+ run(action, () => copy(value));
56
+
57
+ const copyMarkdown = () =>
58
+ run("markdown", async () => {
59
+ const response = await fetch(rawPath.value);
60
+ if (!response.ok) throw new Error(`Markdown request failed with ${response.status}`);
61
+ await copy(await response.text());
62
+ });
63
+
64
+ onBeforeUnmount(() => {
65
+ if (resetTimer) clearTimeout(resetTimer);
66
+ });
67
+
68
+ return {
69
+ actions,
70
+ chatGptUrl,
71
+ claudeUrl,
72
+ copyMarkdown,
73
+ copyValue,
74
+ mcpUrl,
75
+ rawPath,
76
+ rawUrl,
77
+ result,
78
+ };
79
+ }
@@ -0,0 +1,25 @@
1
+ import { toValue, type MaybeRefOrGetter } from "vue";
2
+ import { useHead } from "#imports";
3
+
4
+ export type SchemaJsonNode = Record<string, unknown>;
5
+
6
+ export function useSchemaJsonLd(schemas: MaybeRefOrGetter<SchemaJsonNode[]>) {
7
+ useHead(() => {
8
+ const graph = toValue(schemas).filter(Boolean);
9
+
10
+ return {
11
+ script: graph.length
12
+ ? [
13
+ {
14
+ type: "application/ld+json",
15
+ "data-schema-org": "true",
16
+ innerHTML: JSON.stringify({
17
+ "@context": "https://schema.org",
18
+ "@graph": graph,
19
+ }),
20
+ },
21
+ ]
22
+ : [],
23
+ };
24
+ });
25
+ }
@@ -0,0 +1,86 @@
1
+ import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
2
+ import { useI18n, useRouter } from "#imports";
3
+ import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
4
+ import { computed } from "vue";
5
+ import { useGinkoDocsConfig } from "./useGinkoDocsConfig";
6
+ import { resolveBanner, resolveMainNav, type NavItem } from "./site-navigation.utils";
7
+
8
+ export interface SidebarSection {
9
+ title: string;
10
+ items: { label: string; href: string; badge?: string }[];
11
+ }
12
+
13
+ export const useSiteNavigation = () => {
14
+ const config = useGinkoDocsConfig();
15
+ const { locale, t } = useI18n();
16
+ const router = useRouter();
17
+ const path = useLocalizedPath();
18
+
19
+ const site = computed(() => ({
20
+ name: getLocalizedSiteText(config.site.name, locale.value),
21
+ description: getLocalizedSiteText(config.site.description, locale.value),
22
+ logo: config.site.logo,
23
+ url: config.site.url,
24
+ lupinumAttribution: config.site.lupinumAttribution,
25
+ }));
26
+
27
+ const blogPath = computed(() => path("blog"));
28
+ const blogExists = computed(() =>
29
+ router.getRoutes().some((route) => route.path === blogPath.value),
30
+ );
31
+
32
+ const banner = computed(() =>
33
+ resolveBanner(config.banner, {
34
+ locale: locale.value,
35
+ blogPath: blogPath.value,
36
+ blogExists: blogExists.value,
37
+ defaultText: t("banner.text"),
38
+ defaultLinkLabel: t("banner.linkLabel"),
39
+ }),
40
+ );
41
+
42
+ const socialLinks = computed<NavItem[]>(
43
+ () =>
44
+ [
45
+ config.social.github
46
+ ? {
47
+ label: t("nav.github"),
48
+ href: config.social.github,
49
+ external: true,
50
+ icon: "lucide:github",
51
+ }
52
+ : null,
53
+ config.social.linkedin
54
+ ? {
55
+ label: "LinkedIn",
56
+ href: config.social.linkedin,
57
+ external: true,
58
+ icon: "lucide:linkedin",
59
+ }
60
+ : null,
61
+ ].filter(Boolean) as NavItem[],
62
+ );
63
+
64
+ const mainNav = computed<NavItem[]>(() =>
65
+ resolveMainNav(config.nav.links, {
66
+ locale: locale.value,
67
+ docsPath: path("docs"),
68
+ blogPath: blogPath.value,
69
+ blogExists: blogExists.value,
70
+ docsLabel: t("nav.documentation"),
71
+ blogLabel: t("nav.blog"),
72
+ }),
73
+ );
74
+
75
+ const footerNav = computed<{
76
+ product: NavItem[];
77
+ resources: NavItem[];
78
+ company: NavItem[];
79
+ }>(() => ({
80
+ product: [],
81
+ resources: [...socialLinks.value],
82
+ company: [],
83
+ }));
84
+
85
+ return { site, banner, mainNav, footerNav, socialLinks };
86
+ };
@@ -0,0 +1,36 @@
1
+ import type { GinkoDocsLocalizedText } from "../../shared/types/app-config";
2
+ import { defaultLocale, type LocaleCode } from "../../i18n/locales";
3
+
4
+ export function getLocalizedSiteText(
5
+ value: GinkoDocsLocalizedText | string,
6
+ locale: string,
7
+ fallbackLocale: LocaleCode = defaultLocale,
8
+ ): string {
9
+ if (typeof value === "string") return value;
10
+ return value[locale as LocaleCode] ?? value[fallbackLocale] ?? Object.values(value)[0] ?? "";
11
+ }
12
+
13
+ function isLocalSiteUrl(value: string) {
14
+ try {
15
+ const host = new URL(value).hostname;
16
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export function resolveSiteUrl(
23
+ canonicalUrl: string,
24
+ options: {
25
+ envUrl?: string;
26
+ mode?: string;
27
+ } = {},
28
+ ) {
29
+ const { envUrl, mode } = options;
30
+
31
+ if (!envUrl) return canonicalUrl;
32
+ if (isLocalSiteUrl(envUrl) && mode !== "development" && mode !== "test") {
33
+ return canonicalUrl;
34
+ }
35
+ return envUrl;
36
+ }
package/app/error.vue ADDED
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ import { localizedRoutes } from "../i18n/routes";
3
+ import { messages } from "../i18n/messages/index";
4
+ import { normalizeAppError } from "./lib/errors";
5
+ import { computed } from "vue";
6
+ import { clearError, useHead } from "#imports";
7
+ import { useDocsEntryPath } from "#ginko-docs/features/docs/composables/useDocsEntryPath";
8
+ import { localeFromPath, localizedPath } from "../i18n/locales";
9
+
10
+ type NuxtErrorLike = {
11
+ message?: string;
12
+ status?: number;
13
+ statusCode?: number;
14
+ statusMessage?: string;
15
+ statusText?: string;
16
+ url?: string;
17
+ };
18
+
19
+ const props = defineProps<{
20
+ error: NuxtErrorLike;
21
+ }>();
22
+
23
+ const normalized = computed(() => normalizeAppError(props.error));
24
+ const requestPath = computed(
25
+ () => props.error?.url || (import.meta.client ? window.location.pathname : "/"),
26
+ );
27
+ const locale = computed(() => localeFromPath(requestPath.value));
28
+ const copy = computed(() => {
29
+ const catalog = messages[locale.value].errors;
30
+ return catalog[normalized.value.kind] ?? catalog.unavailable;
31
+ });
32
+ const docsPath = await useDocsEntryPath();
33
+ const homePath = computed(() => localizedPath(locale.value, localizedRoutes[locale.value].home));
34
+ const title = computed(() => `${normalized.value.statusCode} - ${copy.value.title}`);
35
+ // Keep the full site chrome for content-level errors (404 and friends); hard
36
+ // server failures fall back to the bare page so the error screen cannot crash.
37
+ const showChrome = computed(() => normalized.value.kind !== "server");
38
+
39
+ useHead(() => ({
40
+ title: title.value,
41
+ meta: [{ name: "robots", content: "noindex, nofollow" }],
42
+ }));
43
+
44
+ const handleError = () => clearError({ redirect: homePath.value });
45
+ </script>
46
+
47
+ <template>
48
+ <div class="flex min-h-dvh flex-col bg-background text-foreground">
49
+ <template v-if="showChrome">
50
+ <SiteSkipLink />
51
+ <SiteHeader />
52
+ </template>
53
+ <main id="main-content" class="flex flex-1 items-center justify-center px-4 py-24">
54
+ <section class="w-full max-w-xl text-center">
55
+ <p
56
+ class="mb-6 text-8xl leading-none font-bold text-primary/20 select-none"
57
+ aria-hidden="true"
58
+ >
59
+ {{ normalized.statusCode }}
60
+ </p>
61
+
62
+ <p class="mb-3 text-sm font-medium tracking-wider text-primary uppercase">
63
+ {{ normalized.statusMessage }}
64
+ </p>
65
+ <h1 class="mb-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
66
+ {{ copy.title }}
67
+ </h1>
68
+ <p class="mx-auto mb-8 max-w-md leading-7 text-muted-foreground">
69
+ {{ copy.description }}
70
+ </p>
71
+
72
+ <div class="flex flex-col items-center justify-center gap-3 sm:flex-row">
73
+ <button class="error-action error-action-primary" type="button" @click="handleError">
74
+ {{ messages[locale].errors.actions.home }}
75
+ </button>
76
+ <a class="error-action error-action-secondary" :href="docsPath">
77
+ {{ messages[locale].errors.actions.docs }}
78
+ </a>
79
+ </div>
80
+ </section>
81
+ </main>
82
+ <SiteFooter v-if="showChrome" />
83
+ </div>
84
+ </template>
85
+
86
+ <style scoped>
87
+ .error-action {
88
+ align-items: center;
89
+ border-radius: 0.375rem;
90
+ display: inline-flex;
91
+ font-size: 0.875rem;
92
+ font-weight: 500;
93
+ height: 2.5rem;
94
+ justify-content: center;
95
+ padding-inline: 1rem;
96
+ transition:
97
+ background-color 150ms,
98
+ color 150ms,
99
+ border-color 150ms;
100
+ }
101
+
102
+ .error-action:focus-visible {
103
+ outline: 2px solid var(--ring);
104
+ outline-offset: 2px;
105
+ }
106
+
107
+ .error-action-primary {
108
+ background: var(--primary);
109
+ color: var(--primary-foreground);
110
+ }
111
+
112
+ .error-action-primary:hover {
113
+ opacity: 0.9;
114
+ }
115
+
116
+ .error-action-secondary {
117
+ background: var(--background);
118
+ border: 1px solid var(--input);
119
+ }
120
+
121
+ .error-action-secondary:hover {
122
+ background: var(--accent);
123
+ color: var(--accent-foreground);
124
+ }
125
+ </style>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { GinkoDocsAppConfig } from "../../../../shared/types/app-config";
3
+ import { computed } from "vue";
4
+ import { useAppConfig, useI18n, useRoute } from "#imports";
5
+
6
+ const props = defineProps<{
7
+ stem?: string;
8
+ extension?: string;
9
+ title: string;
10
+ }>();
11
+
12
+ const route = useRoute();
13
+ const { t } = useI18n();
14
+ const repository = (useAppConfig().ginkoDocs as GinkoDocsAppConfig).repository;
15
+
16
+ const sourcePath = computed(() => {
17
+ if (!repository || !props.stem) return null;
18
+ const directory = repository.contentDirectory?.replace(/^\/+|\/+$/g, "") ?? "content";
19
+ const extension = props.extension?.replace(/^\./, "") || "md";
20
+ return `${directory}/${props.stem}.${extension}`;
21
+ });
22
+
23
+ const editUrl = computed(() => {
24
+ if (!repository || !sourcePath.value) return null;
25
+ const branch = repository.branch || "main";
26
+ return `${repository.url.replace(/\/$/, "")}/edit/${encodeURIComponent(branch)}/${sourcePath.value}`;
27
+ });
28
+
29
+ const issueUrl = computed(() => {
30
+ if (!repository) return null;
31
+ const url = new URL(`${repository.url.replace(/\/$/, "")}/issues/new`);
32
+ url.searchParams.set("title", t("docs.issueTitle", { title: props.title }));
33
+ url.searchParams.set("body", t("docs.issueBody", { path: route.path }));
34
+ return url.toString();
35
+ });
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ v-if="repository"
41
+ class="mt-12 flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-border pt-5 text-sm"
42
+ :aria-label="t('docs.contribute')"
43
+ >
44
+ <a
45
+ v-if="editUrl"
46
+ :href="editUrl"
47
+ target="_blank"
48
+ rel="noopener noreferrer"
49
+ class="inline-flex min-h-9 items-center gap-1.5 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
50
+ >
51
+ <Icon name="lucide:pen-line" class="size-4" aria-hidden="true" />
52
+ {{ t("docs.editPage") }}
53
+ </a>
54
+ <a
55
+ v-if="issueUrl"
56
+ :href="issueUrl"
57
+ target="_blank"
58
+ rel="noopener noreferrer"
59
+ class="inline-flex min-h-9 items-center gap-1.5 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
60
+ >
61
+ <Icon name="lucide:circle-alert" class="size-4" aria-hidden="true" />
62
+ {{ t("docs.reportIssue") }}
63
+ </a>
64
+ </div>
65
+ </template>
@@ -0,0 +1,104 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { ref, watch } from "vue";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetTitle,
9
+ SheetTrigger,
10
+ } from "#ginko-docs/components/ui/sheet";
11
+ import { cn } from "#ginko-docs/lib/utils";
12
+ import type { TocItem } from "./DocsToc.vue";
13
+ import DocsToc from "./DocsToc.vue";
14
+ import { useI18n, useRoute } from "#imports";
15
+
16
+ const props = defineProps<{
17
+ items: TocItem[];
18
+ activeId?: string;
19
+ pageTitle?: string;
20
+ class?: HTMLAttributes["class"];
21
+ }>();
22
+
23
+ const sidebarOpen = ref(false);
24
+ const tocOpen = ref(false);
25
+ const route = useRoute();
26
+ const { t } = useI18n();
27
+
28
+ watch(
29
+ () => route.hash,
30
+ () => {
31
+ tocOpen.value = false;
32
+ },
33
+ );
34
+ watch(
35
+ () => route.path,
36
+ () => {
37
+ sidebarOpen.value = false;
38
+ tocOpen.value = false;
39
+ },
40
+ );
41
+ </script>
42
+
43
+ <template>
44
+ <div
45
+ :class="
46
+ cn(
47
+ 'sticky top-[var(--site-header-height)] z-30 flex h-11 shrink-0 items-center justify-between border-b border-border bg-background/90 px-4 backdrop-blur-md md:px-6 xl:hidden',
48
+ props.class,
49
+ )
50
+ "
51
+ >
52
+ <Sheet v-model:open="sidebarOpen">
53
+ <SheetTrigger as-child>
54
+ <button
55
+ type="button"
56
+ class="-ml-1 flex h-8 min-w-0 items-center gap-2 rounded-lg px-2 text-sm font-medium text-foreground transition-colors hover:bg-muted"
57
+ :aria-label="t('docs.pageNavigation')"
58
+ >
59
+ <Icon
60
+ name="lucide:panel-left-open"
61
+ class="size-4 shrink-0 text-muted-foreground"
62
+ aria-hidden="true"
63
+ />
64
+ <span class="truncate">{{ pageTitle ?? t("docs.fallbackTitle") }}</span>
65
+ </button>
66
+ </SheetTrigger>
67
+
68
+ <SheetContent side="left" class="flex w-[20rem] max-w-[86vw] flex-col gap-0 p-0">
69
+ <div class="border-b border-border px-4 py-3">
70
+ <SheetTitle class="text-sm font-semibold">{{ t("docs.label") }}</SheetTitle>
71
+ <SheetDescription class="sr-only">{{ t("docs.pageNavigation") }}</SheetDescription>
72
+ </div>
73
+ <div class="min-h-0 flex-1">
74
+ <DocsSidebar variant="drawer" />
75
+ </div>
76
+ </SheetContent>
77
+ </Sheet>
78
+
79
+ <Sheet v-if="items.length" v-model:open="tocOpen">
80
+ <SheetTrigger as-child>
81
+ <button
82
+ class="-mr-1 flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
83
+ >
84
+ <Icon name="lucide:list" class="size-3.5" aria-hidden="true" />
85
+ <span>{{ t("docs.toc") }}</span>
86
+ </button>
87
+ </SheetTrigger>
88
+
89
+ <SheetContent side="bottom" class="flex max-h-[85dvh] flex-col gap-0 rounded-t-2xl p-0">
90
+ <div class="flex shrink-0 flex-col items-center border-b border-border px-6 pt-3 pb-4">
91
+ <div class="mb-4 h-1 w-10 rounded-full bg-border" />
92
+ <SheetTitle class="self-start text-sm font-semibold">{{ t("docs.toc") }}</SheetTitle>
93
+ <SheetDescription class="sr-only">{{
94
+ pageTitle ?? t("docs.fallbackTitle")
95
+ }}</SheetDescription>
96
+ </div>
97
+
98
+ <div class="flex-1 overflow-y-auto px-6 py-5" @click="tocOpen = false">
99
+ <DocsToc :items="items" :active-id="activeId" :show-title="false" />
100
+ </div>
101
+ </SheetContent>
102
+ </Sheet>
103
+ </div>
104
+ </template>