feat: more to explore (#360)

zio/stable
Ayaka Rizumu 2022-12-11 18:52:36 +08:00 committed by GitHub
parent a36a26d745
commit 183b1659d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 530 additions and 17 deletions

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { Account } from 'masto'
const { account, as = 'div' } = $defineProps<{
account: Account
as?: string
}>()
cacheAccount(account)
defineOptions({
inheritAttrs: false,
})
</script>
<template>
<component :is="as" block focus:outline-none focus-visible:ring="2 primary" v-bind="$attrs">
<!-- Banner -->
<div px2 pt2>
<div rounded of-hidden bg="gray-500/20" aspect="3.19">
<img h-full w-full object-cover :src="account.header" :alt="$t('account.profile_description', [account.username])">
</div>
</div>
<div px-4 pb-4 space-y-2>
<!-- User info -->
<div flex sm:flex-row flex-col flex-gap-2>
<div flex items-center justify-between>
<div w-17 h-17 rounded-full border-4 border-bg-base z-2 mt--2 ml--1>
<AccountAvatar :account="account" />
</div>
<a block sm:hidden href="javascript:;" @click.stop>
<AccountFollowButton :account="account" />
</a>
</div>
<div sm:mt-2>
<div>
<ContentRich
font-bold text-lg line-clamp-1 ws-pre-wrap break-all
:content="getDisplayName(account, { rich: true })"
:emojis="account.emojis"
/>
</div>
<AccountHandle text-sm :account="account" />
</div>
</div>
<!-- Note -->
<div v-if="account.note">
<ContentRich
:content="account.note" :emojis="account.emojis"
line-clamp-2
/>
</div>
<!-- Follow info -->
<div flex justify-between items-center>
<AccountPostsFollowers text-sm :account="account" />
<a sm:block hidden href="javascript:;" @click.stop>
<AccountFollowButton :account="account" />
</a>
</div>
</div>
</component>
</template>

View File

@ -0,0 +1,30 @@
<template>
<div>
<!-- Banner -->
<div px2 pt2>
<div rounded of-hidden aspect="3.19" class="flex skeleton-loading-bg" />
<div px-4 pb-4 flex="~ col gap-2">
<!-- User info -->
<div flex sm:flex-row flex-col flex-gap-2>
<div flex items-center justify-between>
<div w-17 h-17 rounded-full border-4 border-bg-base z-2 mt--2 ml--1 of-hidden bg-base>
<div class="flex skeleton-loading-bg" w-full h-full />
</div>
<div block sm:hidden class="skeleton-loading-bg" h-8 w-30 rounded-full />
</div>
<div sm:mt-2 flex="~ col 1 gap-2">
<div flex class="skeleton-loading-bg" h-5 w-20 rounded />
<div flex class="skeleton-loading-bg" h-4 w-40 rounded />
</div>
</div>
<!-- Note -->
<div flex class="skeleton-loading-bg" h-4 my3 w="3/5" rounded />
<!-- Follow info -->
<div flex justify-between items-center>
<div flex class="skeleton-loading-bg" h-4 w="sm:1/2 full" rounded />
<div sm:flex hidden class="skeleton-loading-bg" h-8 w-30 rounded-full />
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{
modelValue?: boolean
}>(), {
modelValue: true,
})
const emits = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(event: 'close'): void
}>()
const visible = useVModel(props, 'modelValue', emits, { passive: true })
function close() {
emits('close')
visible.value = false
}
</script>
<template>
<div
flex="~ gap-2" justify-between items-center
class="border-b border-base text-sm text-secondary px4 py2 sm:py4"
>
<div>
<slot />
</div>
<button text-xl hover:text-primary bg-hover-overflow w-1.4em h-1.4em @click="close()">
<div i-ri:close-line />
</button>
</div>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
const { options, command, replace } = $defineProps<{
options: {
to: RouteLocationRaw
display: string
name?: string
icon?: string
}[]
command?: boolean
replace?: boolean
}>()
const router = useRouter()
useCommands(() => command
? options.map(tab => ({
scope: 'Tabs',
name: tab.display,
icon: tab.icon ?? 'i-ri:file-list-2-line',
onActivate: () => router.replace(tab.to),
}))
: [])
</script>
<template>
<div flex w-full items-center lg:text-lg of-x-auto scrollbar-hide>
<NuxtLink
v-for="(option, index) in options"
:key="option?.name || index"
:to="option.to"
:replace="replace"
relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
tabindex="1"
hover:bg-active transition-100
exact-active-class="children:(font-bold !border-primary !op100)"
@click="$scrollToTop"
>
<span ws-nowrap mxa sm:px2 sm:py3 py2 text-center border-b-3 op50 hover:op70 border-transparent>{{ option.display }}</span>
</NuxtLink>
</div>
</template>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { History } from 'masto'
const {
history,
maxDay = 2,
} = $defineProps<{
history: History[]
maxDay?: number
}>()
const ongoingHot = $computed(() => history.slice(0, maxDay))
const people = $computed(() =>
ongoingHot.reduce((total: number, item) => total + (Number(item.accounts) || 0), 0),
)
</script>
<template>
<p>
{{ $t('command.n-people-in-the-past-n-days', [people, maxDay]) }}
</p>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { History } from 'masto'
import sparkline from '@fnando/sparkline'
const {
history,
} = $defineProps<{
history?: History[]
}>()
const historyNum = $computed(() => {
if (!history)
return [1, 1, 1, 1, 1, 1, 1]
return [...history].reverse().map(item => Number(item.accounts) || 0)
})
const sparklineEl = $ref<SVGSVGElement>()
watch([$$(historyNum), $$(sparklineEl)], ([historyNum, sparklineEl]) => {
if (!sparklineEl)
return
sparkline(sparklineEl, historyNum)
})
</script>
<template>
<svg ref="sparklineEl" class="sparkline" width="60" height="40" stroke-width="3" />
</template>

