@lupinum/ginko-docs 0.4.0-rc.9 → 0.4.0

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 (296) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +1 -1
  3. package/README.md +206 -54
  4. package/app/app.config.ts +42 -55
  5. package/app/assets/docs.css +555 -0
  6. package/app/components/DocsBanner.vue +29 -0
  7. package/app/components/DocsExample.vue +43 -0
  8. package/app/components/DocsFooter.vue +19 -0
  9. package/app/components/DocsHeader.vue +112 -0
  10. package/app/components/DocsLanding.vue +55 -0
  11. package/app/components/DocsLanguage.vue +29 -0
  12. package/app/components/DocsNavTree.vue +38 -0
  13. package/app/components/DocsPage.vue +84 -0
  14. package/app/components/DocsPageActions.vue +47 -0
  15. package/app/components/DocsPost.vue +58 -0
  16. package/app/components/DocsSearch.vue +134 -0
  17. package/app/components/DocsShell.vue +20 -0
  18. package/app/components/DocsSidebar.vue +18 -0
  19. package/app/components/DocsTable.vue +11 -0
  20. package/app/components/DocsThemeToggle.vue +45 -0
  21. package/app/components/DocsToc.vue +73 -0
  22. package/app/composables/useDocs.ts +52 -0
  23. package/app/composables/useDocsBlog.ts +32 -0
  24. package/app/composables/useDocsCopy.ts +17 -0
  25. package/app/composables/useDocsPopover.ts +36 -0
  26. package/app/composables/useDocsSite.ts +10 -0
  27. package/app/docs-pages/blog.vue +47 -0
  28. package/app/docs-pages/changelog.vue +27 -0
  29. package/app/docs-pages/doc.vue +14 -0
  30. package/app/docs-pages/docs.vue +9 -0
  31. package/app/docs-pages/index.vue +12 -0
  32. package/app/docs-pages/post.vue +8 -0
  33. package/app/error.vue +23 -115
  34. package/app/layouts/default.vue +25 -7
  35. package/app/lib/blogTypes.ts +8 -0
  36. package/app/lib/docsFirst.ts +11 -0
  37. package/app/lib/docsInit.ts +18 -0
  38. package/app/lib/docsNav.ts +36 -0
  39. package/app/lib/docsPage.ts +44 -0
  40. package/app/lib/markdownComponents.ts +16 -0
  41. package/app/lib/previewToc.ts +18 -0
  42. package/dist/config.d.mts +232 -0
  43. package/dist/config.d.ts +232 -0
  44. package/dist/config.mjs +114 -0
  45. package/dist/pages.d.mts +5 -0
  46. package/dist/pages.d.ts +5 -0
  47. package/dist/pages.mjs +36 -0
  48. package/dist/shared/ginko-docs.BwbF7JBR.mjs +4 -0
  49. package/i18n/de.json +1 -0
  50. package/i18n/en.json +1 -0
  51. package/icons/README.md +2 -0
  52. package/icons/bluesky.svg +1 -0
  53. package/icons/claude.svg +1 -0
  54. package/icons/discord.svg +1 -0
  55. package/icons/github.svg +1 -0
  56. package/icons/linkedin.svg +1 -0
  57. package/icons/mastodon.svg +1 -0
  58. package/icons/openai.svg +1 -0
  59. package/icons/x.svg +1 -0
  60. package/icons/youtube.svg +1 -0
  61. package/nuxt.config.ts +34 -209
  62. package/package.json +43 -76
  63. package/public/_docs/fonts/LICENSE +188 -0
  64. package/public/_docs/fonts/inter-latin-wght-normal.woff2 +0 -0
  65. package/public/_docs/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  66. package/public/_docs/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  67. package/server/feeds/blog-locale.get.ts +9 -0
  68. package/server/feeds/blog.get.ts +4 -0
  69. package/server/lib/blogFeed.ts +35 -0
  70. package/server/lib/rss.ts +57 -0
  71. package/server/mcp/handler.ts +108 -0
  72. package/server/routes/__ginko/docs-mcp.ts +35 -0
  73. package/shared/paths.ts +4 -0
  74. package/app/app.vue +0 -98
  75. package/app/assets/css/prose.css +0 -3240
  76. package/app/assets/css/tailwind.css +0 -394
  77. package/app/assets/css/theme-palettes.css +0 -309
  78. package/app/assets/css/theme-presets.css +0 -141
  79. package/app/components/OgImage/GinkoDocs.satori.vue +0 -238
  80. package/app/components/content/DocumentPageShell.vue +0 -55
  81. package/app/components/content/Feedback.vue +0 -102
  82. package/app/components/content/ImageZoomDialog.vue +0 -84
  83. package/app/components/content/PageMarkdownCopy.vue +0 -104
  84. package/app/components/content/imageZoom.ts +0 -25
  85. package/app/components/mdc/MdcAccordion.vue +0 -67
  86. package/app/components/mdc/MdcAccordionItem.vue +0 -42
  87. package/app/components/mdc/MdcApi.vue +0 -159
  88. package/app/components/mdc/MdcAside.vue +0 -23
  89. package/app/components/mdc/MdcCallout.vue +0 -61
  90. package/app/components/mdc/MdcCard.vue +0 -175
  91. package/app/components/mdc/MdcCards.vue +0 -30
  92. package/app/components/mdc/MdcCenter.vue +0 -71
  93. package/app/components/mdc/MdcCodeGroup.vue +0 -197
  94. package/app/components/mdc/MdcCodeTree.vue +0 -234
  95. package/app/components/mdc/MdcCollapse.vue +0 -61
  96. package/app/components/mdc/MdcColumn.vue +0 -70
  97. package/app/components/mdc/MdcDropcap.vue +0 -84
  98. package/app/components/mdc/MdcError.vue +0 -23
  99. package/app/components/mdc/MdcExcerpt.vue +0 -26
  100. package/app/components/mdc/MdcFigure.vue +0 -85
  101. package/app/components/mdc/MdcFiles.vue +0 -120
  102. package/app/components/mdc/MdcIdea.vue +0 -17
  103. package/app/components/mdc/MdcInfo.vue +0 -17
  104. package/app/components/mdc/MdcInlineToc.vue +0 -51
  105. package/app/components/mdc/MdcKbd.vue +0 -10
  106. package/app/components/mdc/MdcLayout.vue +0 -57
  107. package/app/components/mdc/MdcNote.vue +0 -17
  108. package/app/components/mdc/MdcQuiz.vue +0 -241
  109. package/app/components/mdc/MdcQuizQuestion.vue +0 -221
  110. package/app/components/mdc/MdcReadMore.vue +0 -70
  111. package/app/components/mdc/MdcSteps.vue +0 -59
  112. package/app/components/mdc/MdcSuccess.vue +0 -23
  113. package/app/components/mdc/MdcTab.vue +0 -17
  114. package/app/components/mdc/MdcTabs.vue +0 -150
  115. package/app/components/mdc/MdcTimeline.vue +0 -17
  116. package/app/components/mdc/MdcTimelineItem.vue +0 -40
  117. package/app/components/mdc/MdcWarning.vue +0 -23
  118. package/app/components/mdc/api.utils.ts +0 -132
  119. package/app/components/mdc/code-tree.utils.ts +0 -22
  120. package/app/components/mdc/icons.ts +0 -10
  121. package/app/components/mdc/quiz.utils.ts +0 -38
  122. package/app/components/prose/ProseA.vue +0 -27
  123. package/app/components/prose/ProseBlockquote.vue +0 -30
  124. package/app/components/prose/ProseCode.vue +0 -14
  125. package/app/components/prose/ProseEm.vue +0 -14
  126. package/app/components/prose/ProseH1.vue +0 -18
  127. package/app/components/prose/ProseH2.vue +0 -29
  128. package/app/components/prose/ProseH3.vue +0 -29
  129. package/app/components/prose/ProseH4.vue +0 -29
  130. package/app/components/prose/ProseH5.vue +0 -15
  131. package/app/components/prose/ProseH6.vue +0 -15
  132. package/app/components/prose/ProseHr.vue +0 -12
  133. package/app/components/prose/ProseImg.vue +0 -55
  134. package/app/components/prose/ProseLi.vue +0 -14
  135. package/app/components/prose/ProseOl.vue +0 -14
  136. package/app/components/prose/ProseP.vue +0 -14
  137. package/app/components/prose/ProsePre.vue +0 -180
  138. package/app/components/prose/ProseStrong.vue +0 -14
  139. package/app/components/prose/ProseTable.vue +0 -18
  140. package/app/components/prose/ProseTbody.vue +0 -14
  141. package/app/components/prose/ProseTd.vue +0 -14
  142. package/app/components/prose/ProseTh.vue +0 -14
  143. package/app/components/prose/ProseThead.vue +0 -14
  144. package/app/components/prose/ProseTr.vue +0 -14
  145. package/app/components/prose/ProseUl.vue +0 -14
  146. package/app/components/site/ModeToggle.vue +0 -81
  147. package/app/components/site/SiteBanner.vue +0 -97
  148. package/app/components/site/SiteFooter.vue +0 -90
  149. package/app/components/site/SiteHeader.vue +0 -371
  150. package/app/components/site/SiteHeroCode.vue +0 -173
  151. package/app/components/site/SiteInteractionLayer.vue +0 -42
  152. package/app/components/site/SiteLocaleSwitcher.vue +0 -190
  153. package/app/components/site/SiteLogoMark.vue +0 -12
  154. package/app/components/site/SiteSkipLink.vue +0 -14
  155. package/app/components/site/SiteSocialLinks.vue +0 -33
  156. package/app/components/site/header-utils.ts +0 -3
  157. package/app/components/site/mode-toggle.utils.ts +0 -18
  158. package/app/components/ui/badge/Badge.vue +0 -28
  159. package/app/components/ui/badge/index.ts +0 -24
  160. package/app/components/ui/button/Button.vue +0 -31
  161. package/app/components/ui/button/index.ts +0 -35
  162. package/app/components/ui/collapsible/Collapsible.vue +0 -15
  163. package/app/components/ui/collapsible/CollapsibleContent.vue +0 -25
  164. package/app/components/ui/collapsible/CollapsibleTrigger.vue +0 -12
  165. package/app/components/ui/collapsible/index.ts +0 -3
  166. package/app/components/ui/dropdown-menu/DropdownMenu.vue +0 -15
  167. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +0 -50
  168. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +0 -41
  169. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +0 -15
  170. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +0 -37
  171. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +0 -23
  172. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +0 -14
  173. package/app/components/ui/dropdown-menu/index.ts +0 -7
  174. package/app/components/ui/kbd/Kbd.vue +0 -23
  175. package/app/components/ui/kbd/index.ts +0 -21
  176. package/app/components/ui/scroll-area/ScrollArea.vue +0 -39
  177. package/app/components/ui/scroll-area/ScrollBar.vue +0 -33
  178. package/app/components/ui/scroll-area/index.ts +0 -2
  179. package/app/components/ui/separator/Separator.vue +0 -27
  180. package/app/components/ui/separator/index.ts +0 -1
  181. package/app/components/ui/sheet/Sheet.vue +0 -15
  182. package/app/components/ui/sheet/SheetContent.vue +0 -59
  183. package/app/components/ui/sheet/SheetDescription.vue +0 -21
  184. package/app/components/ui/sheet/SheetOverlay.vue +0 -26
  185. package/app/components/ui/sheet/SheetTitle.vue +0 -21
  186. package/app/components/ui/sheet/SheetTrigger.vue +0 -12
  187. package/app/components/ui/sheet/index.ts +0 -5
  188. package/app/components/ui/tabs/Tabs.vue +0 -24
  189. package/app/components/ui/tabs/TabsList.vue +0 -26
  190. package/app/components/ui/tabs/TabsTrigger.vue +0 -28
  191. package/app/components/ui/tabs/index.ts +0 -3
  192. package/app/composables/site-navigation.utils.ts +0 -133
  193. package/app/composables/useCanonicalUrl.ts +0 -9
  194. package/app/composables/useContentRouteAlternates.ts +0 -35
  195. package/app/composables/useGinkoAnalytics.ts +0 -52
  196. package/app/composables/useGinkoDocsConfig.ts +0 -6
  197. package/app/composables/useGinkoOgImage.ts +0 -20
  198. package/app/composables/useLocalizedPath.ts +0 -12
  199. package/app/composables/useLocalizedRouteSwitch.ts +0 -31
  200. package/app/composables/useMetaKey.ts +0 -18
  201. package/app/composables/usePageMarkdownActions.ts +0 -79
  202. package/app/composables/useProseAppearance.ts +0 -24
  203. package/app/composables/useSchemaJsonLd.ts +0 -25
  204. package/app/composables/useSiteNavigation.ts +0 -64
  205. package/app/config/site.utils.ts +0 -36
  206. package/app/features/docs/components/DocsBreadcrumb.vue +0 -37
  207. package/app/features/docs/components/DocsContributeLinks.vue +0 -75
  208. package/app/features/docs/components/DocsMobileToc.vue +0 -108
  209. package/app/features/docs/components/DocsPageContent.vue +0 -234
  210. package/app/features/docs/components/DocsPageNav.vue +0 -57
  211. package/app/features/docs/components/DocsSidebar.vue +0 -145
  212. package/app/features/docs/components/DocsSidebarDropdown.vue +0 -70
  213. package/app/features/docs/components/DocsSidebarItem.vue +0 -122
  214. package/app/features/docs/components/DocsSidebarList.vue +0 -35
  215. package/app/features/docs/components/DocsSidebarRow.vue +0 -27
  216. package/app/features/docs/components/DocsSidebarTabs.vue +0 -59
  217. package/app/features/docs/components/DocsToc.vue +0 -106
  218. package/app/features/docs/composables/useDocsNavigation.ts +0 -29
  219. package/app/features/docs/composables/useDocsNavigationData.ts +0 -18
  220. package/app/features/docs/composables/useRevealActive.ts +0 -58
  221. package/app/features/docs/composables/useScrollspy.ts +0 -135
  222. package/app/features/docs/docs-navigation.ts +0 -157
  223. package/app/features/docs/toc-context.ts +0 -6
  224. package/app/features/search/command-center.ts +0 -110
  225. package/app/features/search/components/SiteCommandCenter.vue +0 -359
  226. package/app/features/search/components/SiteSearchPageHighlight.vue +0 -126
  227. package/app/features/search/useCommandCenter.ts +0 -292
  228. package/app/layouts/blog.vue +0 -15
  229. package/app/layouts/docs.vue +0 -20
  230. package/app/lib/errors.ts +0 -37
  231. package/app/lib/schema-org.ts +0 -38
  232. package/app/pages/blog/[slug].vue +0 -228
  233. package/app/pages/blog/index.vue +0 -130
  234. package/app/pages/docs/[...slug].vue +0 -10
  235. package/app/pages/docs/index.vue +0 -19
  236. package/app/pages/index.vue +0 -278
  237. package/app/plugins/ginko-docs-theme.ts +0 -14
  238. package/app/plugins/ssr-width.ts +0 -6
  239. package/app/utils/content.ts +0 -67
  240. package/app/utils/file-icons.ts +0 -90
  241. package/app/utils/index.ts +0 -7
  242. package/app/utils/repository.ts +0 -20
  243. package/components.ts +0 -9
  244. package/content-collections.ts +0 -89
  245. package/content.js +0 -200
  246. package/content.ts +0 -104
  247. package/i18n/i18n.config.ts +0 -10
  248. package/i18n/locales.ts +0 -81
  249. package/i18n/messages/global/banner.ts +0 -8
  250. package/i18n/messages/global/blog.ts +0 -18
  251. package/i18n/messages/global/command.ts +0 -49
  252. package/i18n/messages/global/docs.ts +0 -36
  253. package/i18n/messages/global/errors.ts +0 -36
  254. package/i18n/messages/global/feedback.ts +0 -11
  255. package/i18n/messages/global/index.ts +0 -21
  256. package/i18n/messages/global/nav.ts +0 -26
  257. package/i18n/messages/global/site.ts +0 -4
  258. package/i18n/messages/global/theme.ts +0 -9
  259. package/i18n/messages/index.ts +0 -20
  260. package/i18n/messages/localize.ts +0 -35
  261. package/i18n/routes.ts +0 -33
  262. package/icon-bundle.ts +0 -116
  263. package/index.d.ts +0 -37
  264. package/modules/analytics-boundary.ts +0 -23
  265. package/modules/feature-routing.ts +0 -68
  266. package/modules/syntax-highlighting.ts +0 -120
  267. package/public/fonts/public-sans/LICENSE +0 -93
  268. package/public/fonts/public-sans/README.md +0 -12
  269. package/public/fonts/public-sans/public-sans-400-normal-latin.woff +0 -0
  270. package/public/fonts/public-sans/public-sans-500-normal-latin.woff +0 -0
  271. package/public/fonts/public-sans/public-sans-600-normal-latin.woff +0 -0
  272. package/public/fonts/public-sans/public-sans-700-normal-latin.woff +0 -0
  273. package/runtime/generate-favicons.mjs +0 -124
  274. package/runtime/server/agent-markdown.ts +0 -19
  275. package/server/api/_ginko-docs/redirects.get.ts +0 -28
  276. package/server/mcp/tools/get-page.ts +0 -14
  277. package/server/mcp/tools/list-pages.ts +0 -10
  278. package/server/middleware/redirects.ts +0 -22
  279. package/server/plugins/agent-markdown.ts +0 -6
  280. package/server/plugins/sitemap-policy.ts +0 -12
  281. package/server/routes/.well-known/ai-catalog.json.get.ts +0 -10
  282. package/server/routes/[locale]/blog/rss.xml.get.ts +0 -13
  283. package/server/routes/blog/rss.xml.get.ts +0 -5
  284. package/server/routes/mcp/server-card.get.ts +0 -14
  285. package/server/utils/blog-feed.ts +0 -51
  286. package/server/utils/feed.ts +0 -70
  287. package/server/utils/mcp-server-card.ts +0 -45
  288. package/server/utils/redirects.ts +0 -38
  289. package/server/utils/redirects.utils.ts +0 -58
  290. package/server/utils/sitemap.ts +0 -45
  291. package/shared/route-slugs.ts +0 -5
  292. package/shared/theme-palettes.ts +0 -37
  293. package/shared/theme-presets.ts +0 -3
  294. package/shared/types/app-config.ts +0 -172
  295. package/shared/types/nuxt-config.ts +0 -14
  296. package/tags.ts +0 -192
