rewrite feed table

This commit is contained in:
pa
2026-01-05 23:58:02 +09:00
committed by Natsumi
parent 28488a6887
commit 2b4273b492
54 changed files with 1830 additions and 1054 deletions

View File

@@ -27,7 +27,8 @@
import MacOSTitleBar from './components/MacOSTitleBar.vue';
import VRCXUpdateDialog from './components/dialogs/VRCXUpdateDialog.vue';
import './app.css';
import '@/styles/globals.css';
import '@/app.css';
console.log(`isLinux: ${LINUX}`);

View File

@@ -6,8 +6,6 @@
/* For a copy, see <https://opensource.org/licenses/MIT>.
*/
@import 'tailwindcss';
@import 'element-plus/dist/index.css';
@import 'element-plus/theme-chalk/dark/css-vars.css';
@@ -27,23 +25,28 @@
--font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-fallback-cjk: sans-serif;
--font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans SC Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
'Noto Sans JP Variable', 'Noto Sans SC Variable',
'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='zh-CN'] {
--font-primary-cjk:
'Noto Sans SC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable';
'Noto Sans SC Variable', 'Noto Sans JP Variable',
'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='ja'] {
--font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
'Noto Sans JP Variable', 'Noto Sans KR Variable',
'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='ko'] {
--font-primary-cjk:
'Noto Sans KR Variable', 'Noto Sans JP Variable', 'Noto Sans TC Variable', 'Noto Sans SC Variable';
'Noto Sans KR Variable', 'Noto Sans JP Variable',
'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='zh-TW'] {
--font-primary-cjk:
'Noto Sans TC Variable', 'Noto Sans JP Variable', 'Noto Sans KR Variable', 'Noto Sans SC Variable';
'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans KR Variable', 'Noto Sans SC Variable';
}
:root {
--el-color-primary-light-9: color-mix(
@@ -418,6 +421,12 @@ html.dark .x-friend-item > .detail > .extra,
color: var(--color-neutral-300);
}
.lucide {
width: 16px;
height: 16px;
stroke-width: 1.5px;
}
#x-app {
background: var(--el-bg-color-page);
color: var(--el-text-color-primary);

View File

@@ -0,0 +1,22 @@
<script setup>
import { Primitive } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { badgeVariants } from '.';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
variant: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Primitive data-slot="badge" :class="cn(badgeVariants({ variant }), props.class)" v-bind="delegatedProps">
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,24 @@
import { cva } from 'class-variance-authority';
export { default as Badge } from './Badge.vue';
export const badgeVariants = cva(
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
);

View File

@@ -1,24 +1,24 @@
<script setup>
import { Primitive } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from ".";
import { Primitive } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = defineProps({
variant: { type: null, required: false },
size: { type: null, required: false },
class: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: "button" },
});
import { buttonVariants } from '.';
const props = defineProps({
variant: { type: null, required: false },
size: { type: null, required: false },
class: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: 'button' }
});
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</Primitive>
</template>

View File

@@ -1,35 +1,35 @@
import { cva } from "class-variance-authority";
import { cva } from 'class-variance-authority';
export { default as Button } from "./Button.vue";
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);

View File

@@ -0,0 +1,235 @@
<template>
<div class="flex flex-col">
<div v-if="$slots.toolbar" class="mb-2">
<slot name="toolbar"></slot>
</div>
<div class="rounded-md border">
<div v-loading="loading" class="overflow-auto" :style="tableStyle">
<Table class="table-fixed">
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
:class="getHeaderClass(header)">
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow>
<TableCell
v-for="cell in row.getVisibleCells()"
:key="cell.id"
:class="getCellClass(cell)">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded() && (expandedRenderer || $slots.expanded)">
<TableCell :colspan="row.getVisibleCells().length">
<template v-if="$slots.expanded">
<slot name="expanded" :row="row"></slot>
</template>
<template v-else>
<FlexRender :render="expandedRenderer" :props="{ row }" />
</template>
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell :colspan="visibleColumnCount" class="h-24 text-center">
<slot name="empty">
{{ emptyText }}
</slot>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div class="mt-4 flex w-full items-center gap-3">
<div v-if="pageSizes.length" class="inline-flex items-center flex-1 justify-end gap-2">
<span class="text-xs text-muted-foreground">{{ t('table.pagination.rows_per_page') }}</span>
<Select v-model="pageSizeValue">
<SelectTrigger size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="size in pageSizes" :key="size" :value="String(size)">
{{ size }}
</SelectItem>
</SelectContent>
</Select>
</div>
<Pagination
v-model:page="currentPage"
:total="totalItems"
:items-per-page="pageSizeProxy"
:sibling-count="1"
show-edges
class="flex-none">
<PaginationContent v-slot="{ items }">
<PaginationPrevious />
<template v-for="item in items" :key="item.key">
<PaginationItem
v-if="item.type === 'page'"
:value="item.value"
:is-active="item.value === currentPage">
{{ item.value }}
</PaginationItem>
<PaginationEllipsis v-else />
</template>
<PaginationNext />
</PaginationContent>
</Pagination>
<div class="flex-1"></div>
</div>
</div>
</template>
<script setup>
import { FlexRender } from '@tanstack/vue-table';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationNext,
PaginationPrevious
} from '../pagination';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
const props = defineProps({
table: {
type: Object,
required: true
},
tableStyle: {
type: Object,
default: null
},
loading: {
type: Boolean,
default: false
},
totalItems: {
type: Number,
default: 0
},
pageSizes: {
type: Array,
default: () => []
},
emptyText: {
type: String,
default: 'No results.'
},
onPageSizeChange: {
type: Function,
default: null
},
onPageChange: {
type: Function,
default: null
}
});
const { t } = useI18n();
const visibleColumnCount = computed(() => {
const count = props.table.getVisibleLeafColumns?.().length ?? 0;
if (count > 0) {
return count;
}
return props.table.getAllColumns?.().length ?? 1;
});
const expandedRenderer = computed(() => {
const columns = props.table.getAllColumns?.() ?? [];
for (const column of columns) {
const meta = column.columnDef?.meta;
if (meta?.expandedRow) {
return meta.expandedRow;
}
}
return null;
});
const joinClasses = (...values) =>
values
.flatMap((value) => (Array.isArray(value) ? value : [value]))
.filter(Boolean)
.join(' ');
const resolveClassValue = (value, ctx) => {
if (typeof value === 'function') {
return value(ctx);
}
return value;
};
const getHeaderClass = (header) => {
const columnDef = header?.column?.columnDef;
const meta = columnDef?.meta ?? {};
return joinClasses(
'sticky top-0 z-10 bg-background',
resolveClassValue(meta.class, header?.getContext?.()),
resolveClassValue(meta.headerClass, header?.getContext?.()),
resolveClassValue(meta.thClass, header?.getContext?.()),
resolveClassValue(columnDef?.class, header?.getContext?.()),
resolveClassValue(columnDef?.headerClass, header?.getContext?.())
);
};
const getCellClass = (cell) => {
const columnDef = cell?.column?.columnDef;
const meta = columnDef?.meta ?? {};
return joinClasses(
resolveClassValue(meta.class, cell?.getContext?.()),
resolveClassValue(meta.cellClass, cell?.getContext?.()),
resolveClassValue(meta.tdClass, cell?.getContext?.()),
resolveClassValue(columnDef?.class, cell?.getContext?.()),
resolveClassValue(columnDef?.cellClass, cell?.getContext?.())
);
};
const handlePageSizeChange = (size) => {
if (props.onPageSizeChange) {
props.onPageSizeChange(size);
}
props.table.setPageSize(size);
};
const pageSizeProxy = computed({
get: () => props.table.getState().pagination.pageSize,
set: (size) => handlePageSizeChange(size)
});
const pageSizeValue = computed({
get: () => String(pageSizeProxy.value),
set: (value) => handlePageSizeChange(Number(value))
});
const currentPage = computed({
get: () => props.table.getState().pagination.pageIndex + 1,
set: (page) => {
props.table.setPageIndex(page - 1);
if (props.onPageChange) {
props.onPageChange(page);
}
}
});
</script>