View File

@ -10,7 +10,7 @@ const props = defineProps<{
}>() }>()
const alt = $computed(() => `${props.card.title} - ${props.card.title}`) const alt = $computed(() => `${props.card.title} - ${props.card.title}`)
const isSquare = $computed(() => props.smallPictureOnly || props.card.width === props.card.height) const isSquare = $computed(() => props.smallPictureOnly || props.card.width === props.card.height)
const description = $computed(() => props.card.description ? props.card.description : new URL(props.card.url).hostname) const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname)
// TODO: handle card.type: 'photo' | 'video' | 'rich'; // TODO: handle card.type: 'photo' | 'video' | 'rich';
</script> </script>
@ -32,10 +32,9 @@ const description = $computed(() => props.card.description ? props.card.descript
v-if="card.image" v-if="card.image"
flex flex-col flex flex-col
display-block of-hidden display-block of-hidden
border="base" border="base"
:class="{ :class="{
'min-w-32 w-32 h-32 border-r': isSquare, 'sm:(min-w-32 w-32 h-32) min-w-22 w-22 h-22 border-r': isSquare,
'w-full aspect-[1.91] border-b': !isSquare, 'w-full aspect-[1.91] border-b': !isSquare,
'rounded-lg': root, 'rounded-lg': root,
}" }"
@ -49,19 +48,41 @@ const description = $computed(() => props.card.description ? props.card.descript
w-full h-full object-cover w-full h-full object-cover
/> />
</div> </div>
<div v-else min-w-32 w-32 h-32 bg="slate-500/10" flex justify-center items-center> <div
v-else
min-w-22 w-22 h-22 sm="min-w-32 w-32 h-32" bg="slate-500/10" flex justify-center items-center
:class="[
root ? 'rounded-lg' : '',
]"
>
<div i-ri:profile-line w="30%" h="30%" text-secondary /> <div i-ri:profile-line w="30%" h="30%" text-secondary />
</div> </div>
<div <div
p4 max-h-2xl px3 max-h-2xl
flex flex-col flex flex-col
:class="[
root ? 'flex-gap-1 py1 sm:py3' : 'py3 justify-center sm:justify-start',
]"
> >
<p v-if="card.providerName" text-secondary line-clamp-1 text-ellipsis> <p
{{ card.providerName }} text-secondary ws-pre-wrap break-all
:class="[
!card.description || root
? 'line-clamp-1'
: 'hidden sm:line-clamp-1',
]"
>
{{ providerName }}
</p> </p>
<strong v-if="card.title" line-clamp-1 text-ellipsis>{{ card.title }}</strong> <strong
<p v-if="description" text-secondary line-clamp-2 text-ellipsis> v-if="card.title" font-normal sm:font-medium line-clamp-1
{{ description }} break-all ws-pre-wrap
>{{ card.title }}</strong>
<p
v-if="card.description"
line-clamp-1 break-all sm:line-clamp-2 sm:break-words text-secondary ws-pre-wrap
>
{{ card.description }}
</p> </p>
</div> </div>
</NuxtLink> </NuxtLink>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
defineProps<{
/** For the preview image, only the small image mode is displayed */
square?: boolean
/** When it is root card in the list, not appear as a child card */
root?: boolean
}>()
</script>
<template>
<div
of-hidden
:class="{
'flex': square,
'p-4': root,
'rounded-lg border border-base': !root,
}"
>
<div
flex flex-col
display-block of-hidden
border="base"
:class="{
'sm:(min-w-32 w-32 h-32) min-w-22 w-22 h-22 border-r': square,
'w-full aspect-[1.91] border-b': !square,
'rounded-lg': root,
}"
>
<div w-full h-full class="skeleton-loading-bg" />
</div>
<div
px3 max-h-2xl
flex-1 flex flex-col flex-gap-2 sm:flex-gap-3
:class="[
root ? 'py2.5 sm:py3' : 'py3 justify-center sm:justify-start',
]"
>
<div flex class="skeleton-loading-bg" h-4 w-30 rounded :class="root ? '' : 'hidden sm:block'" />
<div flex class="skeleton-loading-bg" h-5 w="4/5" rounded />
<div flex="~ col gap-2">
<div flex class="skeleton-loading-bg" h-4 w-full rounded />
<div sm:flex hidden class="skeleton-loading-bg" h-4 w="2/5" rounded />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import type { Tag } from 'masto'
const {
tag,
} = $defineProps<{
tag: Tag
}>()
const to = $computed(() => new URL(tag.url).pathname)
</script>
<template>
<NuxtLink :to="to" block p4 hover:bg-active flex justify-between>
<div>
<h4 text-size-base leading-normal font-medium line-clamp-1 break-all ws-pre-wrap>
<span>#</span>
<span hover:underline>{{ tag.name }}</span>
</h4>
<CommonTrending :history="tag.history" text-sm text-secondary line-clamp-1 ws-pre-wrap break-all />
</div>
<div flex items-center>
<CommonTrendingCharts :history="tag.history" />
</div>
</NuxtLink>
</template>

