add empty component and poilsh styles

This commit is contained in:
pa
2026-01-22 19:05:35 +09:00
parent 1514012c4c
commit 3c37071011
22 changed files with 296 additions and 65 deletions

View File

@@ -978,18 +978,31 @@
</div>
</div>
<div class="x-friend-list" style="margin-top: 10px; min-height: 60px">
<template v-if="userDialog.worlds.length">
<div
v-for="world in userDialog.worlds"
:key="world.id"
class="x-friend-item x-friend-item-border"
@click="showWorldDialog(world.id)">
<div class="avatar">
<img :src="world.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="world.name"></span>
<span v-if="world.occupants" class="extra">({{ world.occupants }})</span>
</div>
</div>
</template>
<div
v-for="world in userDialog.worlds"
:key="world.id"
class="x-friend-item x-friend-item-border"
@click="showWorldDialog(world.id)">
<div class="avatar">
<img :src="world.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="world.name"></span>
<span v-if="world.occupants" class="extra">({{ world.occupants }})</span>
</div>
v-else-if="!userDialog.isWorldsLoading"
style="
display: flex;
justify-content: center;
align-items: center;
min-height: 120px;
width: 100%;
">
<DataTableEmpty type="nodata" />
</div>
</div>
</template>
@@ -1055,7 +1068,7 @@
</template>
<template v-else-if="!userDialog.isFavoriteWorldsLoading">
<div style="display: flex; justify-content: center; align-items: center; height: 100%">
<span style="font-size: 16px">No favorite worlds found.</span>
<DataTableEmpty type="nodata" />
</div>
</template>
</template>
@@ -1127,25 +1140,41 @@
</div>
</div>
<div class="x-friend-list" style="margin-top: 10px; min-height: 60px; max-height: 50vh">
<template v-if="userDialogAvatars.length">
<div
v-for="avatar in userDialogAvatars"
:key="avatar.id"
class="x-friend-item x-friend-item-border"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span
v-if="avatar.releaseStatus === 'public'"
class="extra"
v-text="avatar.releaseStatus">
</span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
v-text="avatar.releaseStatus">
</span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
</div>
</div>
</template>
<div
v-for="avatar in userDialogAvatars"
:key="avatar.id"
class="x-friend-item x-friend-item-border"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span v-if="avatar.releaseStatus === 'public'" class="extra" v-text="avatar.releaseStatus">
</span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
v-text="avatar.releaseStatus">
</span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
</div>
v-else-if="!userDialog.isAvatarsLoading"
style="
display: flex;
justify-content: center;
align-items: center;
min-height: 120px;
width: 100%;
">
<DataTableEmpty type="nodata" />
</div>
</div>
</template>
@@ -1217,6 +1246,7 @@
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { DataTableEmpty } from '@/components/ui/data-table';
import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia';

View File

@@ -0,0 +1,34 @@
<template>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<SearchAlert v-if="props.type === 'nomatch'" class="text-lg" />
<Inbox v-else class="text-lg" />
</EmptyMedia>
<EmptyDescription>
{{ emptyText }}
</EmptyDescription>
</EmptyHeader>
</Empty>
</template>
<script setup>
import { Inbox, SearchAlert } from 'lucide-vue-next';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia } from '../empty';
const props = defineProps({
type: {
type: String,
default: 'nodata'
}
});
const { t } = useI18n();
const emptyText = computed(() => {
return props.type === 'nomatch' ? t('common.no_matching_records') : t('common.no_data');
});
</script>

View File

@@ -59,9 +59,9 @@
</template>
<TableRow v-else>
<TableCell class="h-24 text-center">
<TableCell class="h-24 text-center" :colspan="table.getVisibleLeafColumns().length">
<slot name="empty">
{{ emptyText }}
<DataTableEmpty :type="emptyType" />
</slot>
</TableCell>
</TableRow>
@@ -134,6 +134,8 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
import DataTableEmpty from './DataTableEmpty.vue';
const appearanceSettingsStore = useAppearanceSettingsStore();
const { isDataTableStriped } = storeToRefs(appearanceSettingsStore);
@@ -162,10 +164,6 @@
type: Array,
default: () => []
},
emptyText: {
type: String,
default: 'No results.'
},
showPagination: {
type: Boolean,
default: true
@@ -187,6 +185,11 @@
const { t } = useI18n();
const tableScrollRef = ref(null);
const emptyType = computed(() => {
const totalRows = props.table?.getCoreRowModel?.().rows?.length ?? 0;
return totalRows === 0 ? 'nodata' : 'nomatch';
});
const expandedRenderer = computed(() => {
const columns = props.table.getAllColumns?.() ?? [];
for (const column of columns) {

View File

@@ -1 +1,2 @@
export { default as DataTableLayout } from './DataTableLayout.vue';
export { default as DataTableEmpty } from './DataTableEmpty.vue';

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="empty"
:class="
cn(
'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="empty-content"
:class="cn('flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
defineProps({
class: { type: null, required: false }
});
</script>
<template>
<p
data-slot="empty-description"
:class="
cn(
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
$attrs.class ?? ''
)
">
<slot />
</p>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div data-slot="empty-header" :class="cn('flex max-w-sm flex-col items-center gap-2 text-center', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup>
import { cn } from '@/lib/utils';
import { emptyMediaVariants } from '.';
const props = defineProps({
class: { type: null, required: false },
variant: { type: null, required: false }
});
</script>
<template>
<div data-slot="empty-icon" :data-variant="variant" :class="cn(emptyMediaVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div data-slot="empty-title" :class="cn('text-lg font-medium tracking-tight', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
import { cva } from 'class-variance-authority';
export { default as Empty } from './Empty.vue';
export { default as EmptyContent } from './EmptyContent.vue';
export { default as EmptyDescription } from './EmptyDescription.vue';
export { default as EmptyHeader } from './EmptyHeader.vue';
export { default as EmptyMedia } from './EmptyMedia.vue';
export { default as EmptyTitle } from './EmptyTitle.vue';
export const emptyMediaVariants = cva(
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6"
}
},
defaultVariants: {
variant: 'default'
}
}
);