View File

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

View File

@@ -1,23 +1,19 @@
<script setup>
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui";
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
dir: { type: String, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
dir: { type: String, required: false },
modal: { type: Boolean, required: false }
});
const emits = defineEmits(['update:open']);
const forwarded = useForwardPropsEmits(props, emits);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot
v-slot="slotProps"
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DropdownMenuRoot>
<DropdownMenuRoot v-slot="slotProps" data-slot="dropdown-menu" v-bind="forwarded">
<slot v-bind="slotProps" />
</DropdownMenuRoot>
</template>

View File

@@ -1,48 +1,41 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Check } from "lucide-vue-next";
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator, useForwardPropsEmits } from 'reka-ui';
import { Check } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
modelValue: { type: [Boolean, String], required: false },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select", "update:modelValue"]);
const props = defineProps({
modelValue: { type: [Boolean, String], required: false },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(['select', 'update:modelValue']);
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
">
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -1,66 +1,61 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuContent, DropdownMenuPortal, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
defineOptions({
inheritAttrs: false,
});
defineOptions({
inheritAttrs: false
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"closeAutoFocus",
]);
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits([
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'closeAutoFocus'
]);
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class,
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class
)
">
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -1,14 +1,14 @@
<script setup>
import { DropdownMenuGroup } from "reka-ui";
import { DropdownMenuGroup } from 'reka-ui';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
</script>
<template>
<DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
<slot />
</DropdownMenuGroup>
<DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -1,36 +1,35 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuItem, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuItem, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
variant: { type: String, required: false, default: "default" },
});
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
variant: { type: String, required: false, default: 'default' }
});
const delegatedProps = reactiveOmit(props, "inset", "variant", "class");
const delegatedProps = reactiveOmit(props, 'inset', 'variant', 'class');
const forwardedProps = useForwardProps(delegatedProps);
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<slot />
</DropdownMenuItem>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
">
<slot />
</DropdownMenuItem>
</template>

View File

@@ -1,28 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuLabel, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuLabel, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'inset');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="
cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)
"
>
<slot />
</DropdownMenuLabel>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)">
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -1,21 +1,18 @@
<script setup>
import { DropdownMenuRadioGroup, useForwardPropsEmits } from "reka-ui";
import { DropdownMenuRadioGroup, useForwardPropsEmits } from 'reka-ui';
const props = defineProps({
modelValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
const emits = defineEmits(['update:modelValue']);
const forwarded = useForwardPropsEmits(props, emits);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
<DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group" v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>

View File

@@ -1,49 +1,42 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Circle } from "lucide-vue-next";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuItemIndicator, DropdownMenuRadioItem, useForwardPropsEmits } from 'reka-ui';
import { Circle } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
value: { type: String, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const props = defineProps({
value: { type: String, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(["select"]);
const emits = defineEmits(['select']);
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
">
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View File

@@ -1,21 +1,20 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuSeparator } from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuSeparator } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)" />
</template>

View File

@@ -1,18 +1,15 @@
<script setup>
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false },
});
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="
cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)
"
>
<slot />
</span>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)">
<slot />
</span>
</template>

View File

@@ -1,21 +1,17 @@
<script setup>
import { DropdownMenuSub, useForwardPropsEmits } from "reka-ui";
import { DropdownMenuSub, useForwardPropsEmits } from 'reka-ui';
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false }
});
const emits = defineEmits(['update:open']);
const forwarded = useForwardPropsEmits(props, emits);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub
v-slot="slotProps"
data-slot="dropdown-menu-sub"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DropdownMenuSub>
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot v-bind="slotProps" />
</DropdownMenuSub>
</template>

View File

@@ -1,56 +1,55 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuSubContent, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuSubContent, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"entryFocus",
"openAutoFocus",
"closeAutoFocus",
]);
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits([
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'entryFocus',
'openAutoFocus',
'closeAutoFocus'
]);
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class,
)
"
>
<slot />
</DropdownMenuSubContent>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class
)
">
<slot />
</DropdownMenuSubContent>
</template>

View File