View File

@ -0,0 +1,11 @@
<template>
<div p4 flex justify-between>
<div flex="~ col 1 gap-2">
<div flex class="skeleton-loading-bg" h-5 w-30 rounded />
<div flex class="skeleton-loading-bg" h-4 w-45 rounded />
</div>
<div flex items-center>
<div flex class="skeleton-loading-bg" h-9 w-15 rounded />
</div>
</div>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
// @ts-expect-error missing types // @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller' import { DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import type { FilterContext, Paginator, Status, WsEvents } from 'masto' import type { FilterContext, Paginator, Status, WsEvents } from 'masto'
const { paginator, stream } = defineProps<{ const { paginator, stream } = defineProps<{

View File

@ -12,5 +12,8 @@ export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode' export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode'
export const STORAGE_KEY_LANG = 'elk-lang' export const STORAGE_KEY_LANG = 'elk-lang'
export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags' export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags'
export const STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS = 'elk-hide-explore-posts-tips'
export const STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS = 'elk-hide-explore-news-tips'
export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips'
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/ export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/

View File

@ -50,6 +50,7 @@
"complete": "Complete", "complete": "Complete",
"compose_desc": "Write a new post", "compose_desc": "Write a new post",
"lang": "Languages", "lang": "Languages",
"n-people-in-the-past-n-days": "{0} people in the past {1} days",
"select_lang": "Select language", "select_lang": "Select language",
"sign_in_desc": "Add an existing account", "sign_in_desc": "Add an existing account",
"switch_account": "Switch to {0}", "switch_account": "Switch to {0}",
@ -70,6 +71,7 @@
}, },
"error": { "error": {
"account_not_found": "Account {0} not found", "account_not_found": "Account {0} not found",
"explore-list-empty": "Nothing is trending right now. Check back later!",
"status_not_found": "Status not found" "status_not_found": "Status not found"
}, },
"feature_flag": { "feature_flag": {
@ -172,7 +174,10 @@
"edited": "edited {0}" "edited": "edited {0}"
}, },
"tab": { "tab": {
"for_you": "For you",
"hashtags": "Hashtags",
"media": "Media", "media": "Media",
"news": "News",
"notifications_all": "All", "notifications_all": "All",
"notifications_mention": "Mention", "notifications_mention": "Mention",
"posts": "Posts", "posts": "Posts",
@ -220,6 +225,9 @@
"add_content_warning": "Add content warning", "add_content_warning": "Add content warning",
"add_media": "Add images, a video or an audio file", "add_media": "Add images, a video or an audio file",
"change_content_visibility": "Change content visibility", "change_content_visibility": "Change content visibility",
"explore_links_intro": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"explore_posts_intro": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
"explore_tags_intro": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"toggle_code_block": "Toggle code block" "toggle_code_block": "Toggle code block"
}, },
"user": { "user": {

View File

@ -49,6 +49,7 @@
"complete": "完成", "complete": "完成",
"compose_desc": "写一条新帖文", "compose_desc": "写一条新帖文",
"lang": "语言", "lang": "语言",
"n-people-in-the-past-n-days": "{0} 人在过去 {1} 天",
"select_lang": "选择语言", "select_lang": "选择语言",
"sign_in_desc": "添加现有帐户", "sign_in_desc": "添加现有帐户",
"switch_account": "切换到{0}", "switch_account": "切换到{0}",
@ -69,6 +70,7 @@
}, },
"error": { "error": {
"account_not_found": "未找到用户 {0}", "account_not_found": "未找到用户 {0}",
"explore-list-empty": "目前没有热门话题,稍后再来看看吧!",
"status_not_found": "未找到帖文" "status_not_found": "未找到帖文"
}, },
"feature_flag": { "feature_flag": {
@ -168,7 +170,10 @@
"edited": "在 {0} 编辑了" "edited": "在 {0} 编辑了"
}, },
"tab": { "tab": {
"for_you": "推荐关注",
"hashtags": "话题标签",
"media": "媒体", "media": "媒体",
"news": "最新消息",
"notifications_all": "全部", "notifications_all": "全部",
"notifications_mention": "提及", "notifications_mention": "提及",
"posts": "帖文", "posts": "帖文",
@ -216,6 +221,9 @@
"add_content_warning": "添加内容警告标识", "add_content_warning": "添加内容警告标识",
"add_media": "添加图片、视频或者音频文件", "add_media": "添加图片、视频或者音频文件",
"change_content_visibility": "修改内容是否可见", "change_content_visibility": "修改内容是否可见",
"explore_links_intro": "这些新闻故事正被本站和分布式网络上其他站点的用户谈论。",
"explore_posts_intro": "来自本站和分布式网络上其他站点的这些嘟文正在本站引起关注。",
"explore_tags_intro": "这些标签正在本站和分布式网络上其他站点的用户中引起关注。",
"toggle_code_block": "切换代码块" "toggle_code_block": "切换代码块"
}, },
"user": { "user": {

View File

@ -17,6 +17,9 @@
"test": "nr test:unit", "test": "nr test:unit",
"postinstall": "nuxi prepare && simple-git-hooks" "postinstall": "nuxi prepare && simple-git-hooks"
}, },
"dependencies": {
"@fnando/sparkline": "^0.3.10"
},
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.33.1", "@antfu/eslint-config": "^0.33.1",
"@antfu/ni": "^0.18.8", "@antfu/ni": "^0.18.8",
@ -38,6 +41,7 @@
"@tiptap/starter-kit": "2.0.0-beta.204", "@tiptap/starter-kit": "2.0.0-beta.204",
"@tiptap/suggestion": "2.0.0-beta.204", "@tiptap/suggestion": "2.0.0-beta.204",
"@tiptap/vue-3": "2.0.0-beta.204", "@tiptap/vue-3": "2.0.0-beta.204",
"@types/fnando__sparkline": "^0.3.4",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/prettier": "^2.7.1", "@types/prettier": "^2.7.1",