@@ -1,110 +0,0 @@
1
- export type CommandCenterGroup = "recent" | "pages" | "docs_nav" | "actions" | "search";
2
-
3
- export interface CommandCenterItem {
4
- id: string;
5
- sourceId?: string;
6
- title: string;
7
- subtitle?: string;
8
- href?: string;
9
- external?: boolean;
10
- group: CommandCenterGroup;
11
- icon?: string;
12
- keywords?: string[];
13
- badge?: string;
14
- }
15
-
16
- export type StoredRecentItem = string;
17
-
18
- export interface CommandCenterGroupResult {
19
- id: CommandCenterGroup;
20
- title: string;
21
- items: CommandCenterItem[];
22
- }
23
-
24
- export const MAX_RECENT_ITEMS = 5;
25
- export const PAGE_HIGHLIGHT_STORAGE_KEY = "site-command-center-page-highlight";
26
-
27
- const SEARCH_HIGHLIGHT_STOP_WORDS = new Set([
28
- "a",
29
- "an",
30
- "and",
31
- "der",
32
- "die",
33
- "das",
34
- "ein",
35
- "eine",
36
- "for",
37
- "in",
38
- "of",
39
- "or",
40
- "the",
41
- "to",
42
- "und",
43
- "von",
44
- "zu",
45
- ]);
46
-
47
- export function getSearchHighlightTerms(query: string): string[] {
48
- const terms = [...new Set(query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean))];
49
- const meaningfulTerms = terms.filter(
50
- (term) => term.length > 1 && !SEARCH_HIGHLIGHT_STOP_WORDS.has(term),
51
- );
52
-
53
- return meaningfulTerms.length > 0 ? meaningfulTerms : terms;
54
- }
55
-
56
- export function shouldShowSearchResultBadges(items: CommandCenterItem[]): boolean {
57
- return new Set(items.map((item) => item.badge).filter(Boolean)).size > 1;
58
- }
59
-
60
- export function dedupeCommandCenterItems(items: CommandCenterItem[]) {
61
- const seen = new Set<string>();
62
- return items.filter((item) => {
63
- const key = item.href ?? item.sourceId ?? item.id;
64
- if (seen.has(key)) return false;
65
- seen.add(key);
66
- return true;
67
- });
68
- }
69
-
70
- export function groupCommandCenterItems(
71
- items: CommandCenterItem[],
72
- groupTitle: (group: CommandCenterGroup) => string,
73
- ): CommandCenterGroupResult[] {
74
- const groups = new Map<CommandCenterGroup, CommandCenterItem[]>();
75
- for (const item of items) {
76
- const group = groups.get(item.group) ?? [];
77
- group.push(item);
78
- groups.set(item.group, group);
79
- }
80
-
81
- return [...groups].map(([id, groupItems]) => ({
82
- id,
83
- title: groupTitle(id),
84
- items: groupItems.slice(0, 5),
85
- }));
86
- }
87
-
88
- export function resolveRecentItems(
89
- storedItems: StoredRecentItem[],
90
- currentItems: CommandCenterItem[],
91
- ): CommandCenterItem[] {
92
- return storedItems
93
- .map((id) => currentItems.find((item) => item.id === id))
94
- .filter((item): item is CommandCenterItem => Boolean(item))
95
- .slice(0, MAX_RECENT_ITEMS)
96
- .map((item) => ({
97
- ...item,
98
- id: `recent-${item.id}`,
99
- sourceId: item.id,
100
- group: "recent",
101
- }));
102
- }
103
-
104
- export function rememberRecentItem(
105
- selectedItem: CommandCenterItem,
106
- storedItems: StoredRecentItem[],
107
- ): StoredRecentItem[] {
108
- const id = selectedItem.sourceId ?? selectedItem.id;
109
- return [id, ...storedItems.filter((item) => item !== id)].slice(0, MAX_RECENT_ITEMS);
110
- }
@@ -1,359 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from "vue";
3
- import { useI18n, useRoute } from "#imports";
4
- import { useCommandCenter } from "#ginko-docs/features/search/useCommandCenter";
5
- import {
6
- getSearchHighlightTerms,
7
- shouldShowSearchResultBadges,
8
- } from "#ginko-docs/features/search/command-center";
9
- import {
10
- DialogContent,
11
- DialogDescription,
12
- DialogOverlay,
13
- DialogPortal,
14
- DialogRoot,
15
- DialogTitle,
16
- VisuallyHidden,
17
- } from "reka-ui";
18
-
19
- const { t } = useI18n();
20
- const route = useRoute();
21
- const inputRef = useTemplateRef<HTMLInputElement>("input");
22
- const highlightedId = ref("");
23
- const { open, query, groupedItems, searchPending, searchError, searchIsEmpty, selectItem } =
24
- await useCommandCenter();
25
-
26
- const visibleItems = computed(() => groupedItems.value.flatMap((g) => g.items));
27
- const isSearching = computed(() => query.value.trim().length > 0);
28
- const showResultBadges = computed(
29
- () => !isSearching.value || shouldShowSearchResultBadges(visibleItems.value),
30
- );
31
-
32
- // ── text highlighting ──────────────────────────────────────────────────────
33
- function highlightTokens(text: string, q: string): { text: string; match: boolean }[] {
34
- const terms = getSearchHighlightTerms(q);
35
- if (terms.length === 0) return [{ text, match: false }];
36
- const pattern = terms.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
37
- return text
38
- .split(new RegExp(`(${pattern})`, "gi"))
39
- .filter((part) => part !== "")
40
- .map((part) => ({
41
- text: part,
42
- match: terms.some((term) => part.toLowerCase() === term.toLowerCase()),
43
- }));
44
- }
45
-
46
- // ── keyboard navigation ────────────────────────────────────────────────────
47
- function syncHighlight() {
48
- const first = visibleItems.value[0]?.id ?? "";
49
- if (!first) {
50
- highlightedId.value = "";
51
- return;
52
- }
53
- if (!visibleItems.value.some((i) => i.id === highlightedId.value)) {
54
- highlightedId.value = first;
55
- }
56
- }
57
-
58
- function moveHighlight(dir: 1 | -1) {
59
- if (!visibleItems.value.length) return;
60
- const idx = visibleItems.value.findIndex((i) => i.id === highlightedId.value);
61
- const next = idx === -1 ? 0 : (idx + dir + visibleItems.value.length) % visibleItems.value.length;
62
- highlightedId.value = visibleItems.value[next]?.id ?? "";
63
- }
64
-
65
- async function activateHighlighted() {
66
- const item = visibleItems.value.find((i) => i.id === highlightedId.value);
67
- if (item) await selectItem(item);
68
- }
69
-
70
- async function clearQuery() {
71
- query.value = "";
72
- await nextTick();
73
- inputRef.value?.focus();
74
- }
75
-
76
- function itemLabel(item: { title: string; subtitle?: string; badge?: string }) {
77
- return [item.title, item.badge, item.subtitle].filter(Boolean).join(", ");
78
- }
79
-
80
- function handleListNavigation(e: KeyboardEvent) {
81
- if (e.key === "ArrowDown") {
82
- e.preventDefault();
83
- moveHighlight(1);
84
- } else if (e.key === "ArrowUp") {
85
- e.preventDefault();
86
- moveHighlight(-1);
87
- } else if (e.key === "Enter") {
88
- e.preventDefault();
89
- void activateHighlighted();
90
- }
91
- }
92
-
93
- watch(open, async (isOpen) => {
94
- if (!isOpen) {
95
- highlightedId.value = "";
96
- return;
97
- }
98
- await nextTick();
99
- syncHighlight();
100
- inputRef.value?.focus();
101
- });
102
-
103
- watch([query, groupedItems], async () => {
104
- syncHighlight();
105
- await nextTick();
106
- if (!highlightedId.value || !import.meta.client) return;
107
- document
108
- .querySelector<HTMLElement>(`[data-item="${highlightedId.value}"]`)
109
- ?.scrollIntoView({ block: "nearest" });
110
- });
111
-
112
- watch(
113
- () => route.fullPath,
114
- () => {
115
- highlightedId.value = "";
116
- },
117
- );
118
-
119
- onMounted(async () => {
120
- await nextTick();
121
- syncHighlight();
122
- inputRef.value?.focus();
123
- });
124
- </script>
125
-
126
- <template>
127
- <DialogRoot v-model:open="open">
128
- <DialogPortal>
129
- <DialogOverlay
130
- class="fixed inset-0 z-50 bg-overlay backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
131
- />
132
- <DialogContent
133
- class="fixed top-[8vh] left-1/2 z-50 flex max-h-[80vh] w-[min(94vw,44rem)] -translate-x-1/2 flex-col overflow-hidden rounded-xl border border-border bg-background shadow-lg outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:top-[10vh]"
134
- @keydown="handleListNavigation"
135
- >
136
- <VisuallyHidden>
137
- <DialogTitle>{{ t("command.title") }}</DialogTitle>
138
- <DialogDescription>{{ t("command.description") }}</DialogDescription>
139
- </VisuallyHidden>
140
-
141
- <!-- Search input -->
142
- <div class="flex h-14 shrink-0 items-center gap-3 border-b border-border px-3 sm:px-4">
143
- <Icon
144
- name="lucide:search"
145
- class="size-4 shrink-0 text-muted-foreground"
146
- aria-hidden="true"
147
- />
148
- <input
149
- ref="input"
150
- v-model="query"
151
- type="text"
152
- autocomplete="off"
153
- spellcheck="false"
154
- :placeholder="t('command.placeholder')"
155
- class="h-full min-w-0 flex-1 bg-transparent text-[16px] text-foreground outline-none placeholder:text-muted-foreground sm:text-sm"
156
- />
157
- <kbd
158
- v-if="!query"
159
- class="hidden items-center gap-1 rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground sm:flex"
160
- >
161
- Esc
162
- </kbd>
163
- <button
164
- v-else
165
- type="button"
166
- :aria-label="t('command.clear')"
167
- class="flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground transition-colors hover:text-foreground"
168
- @click="clearQuery"
169
- >
170
- <Icon name="lucide:x" class="size-3" aria-hidden="true" />
171
- </button>
172
- </div>
173
-
174
- <!-- Results -->
175
- <div
176
- class="min-h-0 flex-1 overflow-y-auto overscroll-contain py-2 [scrollbar-color:var(--border)_transparent]"
177
- >
178
- <div
179
- v-if="isSearching && searchPending"
180
- class="flex items-center justify-center gap-2 px-6 py-12 text-sm text-muted-foreground"
181
- role="status"
182
- >
183
- <Icon name="lucide:loader-circle" class="size-4 animate-spin" aria-hidden="true" />
184
- {{ t("command.loading") }}
185
- </div>
186
-
187
- <div
188
- v-else-if="isSearching && searchError"
189
- class="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center"
190
- role="alert"
191
- >
192
- <Icon name="lucide:circle-alert" class="size-5 text-destructive" aria-hidden="true" />
193
- <p class="max-w-sm text-sm font-medium text-foreground">{{ t("command.error") }}</p>
194
- </div>
195
-
196
- <!-- Groups -->
197
- <template v-else-if="visibleItems.length">
198
- <div v-for="group in groupedItems" :key="group.id" class="mb-1 px-2 last:mb-0">
199
- <div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
200
- <span class="text-[11px] font-medium text-muted-foreground">{{ group.title }}</span>
201
- <div class="flex-1 border-t border-border/60" />
202
- <span
203
- v-if="group.id === 'search'"
204
- class="text-[11px] tabular-nums text-muted-foreground"
205
- aria-live="polite"
206
- >
207
- {{ t("command.resultCount", { count: group.items.length }) }}
208
- </span>
209
- </div>
210
-
211
- <button
212
- v-for="item in group.items"
213
- :key="item.id"
214
- type="button"
215
- :data-item="item.id"
216
- :aria-label="itemLabel(item)"
217
- class="group flex w-full items-start gap-3 rounded-lg px-2.5 py-2 text-left transition-colors outline-none"
218
- :class="
219
- highlightedId === item.id ? 'bg-accent text-accent-foreground' : 'text-foreground'
220
- "
221
- @mousemove="highlightedId = item.id"
222
- @focus="highlightedId = item.id"
223
- @click="void selectItem(item)"
224
- >
225
- <!-- Icon -->
226
- <div
227
- class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 transition-colors"
228
- :class="highlightedId === item.id ? 'border-border/80 bg-background' : ''"
229
- >
230
- <Icon
231
- v-if="group.id === 'recent'"
232
- name="lucide:clock"
233
- class="size-3.5 text-muted-foreground"
234
- aria-hidden="true"
235
- />
236
- <Icon
237
- v-else
238
- :name="item.icon ?? 'lucide:file-text'"
239
- class="size-3.5 text-muted-foreground"
240
- aria-hidden="true"
241
- />
242
- </div>
243
-
244
- <!-- Text -->
245
- <div class="min-w-0 flex-1">
246
- <div class="flex min-w-0 items-center gap-2">
247
- <span class="min-w-0 truncate text-sm leading-5 font-medium">
248
- <template
249
- v-for="(token, tokenIndex) in highlightTokens(item.title, query)"
250
- :key="tokenIndex"
251
- >
252
- <mark
253
- v-if="token.match"
254
- class="rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground not-italic"
255
- >{{ token.text }}</mark
256
- ><template v-else>{{ token.text }}</template>
257
- </template>
258
- </span>
259
- <span
260
- v-if="item.badge && showResultBadges"
261
- class="shrink-0 rounded-full bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground"
262
- >
263
- {{ item.badge }}
264
- </span>
265
- </div>
266
- <p
267
- v-if="item.subtitle"
268
- class="mt-0.5 flex min-w-0 items-start gap-1 text-xs leading-5 text-muted-foreground"
269
- >
270
- <Icon
271
- name="lucide:hash"
272
- class="mt-1 size-3 shrink-0 opacity-50"
273
- aria-hidden="true"
274
- />
275
- <span class="line-clamp-2 min-w-0">
276
- <template
277
- v-for="(token, tokenIndex) in highlightTokens(item.subtitle, query)"
278
- :key="tokenIndex"
279
- >
280
- <mark
281
- v-if="token.match"
282
- class="rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground not-italic"
283
- >{{ token.text }}</mark
284
- ><template v-else>{{ token.text }}</template>
285
- </template>
286
- </span>
287
- </p>
288
- </div>
289
-
290
- <!-- Right hint -->
291
- <div class="mt-1 flex shrink-0 items-center gap-1.5 text-muted-foreground">
292
- <Icon
293
- v-if="item.external"
294
- name="lucide:arrow-up-right"
295
- class="size-3.5 opacity-0 transition-opacity"
296
- :class="highlightedId === item.id ? 'opacity-100' : 'group-hover:opacity-60'"
297
- aria-hidden="true"
298
- />
299
- <kbd
300
- v-else
301
- class="hidden items-center rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] transition-opacity"
302
- :class="highlightedId === item.id ? 'flex opacity-100' : 'opacity-0'"
303
- >
304
- ↵
305
- </kbd>
306
- </div>
307
- </button>
308
- </div>
309
- </template>
310
-
311
- <!-- Empty state -->
312
- <div
313
- v-else-if="!isSearching || searchIsEmpty"
314
- class="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center"
315
- >
316
- <div
317
- class="flex size-10 items-center justify-center rounded-lg border border-border bg-muted/50"
318
- >
319
- <Icon name="lucide:search" class="size-4 text-muted-foreground" aria-hidden="true" />
320
- </div>
321
- <p class="text-sm font-medium text-foreground">
322
- {{ t("command.noResults", { query }) }}
323
- </p>
324
- <p class="text-xs text-muted-foreground">
325
- {{ t("command.noResultsHelp") }}
326
- </p>
327
- </div>
328
- </div>
329
-
330
- <!-- Footer -->
331
- <div
332
- class="flex shrink-0 items-center justify-between border-t border-border px-3 py-2.5 text-[11px] text-muted-foreground sm:px-4"
333
- >
334
- <div class="flex min-w-0 items-center gap-2 sm:gap-3">
335
- <span class="flex items-center gap-1">
336
- <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px]"
337
- >↑↓</kbd
338
- >
339
- {{ t("command.navigate") }}
340
- </span>
341
- <span class="hidden items-center gap-1 sm:flex">
342
- <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px]"
343
- >↵</kbd
344
- >
345
- {{ t("command.open") }}
346
- </span>
347
- <span class="hidden items-center gap-1 sm:flex">
348
- <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px]"
349
- >Esc</kbd
350
- >
351
- {{ t("command.close") }}
352
- </span>
353
- </div>
354
- <span class="shrink-0 sm:hidden">{{ t("command.closeHint") }}</span>
355
- </div>
356
- </DialogContent>
357
- </DialogPortal>
358
- </DialogRoot>
359
- </template>
@@ -1,126 +0,0 @@
1
- <script setup lang="ts">
2
- import { nextTick, onBeforeUnmount, watch } from "vue";
3
- import { useRoute } from "#imports";
4
- import { getSearchHighlightTerms } from "#ginko-docs/features/search/command-center";
5
-
6
- const STORAGE_KEY = "site-command-center-page-highlight";
7
- const HIGHLIGHT_SELECTOR = "mark[data-search-page-highlight]";
8
- const SKIP_SELECTOR = "script, style, textarea, input, select, button, mark";
9
-
10
- const route = useRoute();
11
-
12
- type StoredHighlight = {
13
- href: string;
14
- query: string;
15
- };
16
-
17
- function readStoredHighlight(): StoredHighlight | null {
18
- try {
19
- const value = sessionStorage.getItem(STORAGE_KEY);
20
- if (!value) return null;
21
- const parsed = JSON.parse(value) as Partial<StoredHighlight>;
22
- if (typeof parsed.href !== "string" || typeof parsed.query !== "string") return null;
23
- return { href: parsed.href, query: parsed.query };
24
- } catch {
25
- return null;
26
- }
27
- }
28
-
29
- function removeHighlights() {
30
- for (const mark of document.querySelectorAll(HIGHLIGHT_SELECTOR)) {
31
- const parent = mark.parentNode;
32
- if (!parent) continue;
33
- mark.replaceWith(document.createTextNode(mark.textContent ?? ""));
34
- parent.normalize();
35
- }
36
- }
37
-
38
- function highlightPattern(root: Element, pattern: RegExp) {
39
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
40
- acceptNode(node) {
41
- const parent = node.parentElement;
42
- if (!parent || parent.closest(SKIP_SELECTOR)) return NodeFilter.FILTER_REJECT;
43
- if (!node.textContent?.trim() || !pattern.test(node.textContent)) {
44
- pattern.lastIndex = 0;
45
- return NodeFilter.FILTER_REJECT;
46
- }
47
- pattern.lastIndex = 0;
48
- return NodeFilter.FILTER_ACCEPT;
49
- },
50
- });
51
-
52
- const nodes: Text[] = [];
53
- while (walker.nextNode()) nodes.push(walker.currentNode as Text);
54
-
55
- for (const node of nodes) {
56
- const text = node.textContent ?? "";
57
- const fragment = document.createDocumentFragment();
58
- let lastIndex = 0;
59
-
60
- for (const match of text.matchAll(pattern)) {
61
- const index = match.index ?? 0;
62
- const value = match[0];
63
- if (!value) continue;
64
-
65
- fragment.append(document.createTextNode(text.slice(lastIndex, index)));
66
-
67
- const mark = document.createElement("mark");
68
- mark.dataset.searchPageHighlight = "";
69
- mark.className =
70
- "rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground";
71
- mark.textContent = value;
72
- fragment.append(mark);
73
-
74
- lastIndex = index + value.length;
75
- }
76
-
77
- fragment.append(document.createTextNode(text.slice(lastIndex)));
78
- node.replaceWith(fragment);
79
- }
80
- }
81
-
82
- function escapePattern(value: string) {
83
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
84
- }
85
-
86
- function buildPatterns(query: string) {
87
- const phrase = query.trim().replace(/\s+/g, " ");
88
- const tokens = getSearchHighlightTerms(phrase);
89
- const phrasePattern = phrase.split(/\s+/).map(escapePattern).join("\\s+");
90
-
91
- return [
92
- new RegExp(phrasePattern, "gi"),
93
- ...(tokens.length > 1 ? [new RegExp(tokens.map(escapePattern).join("|"), "gi")] : []),
94
- ];
95
- }
96
-
97
- async function applyStoredHighlight() {
98
- removeHighlights();
99
-
100
- const stored = readStoredHighlight();
101
- if (!stored) return;
102
-
103
- const currentPath = `${route.path}${route.hash}`;
104
- if (stored.href !== route.fullPath && stored.href !== currentPath && stored.href !== route.path) {
105
- sessionStorage.removeItem(STORAGE_KEY);
106
- return;
107
- }
108
-
109
- await nextTick();
110
-
111
- const root = document.querySelector("#main-content") ?? document.querySelector("main");
112
- if (!root) return;
113
-
114
- for (const pattern of buildPatterns(stored.query)) {
115
- highlightPattern(root, pattern);
116
- if (root.querySelector(HIGHLIGHT_SELECTOR)) break;
117
- }
118
- }
119
-
120
- watch(() => route.fullPath, applyStoredHighlight, { immediate: true });
121
- onBeforeUnmount(removeHighlights);
122
- </script>
123
-
124
- <template>
125
- <span hidden aria-hidden="true" />
126
- </template>