Skip to content

Commit fd99453

Browse files
authored
feat: initial assets tab (#120)
1 parent 7f08540 commit fd99453

27 files changed

+662
-88
lines changed

packages/devtools-ui-kit/src/components/NCodeBlock.vue

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,47 @@
22
// This components requires to run in DevTools to render correctly
33
import { devToolsClient } from '../runtime/client'
44
5-
defineProps<{
6-
code: string
7-
lang?: string
8-
}>()
5+
withDefaults(
6+
defineProps<{
7+
code: string
8+
lang?: string
9+
lines?: boolean
10+
}>(), {
11+
lines: true,
12+
},
13+
)
914
</script>
1015

1116
<template>
1217
<template v-if="lang && devToolsClient?.devtools?.renderCodeHighlight">
13-
<pre class="n-code-block" v-html="devToolsClient.devtools.renderCodeHighlight(code, lang)" />
18+
<pre
19+
class="n-code-block"
20+
:class="lines ? 'n-code-block-lines' : ''"
21+
v-html="devToolsClient.devtools.renderCodeHighlight(code, lang)"
22+
/>
1423
</template>
1524
<template v-else>
16-
<pre class="n-code-block" v-text="code" />
25+
<pre
26+
class="n-code-block"
27+
:class="lines ? 'n-code-block-lines' : ''"
28+
v-text="code"
29+
/>
1730
</template>
1831
</template>
32+
33+
<style>
34+
.n-code-block-lines .shiki code {
35+
counter-reset: step;
36+
counter-increment: step calc(var(--start, 1) - 1);
37+
}
38+
.n-code-block-lines .shiki code .line::before {
39+
content: counter(step);
40+
counter-increment: step;
41+
width: 2rem;
42+
padding-right: 0.5rem;
43+
margin-right: 0.5rem;
44+
display: inline-block;
45+
text-align: right;
46+
--at-apply: text-truegray:50;
47+
}
48+
</style>

packages/devtools-ui-kit/src/unocss.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export const unocssPreset = (): Preset => ({
118118

119119
// icon
120120
'n-icon': 'flex-none',
121+
122+
// code
123+
'n-code-block': 'dark:bg-[#121212] bg-white',
121124
},
122125
})
123126

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<script setup lang="ts">
2+
import { useTimeAgo } from '@vueuse/core'
3+
import type { AssetInfo } from '~/../src/types'
4+
5+
const props = defineProps<{
6+
asset: AssetInfo
7+
}>()
8+
9+
const imageMeta = computedAsync(() => {
10+
if (props.asset.type !== 'image')
11+
return undefined
12+
return rpc.getImageMeta(props.asset.filePath)
13+
})
14+
15+
const textContent = computedAsync(() => {
16+
if (props.asset.type !== 'text')
17+
return undefined
18+
return rpc.getTextAssetContent(props.asset.filePath)
19+
})
20+
21+
const codeSnippets = computed(() => {
22+
const items: [string, string, string][] = []
23+
if (props.asset.type === 'image') {
24+
const attrs = imageMeta.value?.width
25+
? ` width="${imageMeta.value.width}" height="${imageMeta.value.height}" `
26+
: ' '
27+
items.push(
28+
['html', `<nuxt-image${attrs}src="${props.asset.publicPath}" />`, 'with Nuxt Image'],
29+
['html', `<img${attrs}src="${props.asset.publicPath}" />`, 'Image'],
30+
)
31+
return items
32+
}
33+
34+
items.push(['html', `<a download href="${props.asset.publicPath}">\n Download ${props.asset.path}\n</a>`, 'Download link'])
35+
return items
36+
})
37+
38+
const copy = useCopy()
39+
const timeago = useTimeAgo(() => props.asset.mtime)
40+
const fileSize = computed(() => {
41+
const size = props.asset.size
42+
if (size < 1024)
43+
return `${size} B`
44+
if (size < 1024 * 1024)
45+
return `${(size / 1024).toFixed(2)} KB`
46+
return `${(size / 1024 / 1024).toFixed(2)} MB`
47+
})
48+
49+
const aspectRatio = computed(() => {
50+
if (!imageMeta.value?.width || !imageMeta.value?.height)
51+
return ''
52+
const gcd = (a: number, b: number): number => {
53+
if (!b)
54+
return a
55+
return gcd(b, a % b)
56+
}
57+
const ratio = gcd(imageMeta.value.width, imageMeta.value.height)
58+
if (ratio > 3)
59+
return `${imageMeta.value.width / ratio} : ${imageMeta.value.height / ratio}`
60+
return ''
61+
})
62+
63+
const supportsPreview = computed(() => {
64+
return [
65+
'image',
66+
'text',
67+
'video',
68+
].includes(props.asset.type)
69+
})
70+
</script>
71+
72+
<template>
73+
<div flex="~ col gap-4" w-full of-hidden p2 h-full of-auto>
74+
<template v-if="supportsPreview">
75+
<div op50 mb--2 flex="~ gap2" items-center>
76+
<div x-divider />
77+
<div flex-none>
78+
Preview
79+
</div>
80+
<div x-divider />
81+
</div>
82+
83+
<div flex="~" items-center justify-center>
84+
<AssetPreview
85+
rounded max-h-80 w-auto min-h-20 min-w-20 border="~ base"
86+
:asset="asset"
87+
:text-content="textContent"
88+
/>
89+
</div>
90+
</template>
91+
92+
<div op50 mb--2 flex="~ gap2" items-center>
93+
<div x-divider />
94+
<div flex-none>
95+
Details
96+
</div>
97+
<div x-divider />
98+
</div>
99+
100+
<table w-full>
101+
<tbody>
102+
<tr>
103+
<td op50 text-right pr5 w-max ws-nowrap>
104+
Filepath
105+
</td>
106+
<td ws-w>
107+
<FilepathItem :filepath="asset.filePath" />
108+
</td>
109+
</tr>
110+
<tr>
111+
<td text-right op50 pr5 w-max ws-nowrap>
112+
Public
113+
</td>
114+
<td>{{ asset.publicPath }}</td>
115+
</tr>
116+
<tr>
117+
<td text-right op50 pr5 w-max ws-nowrap>
118+
Type
119+
</td>
120+
<td capitalize>
121+
{{ asset.type }}
122+
</td>
123+
</tr>
124+
<template v-if="imageMeta?.width">
125+
<tr>
126+
<td text-right op50 pr5 w-max ws-nowrap>
127+
Image Size
128+
</td>
129+
<td>{{ imageMeta.width }} x {{ imageMeta.height }}</td>
130+
</tr>
131+
<tr v-if="aspectRatio">
132+
<td text-right op50 pr5 w-max ws-nowrap>
133+
Aspect Ratio
134+
</td>
135+
<td>{{ aspectRatio }}</td>
136+
</tr>
137+
</template>
138+
<tr>
139+
<td text-right op50 pr5 w-max ws-nowrap>
140+
File size
141+
</td>
142+
<td>{{ fileSize }}</td>
143+
</tr>
144+
<tr>
145+
<td text-right op50 pr5 w-max ws-nowrap>
146+
Last modified
147+
</td>
148+
<td>{{ new Date(asset.mtime).toLocaleString() }} <span op70>({{ timeago }})</span></td>
149+
</tr>
150+
</tbody>
151+
</table>
152+
153+
<div op50 mb--2 flex="~ gap2" items-center>
154+
<div x-divider />
155+
<div flex-none>
156+
Actions
157+
</div>
158+
<div x-divider />
159+
</div>
160+
<div flex="~ gap2 wrap">
161+
<NButton icon="i-carbon-code" @click="openInEditor(asset.filePath)">
162+
Open in Editor
163+
</NButton>
164+
<NButton icon="carbon-launch" :to="asset.publicPath" target="_blank">
165+
Open in browser
166+
</NButton>
167+
<NButton icon="carbon-copy" @click="copy(asset.publicPath)">
168+
Copy public path
169+
</NButton>
170+
<!-- <NButton v-if="asset.type === 'image'" disabled icon="carbon-image-service">
171+
Optimize image (WIP)
172+
</NButton> -->
173+
</div>
174+
175+
<div flex-auto />
176+
177+
<div v-if="codeSnippets?.length" n-code-block border="~ base rounded">
178+
<template v-for="cs, idx of codeSnippets" :key="idx">
179+
<div v-if="idx" x-divider />
180+
<div v-if="cs" of-hidden p2>
181+
<div items-center flex justify-between pb2>
182+
<div op50 ml1>
183+
{{ cs[2] }}
184+
</div>
185+
<NButton icon="carbon-copy" n="sm primary" @click="copy(cs[1])">
186+
Copy
187+
</NButton>
188+
</div>
189+
<NCodeBlock :code="cs[1]" :lang="cs[0]" of-auto w-full :lines="false" px1 />
190+
</div>
191+
</template>
192+
</div>
193+
</div>
194+
</template>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import { hash } from 'ohash'
3+
import type { AssetInfo } from '~/../src/types'
4+
5+
const props = defineProps<{
6+
asset: AssetInfo
7+
}>()
8+
9+
const id = computed(() => `devtools-assets-${hash(props.asset)}`)
10+
11+
useStyleTag(computed(() => `
12+
@font-face {
13+
font-family: '${id.value}';
14+
src: url('${props.asset.publicPath}');
15+
}
16+
`))
17+
</script>
18+
19+
<template>
20+
<div of-hidden :style="{ fontFamily: `'${id}'` }">
21+
Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz
22+
</div>
23+
</template>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import type { AssetInfo } from '~/../src/types'
3+
4+
const props = defineProps<{
5+
asset: AssetInfo
6+
folder?: string
7+
}>()
8+
9+
const path = computed(() => {
10+
if (props.folder && props.asset.path.startsWith(props.folder))
11+
return props.asset.path.slice(props.folder.length)
12+
return props.asset.path
13+
})
14+
</script>
15+
16+
<template>
17+
<button flex="~ col gap-1" hover="bg-active" rounded p2 of-hidden items-center>
18+
<AssetPreview rounded w-30 h-30 border="~ base" :asset="asset" />
19+
<div of-hidden w-full text-center ws-nowrap truncate text-xs>
20+
{{ path }}
21+
</div>
22+
</button>
23+
</template>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import type { AssetInfo } from '~/../src/types'
3+
4+
const props = defineProps<{
5+
asset: AssetInfo
6+
}>()
7+
8+
const icon = computed(() => {
9+
if (props.asset.type === 'image')
10+
return 'i-carbon-image'
11+
if (props.asset.type === 'video')
12+
return 'i-carbon-video'
13+
if (props.asset.type === 'audio')
14+
return 'i-carbon-volume-up'
15+
if (props.asset.type === 'font')
16+
return 'i-carbon-text-small-caps'
17+
if (props.asset.type === 'text')
18+
return 'i-carbon-document'
19+
if (props.asset.type === 'json')
20+
return 'i-carbon-json'
21+
return 'i-carbon-document-blank'
22+
})
23+
</script>
24+
25+
<template>
26+
<button flex="~ gap-1" w-full items-center hover="bg-active" rounded px4 py2>
27+
<div :class="icon" />
28+
<div text-center ws-nowrap of-hidden truncate>
29+
{{ asset.path }}
30+
</div>
31+
</button>
32+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import type { AssetInfo } from '~/../src/types'
3+
4+
defineProps<{
5+
asset: AssetInfo
6+
textContent?: string
7+
}>()
8+
</script>
9+
10+
<template>
11+
<div flex items-center of-hidden justify-center p1 bg-active object-cover>
12+
<template v-if="asset.type === 'image'">
13+
<img :src="asset.publicPath">
14+
</template>
15+
<AssetFontPreview v-else-if="asset.type === 'font'" :key="asset.publicPath" :asset="asset" p2 self-stretch text-2xl />
16+
<div v-else-if="asset.type === 'text' && !textContent" op20 text-3xl i-carbon-document />
17+
<div v-else-if="asset.type === 'text' && textContent" w-full p4 self-start>
18+
<pre of-hidden text-xs font-mono max-h-10rem v-text="textContent" />
19+
</div>
20+
<div v-else op20 text-3xl i-carbon-help />
21+
</div>
22+
</template>

packages/devtools/client/components/ComponentDetails.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const copy = useCopy()
1414
</script>
1515

1616
<template>
17-
<div flex="~ col gap1">
17+
<div flex="~ col gap1" of-hidden items-start>
1818
<div flex="~ gap2">
1919
<div>
2020
<ComponentName :component="component" />
@@ -32,7 +32,7 @@ const copy = useCopy()
3232
<FilepathItem
3333
v-if="filePath"
3434
:filepath="filePath"
35-
text-sm op25 group-hover:op75
35+
w-full text-sm op25 group-hover:op75
3636
/>
3737
<slot />
3838
</div>

0 commit comments

Comments
 (0)