Skip to content

Commit f983c39

Browse files
authored
Merge branch 'main' into feat/changelog-1
2 parents 82c6a38 + d3f60cb commit f983c39

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+690
-1144
lines changed

app/assets/main.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@
153153
--fg: oklch(1 0 0);
154154
--fg-muted: oklch(0.769 0 0);
155155
--fg-subtle: oklch(0.693 0 0);
156-
157156
/* border, separator colors */
158157
--border: oklch(0.769 0 0);
159158
--border-subtle: oklch(0.739 0 0);
@@ -275,7 +274,8 @@ dd {
275274
}
276275

277276
/* Shiki theme colors */
278-
html.light .shiki {
277+
/* html.light .shiki { */
278+
html .shiki {
279279
color: var(--shiki-light) !important;
280280
background-color: var(--shiki-light-bg) !important;
281281

app/components/Chart/PatternSlot.vue

Lines changed: 0 additions & 32 deletions
This file was deleted.

app/components/Chart/SplitSparkline.vue

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
<script setup lang="ts">
22
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
3+
import { VueUiPatternSeed } from 'vue-data-ui/vue-ui-pattern-seed'
34
import { useCssVariables } from '~/composables/useColors'
45
import {
56
type VueUiSparklineConfig,
67
type VueUiSparklineDatasetItem,
78
type VueUiXyDatasetItem,
89
} from 'vue-data-ui'
910
import { getPalette, lightenColor } from 'vue-data-ui/utils'
11+
import { CHART_PATTERN_CONFIG } from '~/utils/charts'
1012
1113
import('vue-data-ui/style.css')
1214
@@ -194,19 +196,37 @@ const configs = computed(() => {
194196
<ClientOnly v-for="(config, i) in configs" :key="`config_${i}`">
195197
<div @mouseleave="resetHover" @keydown.esc="resetHover" class="w-full max-w-[400px] mx-auto">
196198
<div class="flex gap-2 place-items-center">
197-
<div class="h-3 w-3">
198-
<svg viewBox="0 0 2 2" class="w-full">
199+
<div class="h-5 w-5">
200+
<svg viewBox="0 0 30 30" class="w-full">
201+
<defs>
202+
<VueUiPatternSeed
203+
v-if="i != 0"
204+
:id="`marker_${i}`"
205+
:seed="i"
206+
:foreground-color="colors.bg!"
207+
:background-color="
208+
dataset?.[i]?.color ??
209+
palette[i] ??
210+
palette[i % palette.length] ??
211+
palette[0] ??
212+
'transparent'
213+
"
214+
:max-size="CHART_PATTERN_CONFIG.maxSize"
215+
:min-size="CHART_PATTERN_CONFIG.minSize"
216+
:disambiguator="CHART_PATTERN_CONFIG.disambiguator"
217+
/>
218+
</defs>
199219
<rect
200220
x="0"
201221
y="0"
202-
width="2"
203-
height="2"
204-
rx="0.3"
205-
:fill="dataset?.[i]?.color ?? palette[i]"
222+
width="30"
223+
height="30"
224+
rx="3"
225+
:fill="i === 0 ? (dataset?.[0]?.color ?? palette[0]) : `url(#marker_${i})`"
206226
/>
207227
</svg>
208228
</div>
209-
{{ applyEllipsis(dataset?.[i]?.name ?? '', 28) }}
229+
{{ applyEllipsis(dataset?.[i]?.name ?? '', 27) }}
210230
</div>
211231
<VueUiSparkline
212232
:key="`${i}_${step}`"

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>

0 commit comments

Comments
 (0)