Skip to content

Commit 95c2aae

Browse files
alex-keyautofix-ci[bot]alexdln
authored
fix: package code page improvements (#2217)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alex Savelyev <91429106+alexdln@users.noreply.github.com> Co-authored-by: Vordgi <sasha2822222@gmail.com>
1 parent 88e4db0 commit 95c2aae

38 files changed

+616
-441
lines changed

app/components/Code/DirectoryListing.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const bytesFormatter = useBytesFormatter()
5858
<div class="directory-listing">
5959
<!-- Empty state -->
6060
<div v-if="currentContents.length === 0" class="py-20 text-center text-fg-muted">
61+
<span class="i-lucide:folder-open w-12 h-12 text-fg-subtle mx-auto mb-4"> </span>
6162
<p>{{ $t('code.no_files') }}</p>
6263
</div>
6364

app/components/Code/Header.vue

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<script setup lang="ts">
2+
import type { PackageFileContentResponse } from '#shared/types/npm-registry'
3+
4+
interface BreadcrumbItem {
5+
name: string
6+
path: string
7+
}
8+
9+
const props = defineProps<{
10+
filePath?: string | null
11+
loading: boolean
12+
isViewingFile: boolean
13+
isBinaryFile: boolean
14+
fileContent: PackageFileContentResponse | null | undefined
15+
markdownViewMode: 'preview' | 'code'
16+
selectedLines: { start: number; end: number } | null
17+
getCodeUrlWithPath: (path?: string) => string
18+
packageName: string
19+
version: string
20+
}>()
21+
22+
const emit = defineEmits<{
23+
'update:markdownViewMode': [value: 'preview' | 'code']
24+
'mobile-tree-drawer-toggle': []
25+
}>()
26+
27+
const { toggleCodeContainer } = useCodeContainer()
28+
29+
const markdownViewModes = [
30+
{
31+
key: 'preview' as const,
32+
label: $t('code.markdown_view_mode.preview'),
33+
icon: 'i-lucide:eye',
34+
},
35+
{
36+
key: 'code' as const,
37+
label: $t('code.markdown_view_mode.code'),
38+
icon: 'i-lucide:code',
39+
},
40+
]
41+
42+
// Build breadcrumb path segments
43+
const breadcrumbs = computed<{
44+
items: BreadcrumbItem[]
45+
current: string
46+
}>(() => {
47+
const parts = props.filePath?.split('/').filter(Boolean) ?? []
48+
const result: {
49+
items: BreadcrumbItem[]
50+
current: string
51+
} = {
52+
items: [],
53+
current: parts.at(-1) ?? '',
54+
}
55+
56+
for (let i = 0; i < parts.length - 1; i++) {
57+
const part = parts[i]
58+
if (part) {
59+
result.items.push({
60+
name: part,
61+
path: parts.slice(0, i + 1).join('/'),
62+
})
63+
}
64+
}
65+
66+
return result
67+
})
68+
69+
const { copied: fileContentCopied, copy: copyFileContent } = useClipboard({
70+
source: () => props.fileContent?.content || '',
71+
copiedDuring: 2000,
72+
})
73+
74+
// Copy link to current line(s)
75+
const { copied: permalinkCopied, copy: copyPermalink } = useClipboard({ copiedDuring: 2000 })
76+
77+
function copyPermalinkUrl() {
78+
const url = new URL(window.location.href)
79+
copyPermalink(url.toString())
80+
}
81+
82+
// Path dropdown (mobile breadcrumb collapse)
83+
const isPathDropdownOpen = shallowRef(false)
84+
const pathDropdownButtonRef = useTemplateRef('pathDropdownButtonRef')
85+
const pathDropdownListRef = useTemplateRef<HTMLElement>('pathDropdownListRef')
86+
87+
function togglePathDropdown(forceClose?: boolean) {
88+
if (forceClose) {
89+
isPathDropdownOpen.value = false
90+
return
91+
}
92+
93+
isPathDropdownOpen.value = !isPathDropdownOpen.value
94+
}
95+
96+
onClickOutside(pathDropdownListRef, () => togglePathDropdown(true), {
97+
ignore: [pathDropdownButtonRef],
98+
})
99+
100+
useEventListener('keydown', (event: KeyboardEvent) => {
101+
if (event.key === 'Escape' && isPathDropdownOpen.value) {
102+
togglePathDropdown(true)
103+
}
104+
})
105+
</script>
106+
107+
<template>
108+
<div
109+
class="sticky flex-split h-11 max-md:(h-20 top-32 flex-col items-start) z-5 top-25 gap-0 bg-bg-subtle border-b border-border px-2 py-1 text-nowrap max-w-full"
110+
>
111+
<div class="flex items-center w-full h-full relative">
112+
<!-- Breadcrumb navigation -->
113+
<nav
114+
:aria-label="$t('code.file_path')"
115+
class="flex items-center gap-0.5 font-mono text-sm overflow-x-auto"
116+
dir="ltr"
117+
>
118+
<NuxtLink
119+
v-if="filePath"
120+
:to="getCodeUrlWithPath()"
121+
class="text-fg-muted hover:text-fg transition-colors shrink-0"
122+
>
123+
~
124+
</NuxtLink>
125+
<span class="max-md:hidden">
126+
<template v-for="crumb in breadcrumbs.items" :key="crumb.path">
127+
<span class="text-fg-subtle">/</span>
128+
<NuxtLink
129+
:to="getCodeUrlWithPath(crumb.path)"
130+
class="text-fg-muted hover:text-fg transition-colors"
131+
>
132+
{{ crumb.name }}
133+
</NuxtLink>
134+
</template>
135+
</span>
136+
<!-- Show dropdown with path elements on small screens -->
137+
<span v-if="breadcrumbs.items.length" class="md:hidden">
138+
<span class="text-fg-subtle">/</span>
139+
<span ref="pathDropdownButtonRef">
140+
<ButtonBase
141+
size="sm"
142+
class="px-2 py-1 mx-1"
143+
:aria-label="$t('code.open_path_dropdown')"
144+
:aria-expanded="isPathDropdownOpen"
145+
aria-haspopup="true"
146+
@click="togglePathDropdown()"
147+
>
148+
...
149+
</ButtonBase>
150+
</span>
151+
</span>
152+
<template v-if="breadcrumbs.current">
153+
<span class="text-fg-subtle">/</span>
154+
<span class="text-fg">{{ breadcrumbs.current }}</span>
155+
</template>
156+
</nav>
157+
<Transition
158+
enter-active-class="transition-all duration-150"
159+
leave-active-class="transition-all duration-100"
160+
enter-from-class="opacity-0 translate-y-1"
161+
leave-to-class="opacity-0 translate-y-1"
162+
>
163+
<div
164+
v-if="isPathDropdownOpen"
165+
ref="pathDropdownListRef"
166+
class="absolute top-8 z-50 bg-bg-subtle border border-border rounded-lg shadow-lg py-1 min-w-65 max-w-full font-mono text-sm"
167+
>
168+
<NuxtLink
169+
v-for="(crumb, index) in breadcrumbs.items"
170+
:key="crumb.path"
171+
:to="getCodeUrlWithPath(crumb.path)"
172+
class="flex items-start px-3 py-1 text-fg-muted hover:text-fg hover:bg-bg-muted transition-colors"
173+
@click="togglePathDropdown(false)"
174+
>
175+
<span
176+
v-for="level in index"
177+
:key="level"
178+
aria-hidden="true"
179+
class="relative h-5 w-4 shrink-0"
180+
>
181+
<!-- add └ mark to better visualize nested folders) -->
182+
<template v-if="level === index">
183+
<span class="absolute top-0 bottom-1/2 inset-is-2 w-px bg-fg-subtle/50" />
184+
<span class="absolute top-1/2 inset-is-2 inset-ie-0 h-px bg-fg-subtle/50" />
185+
</template>
186+
</span>
187+
<span :class="{ 'ps-1': index > 0 }" class="min-w-0 break-all"
188+
>{{ crumb.name }}<span class="text-fg-subtle">/</span></span
189+
>
190+
</NuxtLink>
191+
</div>
192+
</Transition>
193+
</div>
194+
<div class="flex max-md:(w-full justify-between border-border border-t pt-1)">
195+
<!-- Toggle button (mobile only) -->
196+
<ButtonBase
197+
class="md:hidden px-2"
198+
:aria-label="$t('code.toggle_tree')"
199+
@click="emit('mobile-tree-drawer-toggle')"
200+
classicon="i-lucide:folder-code"
201+
/>
202+
<div class="flex items-center gap-2">
203+
<template v-if="isViewingFile && !isBinaryFile && fileContent">
204+
<div
205+
v-if="fileContent?.markdownHtml"
206+
class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md overflow-x-auto"
207+
role="tablist"
208+
aria-label="Markdown view mode selector"
209+
>
210+
<button
211+
v-for="mode in markdownViewModes"
212+
:key="mode.key"
213+
role="tab"
214+
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 inline-flex items-center gap-1.5"
215+
:class="
216+
markdownViewMode === mode.key
217+
? 'bg-bg-muted shadow text-fg'
218+
: 'text-fg-subtle hover:text-fg'
219+
"
220+
:aria-selected="markdownViewMode === mode.key"
221+
@click="emit('update:markdownViewMode', mode.key)"
222+
>
223+
{{ mode.label }}
224+
</button>
225+
</div>
226+
<TooltipApp :text="$t('code.copy_link')" position="top">
227+
<ButtonBase
228+
v-if="selectedLines"
229+
class="py-1 px-3"
230+
:classicon="permalinkCopied ? 'i-lucide:check' : 'i-lucide:file-braces-corner'"
231+
:aria-label="$t('code.copy_link')"
232+
@click="copyPermalinkUrl"
233+
/>
234+
</TooltipApp>
235+
<TooltipApp :text="$t('code.copy_content')" position="top">
236+
<ButtonBase
237+
v-if="!!fileContent?.content"
238+
class="px-3"
239+
:classicon="fileContentCopied ? 'i-lucide:check' : 'i-lucide:copy'"
240+
:aria-label="$t('code.copy_content')"
241+
@click="copyFileContent()"
242+
/>
243+
</TooltipApp>
244+
<TooltipApp :text="$t('code.open_raw_file')" position="top">
245+
<LinkBase
246+
variant="button-secondary"
247+
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
248+
class="px-3"
249+
:aria-label="$t('code.open_raw_file')"
250+
/>
251+
</TooltipApp>
252+
</template>
253+
<TooltipApp :text="$t('code.toggle_container')" position="top">
254+
<ButtonBase
255+
class="px-3 max-xl:hidden"
256+
:disabled="loading"
257+
classicon="i-lucide:unfold-horizontal [.container-full>&]:i-lucide:fold-horizontal"
258+
:aria-label="$t('code.toggle_container')"
259+
@click="toggleCodeContainer()"
260+
/>
261+
</TooltipApp>
262+
</div>
263+
</div>
264+
</div>
265+
</template>