@@ -1,34 +1,33 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next";
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { DropdownMenuSubTrigger, useForwardProps } from 'reka-ui';
import { ChevronRight } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'inset');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)
"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class
)
">
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -1,20 +1,17 @@
<script setup>
import { DropdownMenuTrigger, useForwardProps } from "reka-ui";
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui';
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
const forwardedProps = useForwardProps(props);
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
<DropdownMenuTrigger data-slot="dropdown-menu-trigger" v-bind="forwardedProps">
<slot />
</DropdownMenuTrigger>
</template>

View File

@@ -1,16 +1,16 @@
export { default as DropdownMenu } from "./DropdownMenu.vue";
export { default as DropdownMenu } from './DropdownMenu.vue';
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
export { DropdownMenuPortal } from "reka-ui";
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';
export { default as DropdownMenuContent } from './DropdownMenuContent.vue';
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';
export { default as DropdownMenuItem } from './DropdownMenuItem.vue';
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';
export { default as DropdownMenuSub } from './DropdownMenuSub.vue';
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';
export { DropdownMenuPortal } from 'reka-ui';

View File

@@ -1,33 +1,32 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { PaginationRoot, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import { PaginationRoot, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
page: { type: Number, required: false },
defaultPage: { type: Number, required: false },
itemsPerPage: { type: Number, required: true },
total: { type: Number, required: false },
siblingCount: { type: Number, required: false },
disabled: { type: Boolean, required: false },
showEdges: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:page"]);
const props = defineProps({
page: { type: Number, required: false },
defaultPage: { type: Number, required: false },
itemsPerPage: { type: Number, required: true },
total: { type: Number, required: false },
siblingCount: { type: Number, required: false },
disabled: { type: Boolean, required: false },
showEdges: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(['update:page']);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<PaginationRoot
v-slot="slotProps"
data-slot="pagination"
v-bind="forwarded"
:class="cn('mx-auto flex w-full justify-center', props.class)"
>
<slot v-bind="slotProps" />
</PaginationRoot>
<PaginationRoot
v-slot="slotProps"
data-slot="pagination"
v-bind="forwarded"
:class="cn('flex justify-center text-[13px]', props.class)">
<slot v-bind="slotProps" />
</PaginationRoot>
</template>

View File

@@ -1,24 +1,23 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { PaginationList } from "reka-ui";
import { cn } from "@/lib/utils";
import { PaginationList } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<PaginationList
v-slot="slotProps"
data-slot="pagination-content"
v-bind="delegatedProps"
:class="cn('flex flex-row items-center gap-1', props.class)"
>
<slot v-bind="slotProps" />
</PaginationList>
<PaginationList
v-slot="slotProps"
data-slot="pagination-content"
v-bind="delegatedProps"
:class="cn('flex flex-row items-center gap-1', props.class)">
<slot v-bind="slotProps" />
</PaginationList>
</template>

View File

@@ -1,27 +1,26 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { MoreHorizontal } from "lucide-vue-next";
import { PaginationEllipsis } from "reka-ui";
import { cn } from "@/lib/utils";
import { MoreHorizontal } from 'lucide-vue-next';
import { PaginationEllipsis } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<PaginationEllipsis
data-slot="pagination-ellipsis"
v-bind="delegatedProps"
:class="cn('flex size-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="size-4" />
<span class="sr-only">More pages</span>
</slot>
</PaginationEllipsis>
<PaginationEllipsis
data-slot="pagination-ellipsis"
v-bind="delegatedProps"
:class="cn('flex size-9 items-center justify-center text-[13px]', props.class)">
<slot>
<MoreHorizontal class="size-4" />
<span class="sr-only">More pages</span>
</slot>
</PaginationEllipsis>
</template>

View File

@@ -1,36 +1,29 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronLeftIcon } from "lucide-vue-next";
import { PaginationFirst, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { PaginationFirst, useForwardProps } from 'reka-ui';
import { ChevronLeftIcon } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: "default" },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: 'default' },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "size");
const forwarded = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'size');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<PaginationFirst
data-slot="pagination-first"
:class="
cn(
buttonVariants({ variant: 'ghost', size }),
'gap-1 px-2.5 sm:pr-2.5',
props.class,
)
"
v-bind="forwarded"
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">First</span>
</slot>
</PaginationFirst>
<PaginationFirst
data-slot="pagination-first"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'text-[13px] gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded">
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">First</span>
</slot>
</PaginationFirst>
</template>

View File

@@ -1,35 +1,35 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { PaginationListItem } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { PaginationListItem } from 'reka-ui';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
value: { type: Number, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: "icon" },
class: { type: null, required: false },
isActive: { type: Boolean, required: false },
});
const props = defineProps({
value: { type: Number, required: true },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: 'icon' },
class: { type: null, required: false },
isActive: { type: Boolean, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "size", "isActive");
const delegatedProps = reactiveOmit(props, 'class', 'size', 'isActive');
</script>
<template>
<PaginationListItem
data-slot="pagination-item"
v-bind="delegatedProps"
:class="
cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
props.class,
)
"
>
<slot />
</PaginationListItem>
<PaginationListItem
data-slot="pagination-item"
v-bind="delegatedProps"
:class="
cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size
}),
'text-[13px]',
props.class
)
">
<slot />
</PaginationListItem>
</template>

View File

@@ -1,36 +1,29 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next";
import { PaginationLast, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { PaginationLast, useForwardProps } from 'reka-ui';
import { ChevronRightIcon } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: "default" },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: 'default' },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "size");
const forwarded = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'size');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<PaginationLast
data-slot="pagination-last"
:class="
cn(
buttonVariants({ variant: 'ghost', size }),
'gap-1 px-2.5 sm:pr-2.5',
props.class,
)
"
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Last</span>
<ChevronRightIcon />
</slot>
</PaginationLast>
<PaginationLast
data-slot="pagination-last"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'text-[13px] gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded">
<slot>
<span class="hidden sm:block">Last</span>
<ChevronRightIcon />
</slot>
</PaginationLast>
</template>

View File