View File

@ -1,8 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().trends.iterateStatuses() import { invoke } from '@vueuse/shared'
const { t } = useI18n() const { t } = useI18n()
const tabs = $computed(() => [
{
to: `/${currentServer.value}/explore`,
display: t('tab.posts'),
},
{
to: `/${currentServer.value}/explore/tags`,
display: t('tab.hashtags'),
},
{
to: `/${currentServer.value}/explore/links`,
display: t('tab.news'),
},
// This section can only be accessed after logging in
...invoke(() => currentUser.value
? [
{
to: `/${currentServer.value}/explore/users`,
display: t('tab.for_you'),
},
]
: [],
),
] as const)
useHeadFixed({ useHeadFixed({
title: () => t('nav_side.explore'), title: () => t('nav_side.explore'),
}) })
@ -11,15 +36,15 @@ useHeadFixed({
<template> <template>
<MainContent> <MainContent>
<template #title> <template #title>
<NuxtLink to="/explore" text-lg font-bold flex items-center gap-2 @click="$scrollToTop"> <span text-lg font-bold flex items-center gap-2 cursor-pointer @click="$scrollToTop">
<div i-ri:hashtag /> <div i-ri:hashtag />
<span>{{ t('nav_side.explore') }}</span> <span>{{ t('nav_side.explore') }}</span>
</NuxtLink> </span>
</template> </template>
<slot> <template #header>
<!-- TODO: Tabs for trending statuses, tags, and links --> <CommonRouteTabs replace :options="tabs" />
<TimelinePaginator :paginator="paginator" context="public" /> </template>
</slot> <NuxtPage />
</MainContent> </MainContent>
</template> </template>

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS } from '~~/constants'
const paginator = useMasto().trends.iterateStatuses()
const hideNewsTips = useLocalStorage(STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS, false)
</script>
<template>
<CommonAlert v-if="!hideNewsTips" @close="hideNewsTips = true">
<p>{{ $t('tooltip.explore_posts_intro') }}</p>
</CommonAlert>
<!-- TODO: Tabs for trending statuses, tags, and links -->
<TimelinePaginator :paginator="paginator" context="public" />
</template>

View File

@ -0,0 +1,25 @@
<script lang="ts" setup>
// @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller'
import { STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS } from '~~/constants'
const paginator = useMasto().trends.links
const hideNewsTips = useLocalStorage(STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS, false)
</script>
<template>
<CommonAlert v-if="!hideNewsTips" @close="hideNewsTips = true">
<p>{{ $t('tooltip.explore_links_intro') }}</p>
</CommonAlert>
<CommonPaginator v-bind="{ paginator }">
<template #default="{ item }">
<StatusPreviewCard :card="item" border="!b base" rounded="!none" p="!4" small-picture-only root />
</template>
<template #loading>
<StatusPreviewCardSkeleton square root border="b base" />
<StatusPreviewCardSkeleton square root border="b base" op50 />
<StatusPreviewCardSkeleton square root border="b base" op25 />
</template>
</CommonPaginator>
</template>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { Tag } from 'masto'
import { STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS } from '~~/constants'
const { data, pending, error } = useLazyAsyncData(
() => useMasto().trends.fetchTags({ limit: 20 }),
{ immediate: true },
)
const hideTagsTips = useLocalStorage(STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS, false)
function getTagUrl(tag: Tag) {
return new URL(tag.url).pathname
}
</script>
<template>
<CommonAlert v-if="!hideTagsTips && data && data.length" @close="hideTagsTips = true">
<p>{{ $t('tooltip.explore_tags_intro') }}</p>
</CommonAlert>
<div v-if="data && data.length">
<TagCard v-for="item of data" :key="item.name" :tag="item" border="b base" />
<div p5 text-center text-secondary-light italic>
{{ $t('common.end_of_list') }}
</div>
</div>
<div v-else-if="pending">
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op50 />
<TagCardSkeleton border="b base" op25 />
</div>
<div v-else-if="error" p5 text-center text-red italic>
{{ $t('common.error') }}: {{ error }}
</div>
<div v-else p5 text-center text-secondary italic>
{{ $t('error.explore-list-empty') }}
</div>
</template>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
// limit: 20 is the default configuration of the official client
const { data, pending, error } = useLazyAsyncData(
() => useMasto().suggestions.fetchAll({ limit: 20 }),
{ immediate: true },
)
</script>
<template>
<div v-if="data && data.length">
<AccountBigCard
v-for="suggestion of data"
:key="suggestion.account.id"
:account="suggestion.account"
as="router-link"
:to="getAccountRoute(suggestion.account)"
border="b base"
/>
<div p5 text-center text-secondary-light italic>
{{ $t('common.end_of_list') }}
</div>
</div>
<div v-else-if="pending">
<AccountBigCardSkeleton border="b base" />
<AccountBigCardSkeleton border="b base" op50 />
<AccountBigCardSkeleton border="b base" op25 />
</div>
<div v-else-if="error" p5 text-center text-red italic>
{{ $t('common.error') }}: {{ error }}
</div>
<div v-else p5 text-center text-secondary italic>
{{ $t('common.not_found') }}
</div>
</template>

View File

@ -3,6 +3,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@antfu/eslint-config': ^0.33.1 '@antfu/eslint-config': ^0.33.1
'@antfu/ni': ^0.18.8 '@antfu/ni': ^0.18.8
'@fnando/sparkline': ^0.3.10
'@iconify-json/carbon': ^1.1.11 '@iconify-json/carbon': ^1.1.11
'@iconify-json/logos': ^1.1.19 '@iconify-json/logos': ^1.1.19
'@iconify-json/material-symbols': ^1.1.25 '@iconify-json/material-symbols': ^1.1.25
@ -21,6 +22,7 @@ specifiers:
'@tiptap/starter-kit': 2.0.0-beta.204 '@tiptap/starter-kit': 2.0.0-beta.204
'@tiptap/suggestion': 2.0.0-beta.204 '@tiptap/suggestion': 2.0.0-beta.204
'@tiptap/vue-3': 2.0.0-beta.204 '@tiptap/vue-3': 2.0.0-beta.204
'@types/fnando__sparkline': ^0.3.4
'@types/fs-extra': ^9.0.13 '@types/fs-extra': ^9.0.13
'@types/js-yaml': ^4.0.5 '@types/js-yaml': ^4.0.5
'@types/prettier': ^2.7.1 '@types/prettier': ^2.7.1
@ -66,6 +68,9 @@ specifiers:
vue-tsc: ^1.0.11 vue-tsc: ^1.0.11
vue-virtual-scroller: 2.0.0-beta.4 vue-virtual-scroller: 2.0.0-beta.4
dependencies:
'@fnando/sparkline': 0.3.10
devDependencies: devDependencies:
'@antfu/eslint-config': 0.33.1_s5ps7njkmjlaqajutnox5ntcla '@antfu/eslint-config': 0.33.1_s5ps7njkmjlaqajutnox5ntcla
'@antfu/ni': 0.18.8 '@antfu/ni': 0.18.8
@ -87,6 +92,7 @@ devDependencies:
'@tiptap/starter-kit': 2.0.0-beta.204 '@tiptap/starter-kit': 2.0.0-beta.204
'@tiptap/suggestion': 2.0.0-beta.204 '@tiptap/suggestion': 2.0.0-beta.204
'@tiptap/vue-3': 2.0.0-beta.204 '@tiptap/vue-3': 2.0.0-beta.204
'@types/fnando__sparkline': 0.3.4
'@types/fs-extra': 9.0.13 '@types/fs-extra': 9.0.13
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/prettier': 2.7.1 '@types/prettier': 2.7.1
@ -626,6 +632,10 @@ packages:
'@floating-ui/core': 0.3.1 '@floating-ui/core': 0.3.1
dev: true dev: true
/@fnando/sparkline/0.3.10:
resolution: {integrity: sha512-Rwz2swatdSU5F4sCOvYG8EOWdjtLgq5d8nmnqlZ3PXdWJI9Zq9BRUvJ/9ygjajJG8qOyNpMFX3GEVFjZIuB1Jg==}
dev: false
/@humanwhocodes/config-array/0.11.7: /@humanwhocodes/config-array/0.11.7:
resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@ -1617,6 +1627,10 @@ packages:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true dev: true
/@types/fnando__sparkline/0.3.4:
resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==}
dev: true
/@types/fs-extra/9.0.13: /@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies: dependencies:

View File

@ -64,6 +64,7 @@
/* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */ /* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */
body { body {
overflow-y: scroll; overflow-y: scroll;
-webkit-tap-highlight-color: transparent;
} }
.zen .zen-hide { .zen .zen-hide {
@ -164,3 +165,13 @@ body {
/* Prevent arbitrary zooming on mobile devices */ /* Prevent arbitrary zooming on mobile devices */
touch-action: pan-x pan-y; touch-action: pan-x pan-y;
} }
.sparkline--fill {
fill: var(--c-primary-active);
opacity: 0.2;
}
.sparkline--line {
stroke: var(--c-primary);
stroke-width: 2;
}

View File

@ -43,6 +43,7 @@ export default defineConfig({
'flex-center': 'items-center justify-center', 'flex-center': 'items-center justify-center',
'flex-v-center': 'items-center', 'flex-v-center': 'items-center',
'flex-h-center': 'justify-center', 'flex-h-center': 'justify-center',
'bg-hover-overflow': 'relative z-0 transition-colors duration-250 after-content-empty after:(absolute inset--2px bg-transparent rounded-lg z--1 transition-colors duration-250) hover:after:(bg-active)',
}, },
], ],
presets: [ presets: [