44import type { SearchResult } from ' minisearch'
55import type { GenericComponentInstance } from ' reka-ui'
66import type { Ref } from ' vue'
7+ import type { NimiqVitepressThemeConfig } from ' ../types'
78import { computedAsync , debouncedWatch } from ' @vueuse/core'
89import Mark from ' mark.js/src/vanilla.js'
910import MiniSearch from ' minisearch'
1011import { DialogClose , ListboxContent , ListboxFilter , ListboxItem , ListboxRoot } from ' reka-ui'
11- import { useData } from ' vitepress'
12- import { markRaw , nextTick , onMounted , ref , shallowRef , watch } from ' vue'
12+ import { useData , withBase } from ' vitepress'
13+ import { computed , markRaw , nextTick , onMounted , ref , shallowRef , watch } from ' vue'
14+ import { useSourceCode } from ' ../composables/useSourceCode'
1315import { LRUCache } from ' ../lib/lru'
1416
1517const emit = defineEmits <{ close: [] }>()
1618
17- const { localeIndex } = useData ()
19+ const { localeIndex, theme, isDark } = useData <NimiqVitepressThemeConfig >()
20+
21+ const {
22+ copyMarkdownContent,
23+ copyMarkdownLink,
24+ chatGPTUrl,
25+ claudeUrl,
26+ sourceCodeUrl,
27+ copyOptionsConfig,
28+ showCopyMarkdown,
29+ } = useSourceCode ()
1830
1931const filterText = ref (' ' )
2032const enableNoResults = ref (false )
@@ -61,6 +73,80 @@ const searchIndex = computedAsync(async () =>
6173
6274const cache = new LRUCache (16 ) // 16 files
6375
76+ // Computed lists for better maintainability
77+ const moduleItems = computed (() =>
78+ theme .value .modules ?.filter (m => ! m .hidden ) || [],
79+ )
80+
81+ const socialItems = computed (() =>
82+ theme .value .links || [],
83+ )
84+
85+ const copyItems = computed (() => {
86+ if (! showCopyMarkdown .value )
87+ return []
88+
89+ const items = []
90+
91+ // Copy page content (always available if showCopyMarkdown is true)
92+ items .push ({
93+ value: ' copy-page' ,
94+ icon: ' i-nimiq:copy' ,
95+ label: ' Copy page content' ,
96+ action: handleCopyPage ,
97+ })
98+
99+ // Copy markdown link
100+ if (copyOptionsConfig .value .markdownLink ) {
101+ items .push ({
102+ value: ' copy-markdown-link' ,
103+ icon: ' i-nimiq:link' ,
104+ label: ' Copy markdown link' ,
105+ action: handleCopyMarkdownLink ,
106+ })
107+ }
108+
109+ // View as markdown
110+ if (copyOptionsConfig .value .viewMarkdown ) {
111+ items .push ({
112+ value: ' view-markdown' ,
113+ icon: ' i-nimiq:logos-github-mono' ,
114+ label: ' View as markdown' ,
115+ action: handleViewAsMarkdown ,
116+ })
117+ }
118+
119+ // AI tools
120+ if (copyOptionsConfig .value .claude ) {
121+ items .push ({
122+ value: ' open-claude' ,
123+ icon: ' i-simple-icons:claude' ,
124+ label: ' Open in Claude' ,
125+ action: handleOpenInClaude ,
126+ })
127+ }
128+
129+ if (copyOptionsConfig .value .chatgpt ) {
130+ items .push ({
131+ value: ' open-chatgpt' ,
132+ icon: ' i-simple-icons:openai' ,
133+ label: ' Open in ChatGPT' ,
134+ action: handleOpenInChatGPT ,
135+ })
136+ }
137+
138+ return items
139+ })
140+
141+ const utilityItems = computed (() => [
142+ {
143+ value: ' theme-switcher' ,
144+ icon: ' i-nimiq:moon' ,
145+ label: ` Switch to ${isDark .value ? ' light' : ' dark' } theme ` ,
146+ action: toggleTheme ,
147+ },
148+ ])
149+
64150debouncedWatch (
65151 () => [searchIndex .value , filterText .value ] as const ,
66152 async ([index , filterTextValue ], old , onCleanup ) => {
@@ -128,12 +214,52 @@ function formMarkRegex(terms: Set<string>) {
128214 ' gi' ,
129215 )
130216}
217+
218+ function toggleTheme() {
219+ isDark .value = ! isDark .value
220+ emit (' close' )
221+ }
222+
223+ function handleSocialLink(link : string ) {
224+ window .open (withBase (link ), ' _blank' , ' noopener,noreferrer' )
225+ emit (' close' )
226+ }
227+
228+ function handleModuleLink(link : string ) {
229+ window .location .href = withBase (link )
230+ emit (' close' )
231+ }
232+
233+ async function handleCopyPage() {
234+ await copyMarkdownContent ()
235+ emit (' close' )
236+ }
237+
238+ async function handleCopyMarkdownLink() {
239+ await copyMarkdownLink ()
240+ emit (' close' )
241+ }
242+
243+ function handleViewAsMarkdown() {
244+ window .open (sourceCodeUrl .value , ' _blank' , ' noopener,noreferrer' )
245+ emit (' close' )
246+ }
247+
248+ function handleOpenInChatGPT() {
249+ window .open (chatGPTUrl .value , ' _blank' , ' noopener,noreferrer' )
250+ emit (' close' )
251+ }
252+
253+ function handleOpenInClaude() {
254+ window .open (claudeUrl .value , ' _blank' , ' noopener,noreferrer' )
255+ emit (' close' )
256+ }
131257 </script >
132258
133259<template >
134260 <ListboxRoot ref =" listboxRef" >
135261 <div w-full flex =" ~ items-center" relative f-p-2xs >
136- <ListboxFilter v-model =" filterText" rounded-3 nq-input-box of-hidden w-full bg-transparent flex-1 placeholder =" Search documentation" auto-focus border =" none" outline =" 8 ~ neutral-500 hocus:blue" transition-outline-color />
262+ <ListboxFilter v-model =" filterText" rounded-3 nq-input-box of-hidden w-full bg-transparent flex-1 placeholder =" Search documentation" auto-focus border =" none" outline =" 1.5 ~ neutral-500 hocus:blue" transition-outline-color />
137263 <DialogClose absolute right-4 stack size-48 =" !" cursor-pointer >
138264 <div i-nimiq:cross text =" 10 group-focus-within:blue neutral-700" right-16 mx-auto />
139265 </DialogClose >
@@ -143,19 +269,92 @@ function formMarkRegex(terms: Set<string>) {
143269 :ref =" (node) => { if (node && '$el' in node){ resultsEl = node.$el } }"
144270 as =" ul" md:max-h-55vh of-auto empty =" hidden md:block"
145271 >
272+ <!-- Manual Options (only show when no filter text) -->
273+ <template v-if =" ! filterText " >
274+ <!-- 1. Modules (Most Important - Navigation) -->
275+ <ListboxItem
276+ v-for =" module in moduleItems" :key =" module.subpath"
277+ :value =" `module-${module.subpath}`"
278+ class =" data-[highlighted]:bg-blue-400 data-[highlighted]:text-blue data-[highlighted]:font-semibold"
279+ as-child @select =" handleModuleLink(module.defaultPageLink)"
280+ >
281+ <div inline-flex f-p-sm f-p-xs w-full group cursor-pointer >
282+ <div flex =" ~ items-center gap-12" >
283+ <div :class =" module.icon || 'i-nimiq:document'" size-24 text =" neutral-700 group-hocus:blue" />
284+ <div flex =" ~ col" >
285+ <span >{{ module.text }}</span >
286+ <span v-if =" module.description" text =" f-xs neutral-800" >{{ module.description }}</span >
287+ </div >
288+ </div >
289+ </div >
290+ </ListboxItem >
291+
292+ <!-- 2. Utility Actions (Theme, etc.) -->
293+ <ListboxItem
294+ v-for =" item in utilityItems" :key =" item.value"
295+ :value =" item.value"
296+ class =" data-[highlighted]:bg-blue-400 data-[highlighted]:text-blue data-[highlighted]:font-semibold"
297+ as-child @select =" item.action"
298+ >
299+ <div inline-flex f-p-sm f-p-xs w-full group cursor-pointer >
300+ <div flex =" ~ items-center gap-12" >
301+ <div :class =" item.icon" size-16 text =" neutral-700 group-hocus:blue" />
302+ <span >{{ item.label }}</span >
303+ </div >
304+ </div >
305+ </ListboxItem >
306+
307+ <!-- 3. Copy & Page Actions -->
308+ <ListboxItem
309+ v-for =" item in copyItems" :key =" item.value"
310+ :value =" item.value"
311+ class =" data-[highlighted]:bg-blue-400 data-[highlighted]:text-blue data-[highlighted]:font-semibold"
312+ as-child @select =" item.action"
313+ >
314+ <div inline-flex f-p-sm f-p-xs w-full group cursor-pointer >
315+ <div flex =" ~ items-center gap-12" >
316+ <div :class =" item.icon" size-16 text =" neutral-700 group-hocus:blue" />
317+ <span >{{ item.label }}</span >
318+ </div >
319+ </div >
320+ </ListboxItem >
321+
322+ <!-- 4. Social Media Links -->
323+ <ListboxItem
324+ v-for =" social in socialItems" :key =" social.link"
325+ :value =" `social-${social.link}`"
326+ class =" data-[highlighted]:bg-blue-400 data-[highlighted]:text-blue data-[highlighted]:font-semibold"
327+ as-child @select =" handleSocialLink(social.link)"
328+ >
329+ <div inline-flex f-p-sm f-p-xs w-full group cursor-pointer >
330+ <div flex =" ~ items-center gap-12" >
331+ <div :class =" social.icon" size-16 text =" neutral-700 group-hocus:blue" />
332+ <span >{{ social.label }}</span >
333+ </div >
334+ </div >
335+ </ListboxItem >
336+
337+ <!-- Separator -->
338+ <li v-if =" results.length" h-1 bg-neutral-200 my-8 />
339+ </template >
340+
341+ <!-- Search Results -->
146342 <ListboxItem
147343 v-for =" p in results" :key =" p.id" :value =" p.id"
148344 class =" data-[highlighted]:bg-blue-400 data-[highlighted]:text-blue data-[highlighted]:font-semibold" as-child @select =" emit('close')"
149345 >
150346 <a :href =" p.id" inline-flex f-p-sm f-p-xs w-full group >
151- <div flex =" ~ items-center wrap" >
152- <span v-for =" (t, index) in p.titles" :key =" index" flex =" ~ items-center" font-normal >
153- <span v-html =" t" />
154- <div i-nimiq:chevron-right mx-8 text =" 8 neutral-700 group-hocus:blue" />
155- </span >
156- <span font-normal >
157- <span v-html =" p.title" />
158- </span >
347+ <div flex =" ~ items-center gap-12" >
348+ <div i-nimiq:document size-16 text =" neutral-700 group-hocus:blue" />
349+ <div flex =" ~ items-center wrap" >
350+ <span v-for =" (t, index) in p.titles" :key =" index" flex =" ~ items-center" font-normal >
351+ <span v-html =" t" />
352+ <div i-nimiq:chevron-right mx-8 text =" 8 neutral-700 group-hocus:blue" />
353+ </span >
354+ <span font-normal >
355+ <span v-html =" p.title" />
356+ </span >
357+ </div >
159358 </div >
160359 </a >
161360 </ListboxItem >
@@ -164,7 +363,7 @@ function formMarkRegex(terms: Set<string>) {
164363 No results for "<strong >{{ filterText }}</strong >"
165364 </li >
166365
167- <li v-else-if =" !filterText && !results.length" italic text =" f-xs neutral-700 center" f-py-md >
366+ <li v-else-if =" !filterText && !results.length && !moduleItems.length && !utilityItems.length && !copyItems.length && !socialItems.length " italic text =" f-xs neutral-700 center" f-py-md >
168367 Start typing to search
169368 </li >
170369 </ListboxContent >
0 commit comments