@@ -1,36 +1,29 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next";
import { PaginationNext, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { PaginationNext, useForwardProps } from 'reka-ui';
import { ChevronRightIcon } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: "default" },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: 'default' },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "size");
const forwarded = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'size');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<PaginationNext
data-slot="pagination-next"
:class="
cn(
buttonVariants({ variant: 'ghost', size }),
'gap-1 px-2.5 sm:pr-2.5',
props.class,
)
"
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Next</span>
<ChevronRightIcon />
</slot>
</PaginationNext>
<PaginationNext
data-slot="pagination-next"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'text-[13px] gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded">
<slot>
<span class="hidden sm:block">Next</span>
<ChevronRightIcon />
</slot>
</PaginationNext>
</template>

View File

@@ -1,36 +1,29 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronLeftIcon } from "lucide-vue-next";
import { PaginationPrev, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { PaginationPrev, useForwardProps } from 'reka-ui';
import { ChevronLeftIcon } from 'lucide-vue-next';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: "default" },
class: { type: null, required: false },
});
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
size: { type: null, required: false, default: 'default' },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, "class", "size");
const forwarded = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, 'class', 'size');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<PaginationPrev
data-slot="pagination-previous"
:class="
cn(
buttonVariants({ variant: 'ghost', size }),
'gap-1 px-2.5 sm:pr-2.5',
props.class,
)
"
v-bind="forwarded"
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">Previous</span>
</slot>
</PaginationPrev>
<PaginationPrev
data-slot="pagination-previous"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'text-[13px] gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded">
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">Previous</span>
</slot>
</PaginationPrev>
</template>

View File

@@ -1,8 +1,8 @@
export { default as Pagination } from "./Pagination.vue";
export { default as PaginationContent } from "./PaginationContent.vue";
export { default as PaginationEllipsis } from "./PaginationEllipsis.vue";
export { default as PaginationFirst } from "./PaginationFirst.vue";
export { default as PaginationItem } from "./PaginationItem.vue";
export { default as PaginationLast } from "./PaginationLast.vue";
export { default as PaginationNext } from "./PaginationNext.vue";
export { default as PaginationPrevious } from "./PaginationPrevious.vue";
export { default as Pagination } from './Pagination.vue';
export { default as PaginationContent } from './PaginationContent.vue';
export { default as PaginationEllipsis } from './PaginationEllipsis.vue';
export { default as PaginationFirst } from './PaginationFirst.vue';
export { default as PaginationItem } from './PaginationItem.vue';
export { default as PaginationLast } from './PaginationLast.vue';
export { default as PaginationNext } from './PaginationNext.vue';
export { default as PaginationPrevious } from './PaginationPrevious.vue';

View File

@@ -0,0 +1,26 @@
<script setup>
import { SelectRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
defaultValue: { type: null, required: false },
modelValue: { type: null, required: false },
by: { type: [String, Function], required: false },
dir: { type: String, required: false },
multiple: { type: Boolean, required: false },
autocomplete: { type: String, required: false },
disabled: { type: Boolean, required: false },
name: { type: String, required: false },
required: { type: Boolean, required: false },
});
const emits = defineEmits(["update:modelValue", "update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded">
<slot v-bind="slotProps" />
</SelectRoot>
</template>

View File

@@ -0,0 +1,82 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import {
SelectContent,
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { SelectScrollDownButton, SelectScrollUpButton } from ".";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
position: { type: String, required: false, default: "popper" },
bodyLock: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"closeAutoFocus",
"escapeKeyDown",
"pointerDownOutside",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<SelectPortal>
<SelectContent
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"
>
<SelectScrollUpButton />
<SelectViewport
:class="
cn(
'p-1',
position === 'popper' &&
'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1',
)
"
>
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { SelectGroup } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<SelectGroup data-slot="select-group" v-bind="props">
<slot />
</SelectGroup>
</template>

View File

@@ -0,0 +1,49 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Check } from "lucide-vue-next";
import {
SelectItem,
SelectItemIndicator,
SelectItemText,
useForwardProps,
} from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
value: { type: null, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<SelectItem
data-slot="select-item"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
props.class,
)
"
>
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</SelectItemIndicator>
</span>
<SelectItemText>
<slot />
</SelectItemText>
</SelectItem>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { SelectItemText } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<SelectItemText data-slot="select-item-text" v-bind="props">
<slot />
</SelectItemText>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { SelectLabel } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
</script>
<template>
<SelectLabel
data-slot="select-label"
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
>
<slot />
</SelectLabel>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronDown } from "lucide-vue-next";
import { SelectScrollDownButton, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<SelectScrollDownButton
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="
cn('flex cursor-default items-center justify-center py-1', props.class)
"
>
<slot>
<ChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronUp } from "lucide-vue-next";
import { SelectScrollUpButton, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<SelectScrollUpButton
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="
cn('flex cursor-default items-center justify-center py-1', props.class)
"
>
<slot>
<ChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { SelectSeparator } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<SelectSeparator
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronDown } from "lucide-vue-next";
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
size: { type: String, required: false, default: "default" },
});
const delegatedProps = reactiveOmit(props, "class", "size");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<SelectTrigger
data-slot="select-trigger"
:data-size="size"
v-bind="forwardedProps"
:class="
cn(
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<slot />
<SelectIcon as-child>
<ChevronDown class="size-4 opacity-50" />
</SelectIcon>
</SelectTrigger>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { SelectValue } from "reka-ui";
const props = defineProps({
placeholder: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script>
<template>
<SelectValue data-slot="select-value" v-bind="props">
<slot />
</SelectValue>
</template>

View File

@@ -0,0 +1,11 @@
export { default as Select } from "./Select.vue";
export { default as SelectContent } from "./SelectContent.vue";
export { default as SelectGroup } from "./SelectGroup.vue";
export { default as SelectItem } from "./SelectItem.vue";
export { default as SelectItemText } from "./SelectItemText.vue";
export { default as SelectLabel } from "./SelectLabel.vue";
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue";
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue";
export { default as SelectSeparator } from "./SelectSeparator.vue";
export { default as SelectTrigger } from "./SelectTrigger.vue";
export { default as SelectValue } from "./SelectValue.vue";

View File

@@ -8,7 +8,7 @@
<template>
<div data-slot="table-container" class="relative w-full overflow-auto">
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
<table data-slot="table" :class="cn('w-full caption-bottom text-[13px]', props.class)">
<slot />
</table>
</div>

View File

@@ -1,22 +1,22 @@
<script setup>
import { TooltipRoot, useForwardPropsEmits } from "reka-ui";
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
delayDuration: { type: Number, required: false },
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
delayDuration: { type: Number, required: false },
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false }
});
const emits = defineEmits(['update:open']);
const forwarded = useForwardPropsEmits(props, emits);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<TooltipRoot v-slot="slotProps" data-slot="tooltip" v-bind="forwarded">
<slot v-bind="slotProps" />
</TooltipRoot>
<TooltipRoot v-slot="slotProps" data-slot="tooltip" v-bind="forwarded">
<slot v-bind="slotProps" />
</TooltipRoot>
</template>