app/components/Code/MobileTreeDrawer.vue

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@ watch(
2222
const isLocked = useScrollLock(document)
2323
// Prevent body scroll when drawer is open
2424
watch(isOpen, open => (isLocked.value = open))
25+
26+
function toggle() {
27+
isOpen.value = !isOpen.value
28+
}
29+
30+
defineExpose({
31+
toggle,
32+
})
2533
</script>
2634

2735
<template>
28-
<!-- Toggle button (mobile only) -->
29-
<ButtonBase
30-
variant="primary"
31-
class="md:hidden fixed bottom-9 inset-ie-4 z-45"
32-
:aria-label="$t('code.toggle_tree')"
33-
@click="isOpen = !isOpen"
34-
:classicon="isOpen ? 'i-lucide:x' : 'i-lucide:folder'"
35-
/>
36-
3736
<!-- Backdrop -->
3837
<Transition
3938
enter-active-class="transition-opacity duration-200"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div class="flex min-h-full" role="status" aria-busy="true" :aria-label="$t('common.loading')">
3+
<!-- Fake line numbers column -->
4+
<div class="shrink-0 bg-bg-subtle border-ie border-border w-14 py-0">
5+
<div v-for="n in 20" :key="n" class="px-3 h-6 flex items-center justify-end">
6+
<SkeletonInline class="w-4 h-3 rounded-sm" />
7+
</div>
8+
</div>
9+
<!-- Fake code content -->
10+
<div class="flex-1 p-4 space-y-1.5">
11+
<SkeletonBlock class="h-4 w-32 rounded-sm" />
12+
<SkeletonBlock class="h-4 w-48 rounded-sm" />
13+
<SkeletonBlock class="h-4 w-24 rounded-sm" />
14+
<div class="h-4" />
15+
<SkeletonBlock class="h-4 w-64 rounded-sm" />
16+
<SkeletonBlock class="h-4 w-56 rounded-sm" />
17+
<SkeletonBlock class="h-4 w-40 rounded-sm" />
18+
<SkeletonBlock class="h-4 w-72 rounded-sm" />
19+
<div class="h-4" />
20+
<SkeletonBlock class="h-4 w-36 rounded-sm" />
21+
<SkeletonBlock class="h-4 w-52 rounded-sm" />
22+
<SkeletonBlock class="h-4 w-44 rounded-sm" />
23+
<SkeletonBlock class="h-4 w-28 rounded-sm" />
24+
<div class="h-4" />
25+
<SkeletonBlock class="h-4 w-60 rounded-sm" />
26+
<SkeletonBlock class="h-4 w-48 rounded-sm" />
27+
<SkeletonBlock class="h-4 w-32 rounded-sm" />
28+
<SkeletonBlock class="h-4 w-56 rounded-sm" />
29+
<SkeletonBlock class="h-4 w-40 rounded-sm" />
30+
<SkeletonBlock class="h-4 w-24 rounded-sm" />
31+
</div>
32+
</div>
33+
</template>

app/components/Code/Viewer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ watch(
8686
</script>
8787

8888
<template>
89-
<div class="code-viewer flex min-h-full max-w-full">
89+
<div class="code-viewer flex min-h-full max-w-full overflow-x-auto">
9090
<!-- Line numbers column -->
9191
<div
9292
class="line-numbers shrink-0 bg-bg-subtle border-ie border-solid border-border text-end select-none relative"

app/composables/useSettings.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface AppSettings {
3838
/** Automatically open the web auth page in the browser */
3939
autoOpenURL: boolean
4040
}
41+
codeContainerFull: boolean
4142
sidebar: {
4243
collapsed: string[]
4344
}
@@ -63,6 +64,7 @@ const DEFAULT_SETTINGS: AppSettings = {
6364
connector: {
6465
autoOpenURL: false,
6566
},
67+
codeContainerFull: false,
6668
sidebar: {
6769
collapsed: [],
6870
},
@@ -236,3 +238,18 @@ export function useBackgroundTheme() {
236238
setBackgroundTheme,
237239
}
238240
}
241+
242+
export function useCodeContainer() {
243+
const { settings } = useSettings()
244+
245+
const codeContainerFull = computed(() => settings.value.codeContainerFull)
246+
247+
function toggleCodeContainer() {
248+
settings.value.codeContainerFull = !settings.value.codeContainerFull
249+
}
250+
251+
return {
252+
codeContainerFull,
253+
toggleCodeContainer,
254+
}
255+
}

0 commit comments

Comments
 (0)