View File

@@ -1,60 +1,53 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import {
TooltipArrow,
TooltipContent,
TooltipPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
defineOptions({
inheritAttrs: false,
});
defineOptions({
inheritAttrs: false
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
ariaLabel: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
class: { type: null, required: false },
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
ariaLabel: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(["escapeKeyDown", "pointerDownOutside"]);
const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside']);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<TooltipPortal>
<TooltipContent
data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
props.class,
)
"
>
<slot />
<TooltipPortal>
<TooltipContent
data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
props.class
)
">
<slot />
<TooltipArrow
class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]"
/>
</TooltipContent>
</TooltipPortal>
<TooltipArrow
class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipContent>
</TooltipPortal>
</template>

View File

@@ -1,18 +1,18 @@
<script setup>
import { TooltipProvider } from "reka-ui";
import { TooltipProvider } from 'reka-ui';
const props = defineProps({
delayDuration: { type: Number, required: false, default: 0 },
skipDelayDuration: { type: Number, required: false },
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false },
});
const props = defineProps({
delayDuration: { type: Number, required: false, default: 0 },
skipDelayDuration: { type: Number, required: false },
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false }
});
</script>
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>

View File

@@ -1,15 +1,15 @@
<script setup>
import { TooltipTrigger } from "reka-ui";
import { TooltipTrigger } from 'reka-ui';
const props = defineProps({
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const props = defineProps({
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false }
});
</script>
<template>
<TooltipTrigger data-slot="tooltip-trigger" v-bind="props">
<slot />
</TooltipTrigger>
<TooltipTrigger data-slot="tooltip-trigger" v-bind="props">
<slot />
</TooltipTrigger>
</template>

View File

@@ -1,4 +1,4 @@
export { default as Tooltip } from "./Tooltip.vue";
export { default as TooltipContent } from "./TooltipContent.vue";
export { default as TooltipProvider } from "./TooltipProvider.vue";
export { default as TooltipTrigger } from "./TooltipTrigger.vue";
export { default as Tooltip } from './Tooltip.vue';
export { default as TooltipContent } from './TooltipContent.vue';
export { default as TooltipProvider } from './TooltipProvider.vue';
export { default as TooltipTrigger } from './TooltipTrigger.vue';

View File

@@ -0,0 +1,55 @@
import { computed, onMounted, onUnmounted, ref } from 'vue';
export function useDataTableScrollHeight(containerRef, options = {}) {
const offset = options.offset ?? 127;
const toolbarHeight = options.toolbarHeight ?? 0;
const paginationHeight = options.paginationHeight ?? 0;
const maxHeight = ref(0);
let resizeObserver;
const recalc = () => {
const containerEl = containerRef?.value;
if (!containerEl) {
return;
}
const available =
containerEl.clientHeight -
offset -
toolbarHeight -
paginationHeight;
maxHeight.value = Math.max(0, available);
};
onMounted(() => {
recalc();
resizeObserver = new ResizeObserver(() => {
recalc();
});
if (containerRef?.value) {
resizeObserver.observe(containerRef.value);
}
});
onUnmounted(() => {
resizeObserver?.disconnect();
});
const tableStyle = computed(() => {
if (!Number.isFinite(maxHeight.value) || maxHeight.value <= 0) {
return undefined;
}
return {
maxHeight: `${maxHeight.value}px`
};
});
return {
tableStyle
};
}

View File

@@ -2227,6 +2227,9 @@
}
},
"table": {
"pagination": {
"rows_per_page": "Rows per page"
},
"feed": {
"date": "Date",
"type": "Type",

View File

@@ -1,339 +1,148 @@
<template>
<div class="x-container feed" ref="feedRef">
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
<el-switch
v-model="feedTable.vip"
active-color="var(--el-color-success)"
@change="feedTableLookup"></el-switch>
</el-tooltip>
</div>
<el-select
v-model="feedTable.filter"
multiple
clearable
style="flex: 1"
:placeholder="t('view.feed.filter_placeholder')"
@change="feedTableLookup">
<el-option
v-for="type in ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio']"
:key="type"
:label="t('view.feed.filters.' + type)"
:value="type"></el-option>
</el-select>
<el-input
v-model="feedTable.search"
:placeholder="t('view.feed.search_placeholder')"
clearable
style="flex: 0.4; margin-left: 10px"
@keyup.enter="feedTableLookup"
@change="feedTableLookup"></el-input>
</div>
<DataTable v-bind="feedTable" :data="feedDisplayData">
<el-table-column type="expand" width="30">
<template #default="scope">
<div style="position: relative; font-size: 14px" class="pl-5">
<template v-if="scope.row.type === 'GPS'">
<Location
v-if="scope.row.previousLocation"
:location="scope.row.previousLocation"
style="display: inline-block" />
<el-tag type="info" effect="plain" size="small" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
<br />
<span style="margin-right: 5px"> </span>
<Location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
</template>
<template v-else-if="scope.row.type === 'Offline'">
<template v-if="scope.row.location">
<Location
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
<el-tag type="info" effect="plain" size="small" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
</template>
</template>
<template v-else-if="scope.row.type === 'Online'">
<Location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
</template>
<template v-else-if="scope.row.type === 'Avatar'">
<div style="display: flex; align-items: center">
<div style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.previousCurrentAvatarThumbnailImageUrl">
<img
:src="scope.row.previousCurrentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px"
loading="lazy" />
<br />
<AvatarInfo
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.previousOwnerId"
:hintavatarname="scope.row.previousAvatarName"
:avatartags="scope.row.previousCurrentAvatarTags" />
</template>
</div>
<span style="position: relative; margin: 0 10px">
{{ ' → ' }}
</span>
<div style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.currentAvatarThumbnailImageUrl">
<img
:src="scope.row.currentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px"
loading="lazy" />
<br />
<AvatarInfo
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags" />
</template>
</div>
</div>
</template>
<template v-else-if="scope.row.type === 'Status'">
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
<br />
<span> </span>
<i class="x-user-status" :class="statusClass(scope.row.status)" style="margin: 0 5px"></i>
<span v-text="scope.row.statusDescription"></span>
</template>
<template v-else-if="scope.row.type === 'Bio'">
<pre
style="font-family: inherit; font-size: 12px; white-space: pre-wrap; line-height: 22px"
v-html="formatDifference(scope.row.previousBio, scope.row.bio)"></pre>
</template>
<DataTableLayout
:table="table"
:loading="feedTable.loading"
:table-style="tableHeightStyle"
:page-sizes="pageSizes"
:total-items="totalItems"
:on-page-size-change="handlePageSizeChange">
<template #toolbar>
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
<el-switch
v-model="feedTable.vip"
active-color="var(--el-color-success)"
@change="feedTableLookup"></el-switch>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.date')" prop="created_at" width="140">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.type')" prop="type" width="130">
<template #default="scope">
<el-tag type="info" effect="plain" size="small">{{
t('view.feed.filters.' + scope.row.type)
}}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.user')" prop="displayName" width="190">
<template #default="scope">
<span
class="x-link table-user"
style="padding-right: 10px"
@click="showUserDialog(scope.row.userId)"
v-text="scope.row.displayName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.detail')">
<template #default="scope">
<template v-if="scope.row.type === 'GPS'">
<Location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
</template>
<template v-else-if="scope.row.type === 'Offline' || scope.row.type === 'Online'">
<Location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
</template>
<template v-else-if="scope.row.type === 'Status'">
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
<span class="mx-2"> </span>
<i class="x-user-status" :class="statusClass(scope.row.status)"></i>
</template>
<template v-else>
<i class="x-user-status mr-2" :class="statusClass(scope.row.status)"></i>
<span v-text="scope.row.statusDescription"></span>
</template>
</template>
<template v-else-if="scope.row.type === 'Avatar'">
<AvatarInfo
:imageurl="scope.row.currentAvatarImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags" />
</template>
<template v-else-if="scope.row.type === 'Bio'">
<span v-text="scope.row.bio"></span>
</template>
</template>
</el-table-column>
</DataTable>
<el-select
v-model="feedTable.filter"
multiple
clearable
style="flex: 1"
:placeholder="t('view.feed.filter_placeholder')"
@change="feedTableLookup">
<el-option
v-for="type in ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio']"
:key="type"
:label="t('view.feed.filters.' + type)"
:value="type"></el-option>
</el-select>
<el-input
v-model="feedTable.search"
:placeholder="t('view.feed.search_placeholder')"
clearable
style="flex: 0.4; margin-left: 10px"
@keyup.enter="feedTableLookup"
@change="feedTableLookup"></el-input>
</div>
</template>
</DataTableLayout>
</div>
</template>
<script setup>
import { computed } from 'vue';
import {
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable
} from '@tanstack/vue-table';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { formatDateFilter, statusClass, timeToText } from '../../shared/utils';
import { useFeedStore, useUserStore } from '../../stores';
import { useTableHeight } from '../../composables/useTableHeight';
import { useAppearanceSettingsStore, useFeedStore, useVrcxStore } from '../../stores';
import { DataTableLayout } from '../../components/ui/data-table';
import { columns as baseColumns } from './columns.jsx';
import { useDataTableScrollHeight } from '../../composables/useDataTableScrollHeight';
import { valueUpdater } from '../../components/ui/table/utils';
const { showUserDialog } = useUserStore();
const { feedTable } = storeToRefs(useFeedStore());
const { feedTableLookup } = useFeedStore();
const appearanceSettingsStore = useAppearanceSettingsStore();
const vrcxStore = useVrcxStore();
const feedDisplayData = computed(() => feedTable.value.data.slice().reverse());
const { t } = useI18n();
const { containerRef: feedRef } = useTableHeight(feedTable);
const feedRef = ref(null);
/**
* Function that format the differences between two strings with HTML tags
* markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined.
* @param {*} s1
* @param {*} s2
* @param {*} markerStartTag
* @param {*} markerEndTag
* @returns An array that contains both the string 1 and string 2, which the differences are formatted with HTML tags
*/
// TODO: simplify
const { tableStyle: tableHeightStyle } = useDataTableScrollHeight(feedRef, {
offset: 30,
toolbarHeight: 54,
paginationHeight: 52
});
//function getWordDifferences
function formatDifference(
oldString,
newString,
markerAddition = '<span class="x-text-added">{{text}}</span>',
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
) {
[oldString, newString] = [oldString, newString].map((s) =>
s
.replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/"/g, '&quot;')
.replaceAll(/'/g, '&#039;')
.replaceAll(/\n/g, '<br>')
);
const pageSizes = computed(() => appearanceSettingsStore.tablePageSizes);
const pageSize = computed(() =>
feedTable.value.pageSizeLinked ? appearanceSettingsStore.tablePageSize : feedTable.value.pageSize
);
const oldWords = oldString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
const newWords = newString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
const sorting = ref([]);
const expanded = ref({});
const pagination = ref({
pageIndex: 0,
pageSize: pageSize.value
});
function findLongestMatch(oldStart, oldEnd, newStart, newEnd) {
let bestOldStart = oldStart;
let bestNewStart = newStart;
let bestSize = 0;
const lookup = new Map();
for (let i = oldStart; i < oldEnd; i++) {
const word = oldWords[i];
if (!lookup.has(word)) lookup.set(word, []);
lookup.get(word).push(i);
const table = useVueTable({
data: feedDisplayData,
columns: baseColumns,
getRowId: (row) => `${row.type}:${row.rowId ?? row.uid}:${row.created_at ?? ''}`,
getRowCanExpand: () => true,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: (updaterOrValue) => valueUpdater(updaterOrValue, sorting),
onPaginationChange: (updaterOrValue) => valueUpdater(updaterOrValue, pagination),
onExpandedChange: (updaterOrValue) => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() {
return sorting.value;
},
get pagination() {
return pagination.value;
},
get expanded() {
return expanded.value;
}
for (let j = newStart; j < newEnd; j++) {
const word = newWords[j];
if (!lookup.has(word)) continue;
for (const i of lookup.get(word)) {
let size = 0;
while (i + size < oldEnd && j + size < newEnd && oldWords[i + size] === newWords[j + size]) {
size++;
}
if (size > bestSize) {
bestOldStart = i;
bestNewStart = j;
bestSize = size;
}
}
}
return {
oldStart: bestOldStart,
newStart: bestNewStart,
size: bestSize
};
}
});
function buildDiff(oldStart, oldEnd, newStart, newEnd) {
const result = [];
const match = findLongestMatch(oldStart, oldEnd, newStart, newEnd);
const totalItems = computed(() => {
const length = table.getFilteredRowModel().rows.length;
const max = vrcxStore.maxTableSize;
return length > max && length < max + 51 ? max : length;
});
if (match.size > 0) {
// Handle differences before the match
if (oldStart < match.oldStart || newStart < match.newStart) {
result.push(...buildDiff(oldStart, match.oldStart, newStart, match.newStart));
}
// Add the matched words
result.push(oldWords.slice(match.oldStart, match.oldStart + match.size).join(' '));
// Handle differences after the match
if (match.oldStart + match.size < oldEnd || match.newStart + match.size < newEnd) {
result.push(...buildDiff(match.oldStart + match.size, oldEnd, match.newStart + match.size, newEnd));
}
} else {
function build(words, start, end, pattern) {
let r = [];
let ts = words
.slice(start, end)
.filter((w) => w.length > 0)
.join(' ')
.split('<br>');
for (let i = 0; i < ts.length; i++) {
if (i > 0) r.push('<br>');
if (ts[i].length < 1) continue;
r.push(pattern.replace('{{text}}', ts[i]));
}
return r;
}
// Add deletions
if (oldStart < oldEnd) result.push(...build(oldWords, oldStart, oldEnd, markerDeletion));
// Add insertions
if (newStart < newEnd) result.push(...build(newWords, newStart, newEnd, markerAddition));
}
return result;
const handlePageSizeChange = (size) => {
if (feedTable.value.pageSizeLinked) {
appearanceSettingsStore.setTablePageSize(size);
} else {
feedTable.value.pageSize = size;
}
};
return buildDiff(0, oldWords.length, 0, newWords.length)
.join(' ')
.replace(/<br>[ ]+<br>/g, '<br><br>')
.replace(/<br> /g, '<br>');
}
watch(pageSize, (size) => {
if (pagination.value.pageSize === size) {
return;
}
pagination.value = {
...pagination.value,
pageIndex: 0,
pageSize: size
};
table.setPageSize(size);
});
</script>
<style scoped>

View File

@@ -1,22 +1,231 @@
import { ElTag } from 'element-plus';
import { resolveComponent } from 'vue';
import AvatarInfo from '../../components/AvatarInfo.vue';
import Location from '../../components/Location.vue';
import { Badge } from '../../components/ui/badge';
import { Button } from '../../components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '../../components/ui/tooltip';
import { formatDateFilter, statusClass } from '../../shared/utils';
import {
ArrowDown,
ArrowRight,
ArrowUpDown,
ChevronDown,
ChevronRight
} from 'lucide-vue-next';
import { formatDateFilter, statusClass, timeToText } from '../../shared/utils';
import { i18n } from '../../plugin';
import { useUserStore } from '../../stores';
const { t } = i18n.global;
const expandedRow = ({ row }) => {
const original = row.original;
const type = original.type;
if (type === 'GPS') {
return (
<div class="pl-5 text-sm">
{original.previousLocation ? (
<>
<Location
location={original.previousLocation}
class="inline-block"
/>
<Badge variant="secondary" class="ml-1 w-fit">
{timeToText(original.time)}
</Badge>
<br />
<span>
<ArrowDown />
</span>
</>
) : null}
{original.location ? (
<Location
location={original.location}
hint={original.worldName}
grouphint={original.groupName}
/>
) : null}
</div>
);
}
if (type === 'Offline') {
return original.location ? (
<div class="pl-5 text-sm">
<Location
location={original.location}
hint={original.worldName}
grouphint={original.groupName}
/>
<Badge variant="secondary" class="ml-1 w-fit">
{timeToText(original.time)}
</Badge>
</div>
) : null;
}
if (type === 'Online') {
return original.location ? (
<div class="pl-5 text-sm">
<Location
location={original.location}
hint={original.worldName}
grouphint={original.groupName}
/>
</div>
) : null;
}
if (type === 'Avatar') {
return (
<div class="pl-5 text-sm">
<div class="flex items-center">
<div class="inline-block align-top w-[160px]">
{original.previousCurrentAvatarThumbnailImageUrl ? (
<>
<img
src={
original.previousCurrentAvatarThumbnailImageUrl
}
class="x-link h-[120px] w-[160px] rounded"
loading="lazy"
/>
<br />
<AvatarInfo
imageurl={
original.previousCurrentAvatarThumbnailImageUrl
}
userid={original.userId}
hintownerid={original.previousOwnerId}
hintavatarname={original.previousAvatarName}
avatartags={
original.previousCurrentAvatarTags
}
/>
</>
) : null}
</div>
<span class="mx-2">
<ArrowRight />
</span>
<div class="inline-block align-top w-40">
{original.currentAvatarThumbnailImageUrl ? (
<>
<img
src={
original.currentAvatarThumbnailImageUrl
}
class="x-link h-30 w-40 rounded"
loading="lazy"
/>
<br />
<AvatarInfo
imageurl={
original.currentAvatarThumbnailImageUrl
}
userid={original.userId}
hintownerid={original.ownerId}
hintavatarname={original.avatarName}
avatartags={original.currentAvatarTags}
/>
</>
) : null}
</div>
</div>
</div>
);
}
if (type === 'Status') {
return (
<div class="flex items-center pl-5 text-sm">
<i
class={[
'x-user-status',
statusClass(original.previousStatus)
]}
></i>
<span class="ml-1">{original.previousStatusDescription}</span>
<br />
<span class="mx-2">
<ArrowRight />
</span>
<i
class={[
'x-user-status',
statusClass(original.status),
'mx-1'
]}
></i>
<span>{original.statusDescription}</span>
</div>
);
}
if (type === 'Bio') {
return (
<div class="pl-5 text-sm">
<pre
class="text-xs leading-5.5 whitespace-pre-wrap font-[inherit]"
innerHTML={formatDifference(
original.previousBio,
original.bio
)}
></pre>
</div>
);
}
return null;
};
export const columns = [
{
id: 'expander',
header: () => null,
enableSorting: false,
meta: {
class: 'w-[20px]',
expandedRow
},
cell: ({ row }) => {
if (!row.getCanExpand()) {
return null;
}
return (
<button
type="button"
class="inline-flex h-6 items-center justify-center text-xs text-muted-foreground hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
row.toggleExpanded();
}}
>
{row.getIsExpanded() ? <ChevronDown /> : <ChevronRight />}
</button>
);
}
},
{
accessorKey: 'created_at',
header: () => t('table.feed.date'),
meta: {
class: 'w-[140px]'
},
header: ({ column }) => (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
{t('table.feed.date')}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => {
const createdAt = row.getValue('created_at');
const shortText = formatDateFilter(createdAt, 'short');
@@ -38,26 +247,33 @@ export const columns = [
},
{
accessorKey: 'type',
meta: {
class: 'w-[130px]'
},
header: () => t('table.feed.type'),
cell: ({ row }) => {
const type = row.getValue('type');
return (
<ElTag type="info" effect="plain" size="small">
{t(`view.feed.filters.${type}`)}
</ElTag>
<div>
<Badge variant="outline" class="text-muted-foreground">
{t(`view.feed.filters.${type}`)}
</Badge>
</div>
);
}
},
{
accessorKey: 'displayName',
meta: {
class: 'w-[190px]'
},
header: () => t('table.feed.user'),
cell: ({ row }) => {
const { showUserDialog } = useUserStore();
const original = row.original;
return (
<span
class="x-link table-user"
style="padding-right: 10px"
class="x-link pr-2.5"
onClick={() => showUserDialog(original.userId)}
>
{original.displayName}
@@ -68,12 +284,10 @@ export const columns = [
{
id: 'detail',
header: () => t('table.feed.detail'),
enableSorting: false,
cell: ({ row }) => {
const original = row.original;
const type = original.type;
const Location = resolveComponent('Location');
const AvatarInfo = resolveComponent('AvatarInfo');
if (type === 'GPS') {
return original.location ? (
<Location
@@ -100,21 +314,23 @@ export const columns = [
original.previousStatusDescription
) {
return (
<span>
<div class="flex items-center">
<i
class={[
'x-user-status',
statusClass(original.previousStatus)
]}
></i>
<span class="mx-2"> Ўъ </span>
<span class="mx-2">
<ArrowRight />
</span>
<i
class={[
'x-user-status',
statusClass(original.status)
]}
></i>
</span>
</div>
);
}
@@ -152,3 +368,136 @@ export const columns = [
}
}
];
function formatDifference(
oldString,
newString,
markerAddition = '<span class="x-text-added">{{text}}</span>',
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
) {
[oldString, newString] = [oldString, newString].map((s) =>
s
.replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/"/g, '&quot;')
.replaceAll(/'/g, '&#039;')
.replaceAll(/\n/g, '<br>')
);
const oldWords = oldString
.split(/\s+/)
.flatMap((word) => word.split(/(<br>)/));
const newWords = newString
.split(/\s+/)
.flatMap((word) => word.split(/(<br>)/));
function findLongestMatch(oldStart, oldEnd, newStart, newEnd) {
let bestOldStart = oldStart;
let bestNewStart = newStart;
let bestSize = 0;
const lookup = new Map();
for (let i = oldStart; i < oldEnd; i++) {
const word = oldWords[i];
if (!lookup.has(word)) lookup.set(word, []);
lookup.get(word).push(i);
}
for (let j = newStart; j < newEnd; j++) {
const word = newWords[j];
if (!lookup.has(word)) continue;
for (const i of lookup.get(word)) {
let size = 0;
while (
i + size < oldEnd &&
j + size < newEnd &&
oldWords[i + size] === newWords[j + size]
) {
size++;
}
if (size > bestSize) {
bestOldStart = i;
bestNewStart = j;
bestSize = size;
}
}
}
return {
oldStart: bestOldStart,
newStart: bestNewStart,
size: bestSize
};
}
function buildDiff(oldStart, oldEnd, newStart, newEnd) {
const result = [];
const match = findLongestMatch(oldStart, oldEnd, newStart, newEnd);
if (match.size > 0) {
if (oldStart < match.oldStart || newStart < match.newStart) {
result.push(
...buildDiff(
oldStart,
match.oldStart,
newStart,
match.newStart
)
);
}
result.push(
oldWords
.slice(match.oldStart, match.oldStart + match.size)
.join(' ')
);
if (
match.oldStart + match.size < oldEnd ||
match.newStart + match.size < newEnd
) {
result.push(
...buildDiff(
match.oldStart + match.size,
oldEnd,
match.newStart + match.size,
newEnd
)
);
}
} else {
function build(words, start, end, pattern) {
let r = [];
let ts = words
.slice(start, end)
.filter((w) => w.length > 0)
.join(' ')
.split('<br>');
for (let i = 0; i < ts.length; i++) {
if (i > 0) r.push('<br>');
if (ts[i].length < 1) continue;
r.push(pattern.replace('{{text}}', ts[i]));
}
return r;
}
if (oldStart < oldEnd)
result.push(
...build(oldWords, oldStart, oldEnd, markerDeletion)
);
if (newStart < newEnd)
result.push(
...build(newWords, newStart, newEnd, markerAddition)
);
}
return result;
}
return buildDiff(0, oldWords.length, 0, newWords.length)
.join(' ')
.replace(/<br>[ ]+<br>/g, '<br><br>')
.replace(/<br> /g, '<br>');
}