refactor: tailwindcss

This commit is contained in:
overtrue
2025-10-27 22:04:05 +08:00
parent 422f6fa763
commit 50cef5195b
148 changed files with 3885 additions and 26678 deletions
+14 -9
View File
@@ -1,5 +1,9 @@
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import Spinner from '@/components/ui/spinner/Spinner.vue'
import { AppCard, AppModal } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -106,7 +110,7 @@ async function submitForm() {
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label for="password-current">{{ t('Current Password') }}</Label>
<AppInput
<Input
id="password-current"
v-model="formModel.current_secret_key"
type="password"
@@ -119,7 +123,7 @@ async function submitForm() {
<div class="grid gap-2">
<Label for="password-new">{{ t('New Password') }}</Label>
<AppInput
<Input
id="password-new"
v-model="formModel.new_secret_key"
type="password"
@@ -132,7 +136,7 @@ async function submitForm() {
<div class="grid gap-2">
<Label for="password-new-confirm">{{ t('Confirm New Password') }}</Label>
<AppInput
<Input
id="password-new-confirm"
v-model="formModel.re_new_secret_key"
type="password"
@@ -147,12 +151,13 @@ async function submitForm() {
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">
<Button variant="outline" @click="closeModal()">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">
{{ t('Submit') }}
</AppButton>
</Button>
<Button variant="default" :disabled="submitting" @click="submitForm">
<Spinner v-if="submitting" class="size-4" />
<span>{{ t('Submit') }}</span>
</Button>
</div>
</template>
</AppModal>
+14 -9
View File
@@ -1,5 +1,9 @@
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import Spinner from '@/components/ui/spinner/Spinner.vue'
import { AppCard, AppModal, AppSwitch, AppTextarea } from '@/components/app'
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
import { Label } from '@/components/ui/label'
import dayjs from 'dayjs'
@@ -96,7 +100,7 @@ async function submitForm() {
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formModel.accesskey" disabled />
<Input v-model="formModel.accesskey" disabled />
</div>
<div class="grid gap-2">
@@ -111,7 +115,7 @@ async function submitForm() {
<div class="grid gap-2">
<Label>{{ t('Name') }}</Label>
<AppInput v-model="formModel.name" />
<Input v-model="formModel.name" />
</div>
<div class="grid gap-2">
@@ -119,7 +123,7 @@ async function submitForm() {
<AppTextarea v-model="formModel.description" :rows="3" />
</div>
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between rounded-md border p-3">
<span class="text-sm font-medium">{{ t('Status') }}</span>
<AppSwitch v-model="statusBoolean" />
</div>
@@ -127,12 +131,13 @@ async function submitForm() {
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">
<Button variant="outline" @click="closeModal()">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">
{{ t('Submit') }}
</AppButton>
</Button>
<Button variant="default" :disabled="submitting" @click="submitForm">
<Spinner v-if="submitting" class="size-4" />
<span>{{ t('Submit') }}</span>
</Button>
</div>
</template>
</AppModal>
+15 -10
View File
@@ -8,13 +8,13 @@
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label for="create-access-key">{{ t('Access Key') }}</Label>
<AppInput id="create-access-key" v-model="formModel.accessKey" autocomplete="off" />
<Input id="create-access-key" v-model="formModel.accessKey" autocomplete="off" />
<p v-if="errors.accessKey" class="text-sm text-destructive">{{ errors.accessKey }}</p>
</div>
<div class="grid gap-2">
<Label for="create-secret-key">{{ t('Secret Key') }}</Label>
<AppInput id="create-secret-key" v-model="formModel.secretKey" type="password" autocomplete="off" />
<Input id="create-secret-key" v-model="formModel.secretKey" type="password" autocomplete="off" />
<p v-if="errors.secretKey" class="text-sm text-destructive">{{ errors.secretKey }}</p>
</div>
@@ -31,7 +31,7 @@
<div class="grid gap-2">
<Label for="create-name">{{ t('Name') }}</Label>
<AppInput id="create-name" v-model="formModel.name" autocomplete="off" />
<Input id="create-name" v-model="formModel.name" autocomplete="off" />
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
</div>
@@ -40,7 +40,7 @@
<AppTextarea id="create-description" v-model="formModel.description" :rows="3" />
</div>
<div class="flex items-start justify-between gap-3 rounded-md border border-border/60 p-3">
<div class="flex items-start justify-between gap-3 rounded-md border p-3">
<div>
<p class="text-sm font-medium">{{ t('Use main account policy') }}</p>
<p class="text-xs text-muted-foreground">
@@ -58,19 +58,24 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">
<Button variant="outline" @click="closeModal()">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">
{{ t('Submit') }}
</AppButton>
</Button>
<Button variant="default" :disabled="submitting" @click="submitForm">
<Spinner v-if="submitting" class="size-4" />
<span>{{ t('Submit') }}</span>
</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import Spinner from '@/components/ui/spinner/Spinner.vue'
import { AppCard, AppModal, AppSwitch, AppTextarea } from '@/components/app'
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref } from 'vue'
-85
View File
@@ -1,85 +0,0 @@
<script setup lang="ts">
import type { ButtonVariants } from '@/components/ui/button'
import { Button } from '@/components/ui/button'
import Spinner from '@/components/ui/spinner/Spinner.vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue'
type AppButtonVariant =
| 'default'
| 'primary'
| 'secondary'
| 'outline'
| 'ghost'
| 'link'
| 'danger'
| 'destructive'
type AppButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
const variantMap: Record<AppButtonVariant, ButtonVariants['variant']> = {
default: 'secondary',
primary: 'default',
secondary: 'secondary',
outline: 'outline',
ghost: 'ghost',
link: 'link',
danger: 'destructive',
destructive: 'destructive',
}
const sizeMap: Record<AppButtonSize, ButtonVariants['size']> = {
xs: 'sm',
sm: 'sm',
md: 'default',
lg: 'lg',
xl: 'lg',
}
const props = withDefaults(
defineProps<{
variant?: AppButtonVariant
size?: AppButtonSize
loading?: boolean
block?: boolean
disabled?: boolean
type?: 'button' | 'submit' | 'reset'
class?: HTMLAttributes['class']
}>(),
{
variant: 'primary',
size: 'md',
loading: false,
block: false,
disabled: false,
type: 'button',
class: undefined,
}
)
const emits = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
const handleClick = (event: MouseEvent) => {
if (props.loading || props.disabled) {
event.preventDefault()
return
}
emits('click', event)
}
</script>
<template>
<Button
:type="type"
:variant="variantMap[variant]"
:size="sizeMap[size]"
:disabled="disabled || loading"
:aria-busy="loading ? 'true' : undefined"
:class="cn('gap-2', block && 'w-full', loading && 'cursor-wait', props.class)"
@click="handleClick"
>
<Spinner v-if="loading" class="size-4" />
<slot />
</Button>
</template>
+1 -1
View File
@@ -31,7 +31,7 @@ const props = withDefaults(
</script>
<template>
<Card :class="cn('border-border/60', props.class)">
<Card :class="props.class">
<CardHeader v-if="title || $slots.header">
<slot name="header">
<CardTitle v-if="title">{{ title }}</CardTitle>
-54
View File
@@ -1,54 +0,0 @@
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue'
type InputSize = 'sm' | 'md' | 'lg'
const sizeClass: Record<InputSize, string> = {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-3',
lg: 'h-11 px-4 text-base',
}
const props = withDefaults(
defineProps<{
type?: HTMLInputElement['type']
placeholder?: string
disabled?: boolean
readonly?: boolean
size?: InputSize
class?: HTMLAttributes['class']
autocomplete?: string
}>(),
{
type: 'text',
placeholder: '',
disabled: false,
readonly: false,
size: 'md',
class: undefined,
autocomplete: undefined,
}
)
const modelValue = defineModel<string | number | null>({ default: '' })
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
modelValue.value = target.value
}
</script>
<template>
<Input
:type="type"
:value="modelValue ?? ''"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:autocomplete="autocomplete"
:class="cn(sizeClass[size], props.class)"
@input="handleInput"
/>
</template>
+24 -73
View File
@@ -1,5 +1,9 @@
<script setup lang="ts">
import { Icon } from '#components'
import LanguageSwitcher from '@/components/language-switcher.vue'
import ThemeSwitcher from '@/components/theme-switcher.vue'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Sidebar,
SidebarContent,
@@ -17,14 +21,10 @@ import {
SidebarSeparator,
useSidebar,
} from '@/components/ui/sidebar'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { ScrollArea } from '@/components/ui/scroll-area'
import LanguageSwitcher from '@/components/language-switcher.vue'
import ThemeSwitcher from '@/components/theme-switcher.vue'
import UserDropdown from '@/components/user-dropdown.vue'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import type { AppConfig, NavItem } from '~/types/app-config'
const appConfig = useAppConfig() as unknown as AppConfig
@@ -80,78 +80,46 @@ const getLabel = (item: NavItem) => t(item.label)
</script>
<template>
<Sidebar collapsible="icon" class="bg-sidebar text-sidebar-foreground">
<SidebarHeader class="border-b border-sidebar-border px-4 py-4">
<Sidebar collapsible="icon">
<SidebarHeader>
<NuxtLink to="/" class="flex items-center gap-3">
<div
class="flex size-10 items-center justify-center rounded-lg bg-primary text-lg font-semibold text-primary-foreground"
>
<span v-if="isCollapsed">{{ brandInitial }}</span>
<img v-else src="~/assets/logo.svg" alt="RustFS" class="h-8" />
<div v-if="isCollapsed" class="flex size-8 items-center justify-center rounded-lg bg-primary text-md font-semibold text-primary-foreground">
<span>{{ brandInitial }}</span>
</div>
<div v-if="!isCollapsed" class="flex min-w-0 flex-col">
<span class="truncate text-sm font-semibold leading-tight">{{ appConfig.name }}</span>
<span class="truncate text-xs text-muted-foreground">{{ appConfig.description }}</span>
<div v-if="!isCollapsed" class="flex min-w-0 flex-col px-3 py-4">
<img src="~/assets/logo.svg" alt="RustFS" class="h-6" />
</div>
</NuxtLink>
</SidebarHeader>
<SidebarContent class="px-2">
<SidebarContent>
<ScrollArea class="flex-1 pr-1">
<div class="flex flex-col gap-4">
<SidebarGroup v-for="(group, groupIndex) in navGroups" :key="groupIndex" class="gap-1">
<SidebarGroupContent>
<SidebarMenu>
<template v-for="item in group" :key="item.label">
<Collapsible
v-if="hasChildren(item)"
as-child
:default-open="isRouteActive(item)"
class="group/collapsible"
>
<Collapsible v-if="hasChildren(item)" as-child :default-open="isRouteActive(item)" class="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton
:is-active="isRouteActive(item)"
:tooltip="getLabel(item)"
>
<SidebarMenuButton :is-active="isRouteActive(item)" :tooltip="getLabel(item)">
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
<Icon
name="ri:arrow-right-s-line"
class="size-4 shrink-0 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
/>
<Icon name="ri:arrow-right-s-line" class="size-4 shrink-0 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem v-for="child in item.children" :key="child.label">
<SidebarMenuSubButton
v-if="isExternal(child)"
as-child
size="sm"
>
<a
:href="normalizedTo(child)"
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
<SidebarMenuSubButton v-if="isExternal(child)" as-child size="sm">
<a :href="normalizedTo(child)" target="_blank" rel="noopener noreferrer" class="flex w-full items-center gap-2">
<Icon v-if="child.icon" :name="child.icon" class="size-3.5 shrink-0" />
<span class="truncate">{{ getLabel(child) }}</span>
<Icon name="ri:external-link-line" class="ml-auto size-3 text-muted-foreground" />
</a>
</SidebarMenuSubButton>
<SidebarMenuSubButton
v-else
as-child
size="sm"
:is-active="isRouteActive(child)"
>
<NuxtLink
:to="normalizedTo(child)"
class="flex w-full items-center gap-2"
>
<SidebarMenuSubButton v-else as-child size="sm" :is-active="isRouteActive(child)">
<NuxtLink :to="normalizedTo(child)" class="flex w-full items-center gap-2">
<Icon v-if="child.icon" :name="child.icon" class="size-3.5 shrink-0" />
<span class="truncate">{{ getLabel(child) }}</span>
</NuxtLink>
@@ -162,32 +130,15 @@ const getLabel = (item: NavItem) => t(item.label)
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem v-else>
<SidebarMenuButton
v-if="isExternal(item)"
as-child
:tooltip="getLabel(item)"
>
<a
:href="normalizedTo(item)"
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-3"
>
<SidebarMenuButton v-if="isExternal(item)" as-child :tooltip="getLabel(item)">
<a :href="normalizedTo(item)" target="_blank" rel="noopener noreferrer" class="flex w-full items-center gap-3">
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
<Icon name="ri:external-link-line" class="size-3.5 text-muted-foreground" />
</a>
</SidebarMenuButton>
<SidebarMenuButton
v-else
as-child
:is-active="isRouteActive(item)"
:tooltip="getLabel(item)"
>
<NuxtLink
:to="normalizedTo(item)"
class="flex w-full items-center gap-3"
>
<SidebarMenuButton v-else as-child :is-active="isRouteActive(item)" :tooltip="getLabel(item)">
<NuxtLink :to="normalizedTo(item)" class="flex w-full items-center gap-3">
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
</NuxtLink>
@@ -203,7 +154,7 @@ const getLabel = (item: NavItem) => t(item.label)
</SidebarContent>
<SidebarFooter class="mt-auto flex flex-col gap-3 px-2 pb-2">
<div class="border-t border-sidebar-border pt-3">
<div class="pt-3">
<div class="flex flex-col gap-2">
<LanguageSwitcher />
<ThemeSwitcher />
+1 -1
View File
@@ -16,7 +16,7 @@ const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const value = defineModel<string>({ default: '' })
const value = defineModel<string>()
watch(
() => props.tabs,
+1 -1
View File
@@ -21,7 +21,7 @@ const props = withDefaults(
const emit = defineEmits<{
(e: 'change', file: File | null): void
}>
}>()
const hovering = ref(false)
@@ -1,6 +1,7 @@
<script setup lang="ts" generic="TData">
import { Button } from '@/components/ui/button'
import type { Table } from '@tanstack/vue-table'
import AppButton from '@/components/app/AppButton.vue'
import AppSelect from '@/components/app/AppSelect.vue'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
@@ -54,23 +55,23 @@ const handlePageSizeChange = (value: number | string | boolean | null) => {
Page {{ pageCount === 0 ? 0 : currentPage }} of {{ pageCount }}
</span>
<div class="flex items-center gap-2">
<AppButton variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.setPageIndex(0)">
<Button variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.setPageIndex(0)">
First
</AppButton>
<AppButton variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.previousPage()">
</Button>
<Button variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.previousPage()">
Prev
</AppButton>
<AppButton variant="outline" size="sm" :disabled="!canNext" @click="props.table.nextPage()">
</Button>
<Button variant="outline" size="sm" :disabled="!canNext" @click="props.table.nextPage()">
Next
</AppButton>
<AppButton
</Button>
<Button
variant="outline"
size="sm"
:disabled="!canNext"
@click="props.table.setPageIndex(Math.max(pageCount - 1, 0))"
>
Last
</AppButton>
</Button>
</div>
</div>
</div>
-2
View File
@@ -1,3 +1 @@
export { default as AppDataTable } from './AppDataTable.vue'
export { default as AppDataTablePagination } from './AppDataTablePagination.vue'
export { useDataTable } from './useDataTable'
+19 -4
View File
@@ -16,11 +16,11 @@ import {
useVueTable,
} from '@tanstack/vue-table'
import type { MaybeRefOrGetter } from 'vue'
import { computed, reactive, ref, toValue, unref, watch, type Ref } from 'vue'
import { computed, reactive, ref, toValue, watch, type Ref } from 'vue'
export interface UseDataTableOptions<TData extends RowData> {
data: MaybeRefOrGetter<TData[]>
columns: ColumnDef<TData, any>[]
columns: MaybeRefOrGetter<ColumnDef<TData, any>[]>
pageSize?: number
manualPagination?: boolean
manualSorting?: boolean
@@ -58,10 +58,11 @@ export function useDataTable<TData extends RowData>(
})
const dataRef = computed(() => toValue(options.data))
const columnsRef = computed(() => toValue(options.columns))
const table = useVueTable({
data: dataRef,
columns: options.columns,
data: dataRef.value,
columns: columnsRef.value,
state: reactive({
get sorting() {
return sorting.value
@@ -98,6 +99,20 @@ export function useDataTable<TData extends RowData>(
getPaginationRowModel: options.manualPagination ? undefined : getPaginationRowModel(),
})
watch(dataRef, value => {
table.setOptions(prev => ({
...prev,
data: value,
}))
})
watch(columnsRef, value => {
table.setOptions(prev => ({
...prev,
columns: value,
}))
})
return {
table,
sorting,
-2
View File
@@ -1,8 +1,6 @@
export { default as AppButton } from './AppButton.vue'
export { default as AppCard } from './AppCard.vue'
export { default as AppBadge } from './AppBadge.vue'
export { default as AppTag } from './AppTag.vue'
export { default as AppInput } from './AppInput.vue'
export { default as AppTextarea } from './AppTextarea.vue'
export { default as AppSelect } from './AppSelect.vue'
export { default as AppSwitch } from './AppSwitch.vue'
+4 -4
View File
@@ -4,7 +4,7 @@
>
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<div class="text-4xl my-6 font-semibold text-primary! px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords
:words="[
@@ -15,7 +15,7 @@
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
class="text-4xl font-semibold text-primary! px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
@@ -31,8 +31,8 @@
</a>
<div class="h-full inset-0 absolute z-0">
<Ripple
class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] [mask-image:linear-gradient(to_bottom,white,transparent)]"
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full"
class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] mask-[linear-gradient(to_bottom,white,transparent)]"
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-white/25 rounded-full"
/>
</div>
</div>
+2 -2
View File
@@ -4,7 +4,7 @@
>
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<div class="text-4xl my-6 font-semibold text-primary! px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords
:words="[
@@ -15,7 +15,7 @@
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
class="text-4xl font-semibold text-primary! px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
+2 -2
View File
@@ -2,7 +2,7 @@
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8">
<div class="max-w-7xl flex flex-col">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<div class="text-4xl my-6 font-semibold text-primary! px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords
:words="[
@@ -13,7 +13,7 @@
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
class="text-4xl font-semibold text-primary! px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
+36 -33
View File
@@ -6,10 +6,10 @@
<div class="space-y-1">
<p class="text-sm font-medium text-foreground">{{ t('Access Policy') }}</p>
</div>
<AppButton variant="outline" size="sm" class="shrink-0" @click="editPolicy">
<Button variant="outline" size="sm" class="shrink-0" @click="editPolicy">
<Icon name="ri:edit-2-line" class="mr-2 size-4" />
<span>{{ t('Edit') }}</span>
</AppButton>
</Button>
</div>
<p class="text-sm text-muted-foreground">{{ currentPolicyLabel }}</p>
</AppCard>
@@ -20,38 +20,38 @@
<p class="text-sm font-medium text-foreground">{{ t('Encryption') }}</p>
<p class="text-xs text-muted-foreground">{{ encryptionLabel }}</p>
</div>
<AppButton variant="outline" size="sm" class="shrink-0" @click="editEncrypt">
<Button variant="outline" size="sm" class="shrink-0" @click="editEncrypt">
<Icon name="ri:edit-2-line" class="mr-2 size-4" />
<span>{{ t('Edit') }}</span>
</AppButton>
</Button>
</div>
</AppCard>
<AppCard padded class="space-y-3">
<div class="flex items-start justify-between gap-3">
<p class="text-sm font-medium text-foreground">{{ t('Tag') }}</p>
<AppButton variant="outline" size="sm" class="shrink-0" @click="addTag">
<Button variant="outline" size="sm" class="shrink-0" @click="addTag">
<Icon name="ri:add-line" class="mr-2 size-4" />
<span>{{ t('Add') }}</span>
</AppButton>
</Button>
</div>
<div v-if="tags.length" class="flex flex-wrap gap-2">
<div
v-for="(tag, index) in tags"
:key="`${tag.Key}-${index}`"
class="flex items-center gap-2 rounded-full border border-border/60 bg-muted/40 px-3 py-1 text-xs"
class="flex items-center gap-2 rounded-full border bg-muted/40 px-3 py-1 text-xs"
>
<button type="button" class="text-left" @click="editTag(index)">
{{ tag.Key }}:{{ tag.Value }}
</button>
<AppButton
<Button
variant="ghost"
size="xs"
size="sm"
class="h-6 w-6 p-0"
@click.stop="handleDeleteTag(index)"
>
<Icon name="ri:close-line" class="size-3.5" />
</AppButton>
</Button>
</div>
</div>
<p v-else class="text-sm text-muted-foreground">
@@ -88,9 +88,9 @@
{{ retentionEnabled ? t('Enabled') : t('Disabled') }}
</p>
</div>
<AppButton variant="outline" size="sm" class="shrink-0" @click="editRetention">
<Button variant="outline" size="sm" class="shrink-0" @click="editRetention">
<span>{{ t('Edit') }}</span>
</AppButton>
</Button>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div>
@@ -128,19 +128,19 @@
</div>
<div v-if="policyFormValue.policy === 'custom'" class="space-y-2">
<Label>{{ t('Policy Content') }}</Label>
<div class="max-h-[60vh] overflow-y-auto rounded-md border border-border/60 p-2">
<div class="max-h-[60vh] overflow-y-auto rounded-md border p-2">
<json-editor v-model="policyFormValue.content" />
</div>
</div>
</AppCard>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="showPolicyModal = false">
<Button variant="outline" @click="showPolicyModal = false">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" @click="submitPolicyForm">
</Button>
<Button variant="default" @click="submitPolicyForm">
{{ t('Confirm') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
@@ -149,21 +149,21 @@
<AppCard padded class="space-y-4">
<div class="space-y-2">
<Label>{{ t('Tag Key') }}</Label>
<AppInput v-model="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
<Input v-model="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
</div>
<div class="space-y-2">
<Label>{{ t('Tag Value') }}</Label>
<AppInput v-model="tagFormValue.value" :placeholder="t('Please enter tag value')" />
<Input v-model="tagFormValue.value" :placeholder="t('Please enter tag value')" />
</div>
</AppCard>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="showTagModal = false">
<Button variant="outline" @click="showTagModal = false">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" @click="submitTagForm">
</Button>
<Button variant="default" @click="submitTagForm">
{{ t('Confirm') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
@@ -189,12 +189,12 @@
</AppCard>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="showEncryptModal = false">
<Button variant="outline" @click="showEncryptModal = false">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" @click="submitEncryptForm">
</Button>
<Button variant="default" @click="submitEncryptForm">
{{ t('Confirm') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
@@ -219,24 +219,27 @@
</div>
<div class="space-y-2">
<Label>{{ t('Retention Period') }}</Label>
<AppInput v-model="retentionPeriodInput" type="number" />
<Input v-model="retentionPeriodInput" type="number" />
</div>
</AppCard>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="showRetentionModal = false">
<Button variant="outline" @click="showRetentionModal = false">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" @click="submitRetentionForm">
</Button>
<Button variant="default" @click="submitRetentionForm">
{{ t('Confirm') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppDrawer, AppInput, AppModal, AppRadioGroup, AppSelect, AppSwitch, AppSpinner } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppDrawer, AppModal, AppRadioGroup, AppSelect, AppSwitch, AppSpinner } from '@/components/app'
import { Label } from '@/components/ui/label'
import { Icon } from '#components'
import { computed, ref, watch } from 'vue'
+14 -11
View File
@@ -3,7 +3,7 @@
<AppCard padded class="space-y-6">
<div class="space-y-2">
<Label for="bucket-name">{{ t('Please enter name') }}</Label>
<AppInput
<Input
id="bucket-name"
v-model="objectKey"
autocomplete="off"
@@ -16,19 +16,19 @@
<div class="space-y-2">
<Label>{{ t('Version') }}</Label>
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between rounded-md border p-3">
<AppSwitch v-model="version" />
</div>
</div>
<div class="space-y-2">
<Label>{{ t('Object Lock') }}</Label>
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between rounded-md border p-3">
<AppSwitch v-model="objectLock" />
</div>
</div>
<div v-if="objectLock" class="space-y-4 rounded-lg border border-border/60 p-4">
<div v-if="objectLock" class="space-y-4 rounded-lg border p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">{{ t('Retention') }}</p>
@@ -50,7 +50,7 @@
<div class="space-y-2">
<Label>{{ t('Validity') }}</Label>
<div class="flex flex-col gap-2 sm:flex-row">
<AppInput
<Input
v-model="retentionPeriod"
type="number"
class="sm:w-32"
@@ -68,24 +68,27 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal">
<Button variant="outline" @click="closeModal">
{{ t('Cancel') }}
</AppButton>
<AppButton
variant="primary"
</Button>
<Button
variant="default"
:loading="creating"
:disabled="isSubmitDisabled"
@click="handleCreateBucket"
>
{{ t('Create') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal, AppRadioGroup, AppSelect, AppSwitch } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppModal, AppRadioGroup, AppSelect, AppSwitch } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
+9 -8
View File
@@ -1,8 +1,9 @@
<script lang="ts" setup>
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import ClipboardJS from 'clipboard'
import AppButton from '@/components/app/AppButton.vue'
import AppInput from '@/components/app/AppInput.vue'
import { useMessage } from '@/lib/ui/message'
import { onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -52,21 +53,21 @@ onUnmounted(() => {
<template>
<div class="flex h-full items-center gap-2">
<AppInput
<Input
v-model="model"
:readonly="props.readonly"
:id="props.id"
class="flex-1"
/>
<AppButton
<Button
v-if="!props.copyIcon"
:id="`${props.id}-btn`"
:data-clipboard-target="`#${props.id}`"
variant="primary"
variant="default"
>
{{ t('Copy') }}
</AppButton>
<AppButton
</Button>
<Button
v-else
:id="`${props.id}-btn-icon`"
variant="ghost"
@@ -77,6 +78,6 @@ onUnmounted(() => {
>
<Icon :size="18" name="ri:file-copy-line" />
<span class="sr-only">{{ t('Copy') }}</span>
</AppButton>
</Button>
</div>
</template>
+13 -10
View File
@@ -25,7 +25,7 @@
<div v-else class="space-y-4">
<AppCard
class="flex cursor-pointer items-center gap-3 border border-border/60 transition hover:border-primary"
class="flex cursor-pointer items-center gap-3 border transition hover:border-primary"
@click="resetType"
>
<img :src="iconUrl" class="h-10 w-10" alt="" />
@@ -38,7 +38,7 @@
<div class="grid gap-4">
<div class="grid gap-2">
<Label for="target-name">{{ t('Name') }} (A-Z,0-9,_)</Label>
<AppInput
<Input
id="target-name"
v-model="formData.name"
:placeholder="t('Please enter name')"
@@ -54,13 +54,13 @@
class="grid gap-2"
>
<Label :for="`config-${config.name}`">{{ config.label }}</Label>
<AppInput
<Input
v-if="config.type === 'text'"
:id="`config-${config.name}`"
v-model="formData.config[config.name]"
:placeholder="`${t('Please enter')} ${config.label.toLowerCase()}`"
/>
<AppInput
<Input
v-else-if="config.type === 'password'"
:id="`config-${config.name}`"
v-model="formData.config[config.name]"
@@ -68,7 +68,7 @@
autocomplete="off"
:placeholder="`${t('Please enter')} ${config.label.toLowerCase()}`"
/>
<AppInput
<Input
v-else-if="config.type === 'number'"
:id="`config-${config.name}`"
v-model="formData.config[config.name]"
@@ -82,20 +82,23 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" :disabled="!formData.type" @click="handleSave">
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" :disabled="!formData.type" @click="handleSave">
{{ t('Save') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { useI18n } from 'vue-i18n'
import MqttIcon from '~/assets/svg/mqtt.svg'
import WebhooksIcon from '~/assets/svg/webhooks.svg'
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { AppCard, AppModal } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref, watch } from 'vue'
import { useEventTarget } from '#imports'
@@ -258,4 +261,4 @@ defineExpose({
resetType()
},
})
</script>
</script>
+5 -1
View File
@@ -72,7 +72,7 @@
<Checkbox
:checked="formData.events.includes(event.value)"
class="mt-1"
@update:checked="value => toggleEvent(event.value, value)"
@update:checked="handleEventChecked(event.value, $event)"
/>
<span>{{ t(event.labelKey) }}</span>
</label>
@@ -213,6 +213,10 @@ const toggleEvent = (event: string, value: boolean | 'indeterminate') => {
}
}
const handleEventChecked = (eventValue: string, value: boolean | 'indeterminate') => {
toggleEvent(eventValue, value)
}
const validate = () => {
errors.resourceName = formData.value.resourceName ? '' : t('Please select resource name')
errors.events = formData.value.events.length ? '' : t('Please select at least one event')
+33 -29
View File
@@ -22,7 +22,7 @@
<div class="grid gap-2">
<Label>{{ t('Time Cycle') }}</Label>
<div class="flex items-center gap-3">
<AppInput
<Input
v-model="formData.days"
type="number"
min="1"
@@ -40,27 +40,27 @@
<div class="mt-4 space-y-4">
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
<AppButton variant="outline" size="sm" @click="addTag">
<Button variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</AppButton>
</Button>
</div>
<div v-if="formData.tags.length" class="space-y-3">
<div
v-for="(tag, index) in formData.tags"
:key="index"
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
>
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
<Input v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<AppButton
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<Button
variant="ghost"
size="sm"
class="text-destructive"
@@ -68,7 +68,7 @@
@click="removeTag(index)"
>
<Icon name="ri:delete-bin-line" class="size-4" />
</AppButton>
</Button>
</div>
</div>
</div>
@@ -103,7 +103,7 @@
<div class="grid gap-2">
<Label>{{ t('Time Cycle') }}</Label>
<div class="flex items-center gap-3">
<AppInput
<Input
v-model="formData.days"
type="number"
min="1"
@@ -130,27 +130,27 @@
<div class="mt-4 space-y-4">
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
<AppButton variant="outline" size="sm" @click="addTag">
<Button variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</AppButton>
</Button>
</div>
<div v-if="formData.tags.length" class="space-y-3">
<div
v-for="(tag, index) in formData.tags"
:key="index"
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
>
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
<Input v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<AppButton
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<Button
variant="ghost"
size="sm"
class="text-destructive"
@@ -158,7 +158,7 @@
@click="removeTag(index)"
>
<Icon name="ri:delete-bin-line" class="size-4" />
</AppButton>
</Button>
</div>
</div>
</div>
@@ -172,16 +172,19 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="handleSave">{{ t('Save') }}</AppButton>
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="handleSave">{{ t('Save') }}</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
import { AppCard, AppModal, AppSelect, AppSwitch } from '@/components/app'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Label } from '@/components/ui/label'
import { computed, onMounted, reactive, ref, watch } from 'vue'
@@ -247,7 +250,7 @@ const loadTiers = async () => {
}))
}
if (!formData.storageType && tiers.value.length > 0) {
formData.storageType = tiers.value[0].value
formData.storageType = tiers.value[0]?.value ?? ''
}
}
@@ -327,11 +330,12 @@ const buildFilter = () => {
}
const filter: any = {}
const [firstTag] = validTags
if (validTags.length === 1 && !formData.prefix) {
if (firstTag && validTags.length === 1 && !formData.prefix) {
filter.Tag = {
Key: validTags[0].key,
Value: validTags[0].value,
Key: firstTag.key,
Value: firstTag.value,
}
} else if (validTags.length > 0) {
filter.And = {
@@ -347,11 +351,11 @@ const buildFilter = () => {
filter.Prefix = formData.prefix
}
if (formData.prefix && validTags.length === 1 && !filter.And) {
if (formData.prefix && firstTag && validTags.length === 1 && !filter.And) {
filter.Prefix = formData.prefix
filter.Tag = {
Key: validTags[0].key,
Value: validTags[0].value,
Key: firstTag.key,
Value: firstTag.value,
}
} else if (formData.prefix && validTags.length === 0) {
filter.Prefix = formData.prefix
@@ -440,4 +444,4 @@ onMounted(() => {
loadTiers()
loadVersioningStatus()
})
</script>
</script>
+6 -4
View File
@@ -2,9 +2,9 @@
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between gap-3">
<div class="truncate text-sm font-medium text-foreground">{{ task.key }}</div>
<AppButton variant="ghost" size="xs" class="h-auto px-2 text-xs" @click="handleDeleteTask">
<Button variant="ghost" size="sm" class="h-auto px-2 text-xs" @click="handleDeleteTask">
{{ t('Delete Record') }}
</AppButton>
</Button>
</div>
<AppProgress :value="task.progress" :processing="task.status === 'deleting'" :height="2" />
<div class="flex items-center justify-between text-muted-foreground">
@@ -21,7 +21,9 @@
</template>
<script setup lang="ts">
import { AppButton, AppProgress } from '@/components/app'
import { Button } from '@/components/ui/button'
import { AppProgress } from '@/components/app'
import type { DeleteTask } from '~/lib/delete-task-manager'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
@@ -33,4 +35,4 @@ const props = defineProps<{ task: DeleteTask }>()
const handleDeleteTask = () => {
store.removeTask(props.task.id)
}
</script>
</script>
+52 -43
View File
@@ -2,23 +2,23 @@
<AppDrawer v-model="visible" :title="t('Object Details')" size="lg">
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-2">
<AppButton variant="outline" size="sm" @click="download">
<Button variant="outline" size="sm" @click="download">
<Icon name="ri:download-line" class="size-4" />
{{ t('Download') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="() => (showPreview = true)">
</Button>
<Button variant="outline" size="sm" @click="() => (showPreview = true)">
<Icon name="ri:eye-line" class="size-4" />
{{ t('Preview') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="() => (showTagView = true)">
</Button>
<Button variant="outline" size="sm" @click="() => (showTagView = true)">
<Icon name="ri:price-tag-3-line" class="size-4" />
{{ t('Set Tags') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="geVersions">
</Button>
<Button variant="outline" size="sm" @click="viewVersions">
<Icon name="ri:file-list-2-line" class="size-4" />
{{ t('Versions') }}
</AppButton>
<AppButton
</Button>
<Button
v-if="lockStatus"
variant="outline"
size="sm"
@@ -26,7 +26,7 @@
>
<Icon name="ri:lock-line" class="size-4" />
{{ t('Retention') }}
</AppButton>
</Button>
</div>
<AppCard :title="t('Info')" padded>
@@ -63,10 +63,10 @@
</div>
</div>
<div class="flex items-center gap-2">
<AppInput v-model="signedUrl" id="signedUrl" :placeholder="t('Temporary URL')" />
<AppButton variant="outline" size="sm" :disabled="!object?.Key" @click="copyTemporaryUrl">
<Input v-model="signedUrl" :placeholder="t('Temporary URL')" readonly />
<Button variant="outline" size="sm" :disabled="!object?.Key" @click="copyTemporaryUrl">
{{ t('Copy') }}
</AppButton>
</Button>
</div>
</div>
</AppCard>
@@ -80,9 +80,9 @@
</AppTag>
</div>
<form class="flex flex-wrap gap-3" @submit.prevent="submitTagForm">
<AppInput v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
<AppInput v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
<AppButton type="submit" variant="primary">{{ t('Add') }}</AppButton>
<Input v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
<Input v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
<Button type="submit" variant="default">{{ t('Add') }}</Button>
</form>
</AppCard>
</AppModal>
@@ -102,12 +102,12 @@
</div>
<div class="flex flex-col gap-2">
<Label>{{ t('Retention RetainUntilDate') }}</Label>
<AppInput v-model="retainUntilDate" type="datetime-local" />
<Input v-model="retainUntilDate" type="datetime-local" />
</div>
<div class="flex justify-end gap-2">
<AppButton variant="secondary" @click="resetRetention">{{ t('Reset') }}</AppButton>
<AppButton type="submit" variant="primary">{{ t('Confirm') }}</AppButton>
<AppButton variant="outline" @click="() => (showRetentionView = false)">{{ t('Cancel') }}</AppButton>
<Button variant="secondary" @click="resetRetention">{{ t('Reset') }}</Button>
<Button type="submit" variant="default">{{ t('Confirm') }}</Button>
<Button variant="outline" @click="() => (showRetentionView = false)">{{ t('Cancel') }}</Button>
</div>
</form>
</AppCard>
@@ -118,17 +118,17 @@
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppDrawer, AppInput, AppModal, AppRadioGroup, AppSwitch, AppTag } from '@/components/app'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppDrawer, AppModal, AppRadioGroup, AppSwitch, AppTag } from '@/components/app'
import { Label } from '@/components/ui/label'
import { useI18n } from 'vue-i18n'
import { ref, computed } from 'vue'
import ClipboardJS from 'clipboard'
import { ref } from 'vue'
import { joinRelativeURL } from 'ufo'
const { t } = useI18n()
const message = useMessage()
const dialog = useDialog()
const props = defineProps<{
bucketName: string
@@ -140,7 +140,7 @@ const tags = ref<Array<{ Key: string; Value: string }>>([])
const lockStatus = ref(false)
const retention = ref('')
const retainUntilDate = ref('')
const retentionMode = ref('GOVERNANCE')
const retentionMode = ref<'COMPLIANCE' | 'GOVERNANCE'>('GOVERNANCE')
const signedUrl = ref('')
const showTagView = ref(false)
const showRetentionView = ref(false)
@@ -148,8 +148,15 @@ const showPreview = ref(false)
const tagFormValue = ref({ Key: '', Value: '' })
const { getObjectInfo, setLegalHold, putObjectTags, getObjectTags, getObjectRetention, putObjectRetention, geObjectVersions } =
useObject({ bucket: props.bucketName })
const {
getObjectInfo,
setLegalHold,
putObjectTags,
getObjectTags,
getObjectRetention,
putObjectRetention,
getObjectVersions,
} = useObject({ bucket: props.bucketName })
const openDrawer = async (bucket: string, key: string) => {
visible.value = true
@@ -168,7 +175,7 @@ const openDrawer = async (bucket: string, key: string) => {
const fetchTags = async (key: string) => {
try {
const response = await getObjectTags(key)
tags.value = response.TagSet || []
tags.value = response
} catch (error) {
tags.value = []
}
@@ -177,8 +184,9 @@ const fetchTags = async (key: string) => {
const fetchRetention = async (key: string) => {
try {
const response = await getObjectRetention(key)
retention.value = response.Mode || ''
retainUntilDate.value = response.RetainUntilDate || ''
retention.value = response.Mode ?? ''
retainUntilDate.value = response.RetainUntilDate ?? ''
retentionMode.value = response.Mode === 'COMPLIANCE' ? 'COMPLIANCE' : 'GOVERNANCE'
} catch (error) {
retention.value = ''
retainUntilDate.value = ''
@@ -219,7 +227,7 @@ const submitRetention = async () => {
try {
await putObjectRetention(object.value.Key, {
Mode: retentionMode.value,
RetainUntilDate: retainUntilDate.value,
RetainUntilDate: retainUntilDate.value || undefined,
})
message.success(t('Update Success'))
showRetentionView.value = false
@@ -232,7 +240,7 @@ const submitRetention = async () => {
const resetRetention = async () => {
if (!object.value) return
try {
await putObjectRetention(object.value.Key, { Mode: 'GOVERNANCE', RetainUntilDate: '' })
await putObjectRetention(object.value.Key, { Mode: 'GOVERNANCE' })
message.success(t('Update Success'))
fetchRetention(object.value.Key)
} catch (error: any) {
@@ -240,9 +248,9 @@ const resetRetention = async () => {
}
}
const geVersions = async () => {
const viewVersions = async () => {
if (!object.value) return
await geObjectVersions(object.value.Key)
await getObjectVersions(object.value.Key)
}
const download = async () => {
@@ -251,16 +259,17 @@ const download = async () => {
window.open(url, '_blank')
}
const copyTemporaryUrl = () => {
const clipboard = new ClipboardJS('#signedUrl', {
text: () => signedUrl.value,
})
clipboard.on('success', () => message.success(t('Copy Success')))
clipboard.on('error', () => message.error(t('Copy Failed')))
clipboard.destroy()
const copyTemporaryUrl = async () => {
if (!signedUrl.value) return
try {
await navigator.clipboard.writeText(signedUrl.value)
message.success(t('Copy Success'))
} catch {
message.error(t('Copy Failed'))
}
}
defineExpose({
openDrawer,
})
</script>
</script>
+72 -50
View File
@@ -1,8 +1,8 @@
<template>
<div class="space-y-4">
<PageHeader class="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<PageHeader class="sticky top-0 z-10 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div class="flex flex-wrap items-center justify-between gap-3">
<AppInput
<Input
v-model="searchTerm"
:placeholder="t('Search')"
class="max-w-md"
@@ -11,15 +11,15 @@
<div class="flex flex-wrap items-center gap-2">
<object-upload-stats />
<object-delete-stats />
<AppButton variant="secondary" @click="() => handleNewObject(true)">
<Button variant="secondary" @click="() => handleNewObject(true)">
<Icon name="ri:add-line" class="size-4" />
<span>{{ t('New Folder') }}</span>
</AppButton>
<AppButton variant="secondary" @click="() => (uploadPickerVisible = true)">
</Button>
<Button variant="secondary" @click="() => (uploadPickerVisible = true)">
<Icon name="ri:file-add-line" class="size-4" />
<span>{{ t('Upload File') }}/{{ t('Folder') }}</span>
</AppButton>
<AppButton
</Button>
<Button
variant="destructive"
:disabled="!checkedKeys.length"
v-show="checkedKeys.length"
@@ -27,8 +27,8 @@
>
<Icon name="ri:delete-bin-5-line" class="size-4" />
<span>{{ t('Delete Selected') }}</span>
</AppButton>
<AppButton
</Button>
<Button
variant="outline"
:disabled="!checkedKeys.length"
v-show="checkedKeys.length"
@@ -36,16 +36,16 @@
>
<Icon name="ri:download-cloud-2-line" class="size-4" />
<span>{{ t('Download') }}</span>
</AppButton>
<AppButton variant="outline" @click="refresh">
</Button>
<Button variant="outline" @click="handleRefresh">
<Icon name="ri:refresh-line" class="size-4" />
<span>{{ t('Refresh') }}</span>
</AppButton>
</Button>
</div>
</div>
</PageHeader>
<AppCard padded class="border border-border/60">
<AppCard padded class="border">
<AppDataTable
:table="table"
:is-loading="loading"
@@ -55,14 +55,14 @@
</AppCard>
<div class="flex justify-end gap-2">
<AppButton variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
<Button variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
<Icon name="ri:arrow-left-s-line" class="mr-2" />
<span>{{ t('Previous Page') }}</span>
</AppButton>
<AppButton variant="outline" :disabled="!nextToken" @click="goToNextPage">
</Button>
<Button variant="outline" :disabled="!nextToken" @click="goToNextPage">
<span>{{ t('Next Page') }}</span>
<Icon name="ri:arrow-right-s-line" class="ml-2" />
</AppButton>
</Button>
</div>
<object-upload-picker
@@ -84,26 +84,36 @@
refresh()
}"
/>
<object-info ref="infoRef" @refresh-parent="handleObjectDeleted" />
<object-info
ref="infoRef"
:bucket-name="bucketName"
@refresh-parent="handleObjectDeleted"
/>
</div>
</template>
<script setup lang="ts">
import { icon } from '@/utils/ui'
import { AppButton, AppCard, AppDataTable, AppInput } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard } from '@/components/app'
import AppDataTable from '@/components/app/data-table/AppDataTable.vue'
import { useDataTable } from '@/components/app/data-table'
import PageHeader from '@/components/page/header.vue'
import { ListObjectsV2Command, type CommonPrefix, type _Object } from '@aws-sdk/client-s3'
import { ListObjectsV2Command } from '@aws-sdk/client-s3'
import dayjs from 'dayjs'
import { saveAs } from 'file-saver'
import JSZip from 'jszip'
import { joinRelativeURL } from 'ufo'
import { computed, h, ref, watch } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
import { Icon, NuxtLink } from '#components'
const { $s3Client } = useNuxtApp()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const dialog = useDialog()
const message = useMessage()
const props = defineProps<{ bucket: string; path: string }>()
@@ -133,6 +143,13 @@ const handleSearch = debounce(() => {
const uploadTaskStore = useUploadTaskManagerStore()
const deleteTaskStore = useDeleteTaskManagerStore()
type ObjectRow = {
Key: string
type: 'prefix' | 'object'
Size: number
LastModified: string
}
const bucketName = computed(() => props.bucket)
const prefix = computed(() => decodeURIComponent(props.path))
@@ -156,7 +173,7 @@ const handleObjectDeleted = () => {
refresh()
}
const fetchObjects = async () => {
const fetchObjects = async (): Promise<ObjectRow[]> => {
loading.value = true
try {
const params: any = {
@@ -169,14 +186,14 @@ const fetchObjects = async () => {
const response = await $s3Client.send(new ListObjectsV2Command(params))
nextToken.value = response.NextContinuationToken
const prefixData = (response.CommonPrefixes ?? []).map(item => ({
const prefixData: ObjectRow[] = (response.CommonPrefixes ?? []).map(item => ({
Key: item.Prefix ?? '',
type: 'prefix' as const,
Size: 0,
LastModified: '',
}))
const objectData = (response.Contents ?? []).map(item => ({
const objectData: ObjectRow[] = (response.Contents ?? []).map(item => ({
Key: item.Key ?? '',
type: 'object' as const,
Size: item.Size ?? 0,
@@ -189,20 +206,23 @@ const fetchObjects = async () => {
}
}
const { data, refresh } = await useAsyncData(
() => ['objects', bucketName.value, prefix.value, continuationToken.value, searchTerm.value],
const { data, refresh } = await useAsyncData<ObjectRow[]>(
'objects',
async () => {
const objects = await fetchObjects()
if (!searchTerm.value) return objects
const term = searchTerm.value.toLowerCase()
return objects.filter(item => item.Key.toLowerCase().includes(term))
},
{ default: () => [] }
{
default: () => [],
watch: [bucketName, prefix, continuationToken, searchTerm],
}
)
const checkedKeys = ref<string[]>([])
const columns = computed(() => {
const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
return [
{
id: 'select',
@@ -222,6 +242,7 @@ const columns = computed(() => {
}),
},
{
id: 'object',
header: () => t('Object'),
cell: ({ row }: any) => {
const displayKey = prefix.value ? row.original.Key.substring(prefix.value.length) : row.original.Key
@@ -247,10 +268,12 @@ const columns = computed(() => {
},
},
{
id: 'size',
header: () => t('Size'),
cell: ({ row }: any) => (row.original.type === 'object' ? formatBytes(row.original.Size) : ''),
},
{
id: 'lastModified',
header: () => t('Last Modified'),
cell: ({ row }: any) => (row.original.LastModified ? dayjs(row.original.LastModified).format('YYYY-MM-DD HH:mm:ss') : ''),
},
@@ -262,7 +285,7 @@ const columns = computed(() => {
h('div', { class: 'flex justify-center gap-2' }, [
row.original.type === 'object'
? h(
AppButton,
Button,
{
variant: 'outline',
size: 'sm',
@@ -272,7 +295,7 @@ const columns = computed(() => {
)
: null,
h(
AppButton,
Button,
{
variant: 'outline',
size: 'sm',
@@ -285,31 +308,29 @@ const columns = computed(() => {
]
})
const { table } = useDataTable({
const { table } = useDataTable<ObjectRow>({
data,
columns,
getRowId: (row: any) => row.Key,
state: computed(() => ({
rowSelection: checkedKeys.value.reduce((acc: Record<string, boolean>, key) => {
acc[key] = true
return acc
}, {}),
})),
onRowSelectionChange: updater => {
const next = typeof updater === 'function' ? updater({}) : updater
checkedKeys.value = Object.keys(next).filter(key => next[key])
},
})
watch(
() => table.getState().rowSelection,
selection => {
checkedKeys.value = Object.keys(selection).filter(key => selection[key])
},
{ deep: true }
)
watch(
() => uploadTaskStore.tasks,
() => setTimeout(refresh, 500),
() => setTimeout(() => refresh(), 500),
{ deep: true }
)
watch(
() => deleteTaskStore.tasks,
() => setTimeout(refresh, 500),
() => setTimeout(() => refresh(), 500),
{ deep: true }
)
@@ -328,6 +349,10 @@ const goToPreviousPage = () => {
refresh()
}
const handleRefresh = () => {
refresh()
}
const downloadFile = async (key: string) => {
const url = bucketPath(['download', key])
window.open(url, '_blank')
@@ -369,12 +394,9 @@ const confirmDelete = (keys: string[]) => {
}
const handleDelete = async (keys: string[]) => {
const bucket = bucketName.value
const taskStore = useDeleteTaskManagerStore()
for (const key of keys) {
await taskStore.addTask({ bucket, key })
}
deleteTaskStore.addKeys(keys, bucketName.value, prefix.value)
checkedKeys.value = []
table.resetRowSelection()
refresh()
}
@@ -389,4 +411,4 @@ const formatBytes = (bytes: number) => {
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / 1024 ** i).toFixed(2)} ${units[i]}`
}
</script>
</script>
+9 -6
View File
@@ -5,27 +5,30 @@
<AlertDescription>{{ t('Overwrite Warning') }}</AlertDescription>
</Alert>
<AppInput
<Input
v-model="objectKey"
:placeholder="t('Name Placeholder', { type: displayType })"
autocomplete="off"
/>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal">{{ t('Close') }}</AppButton>
<AppButton variant="primary" :disabled="objectKey.trim().length < 1" @click="handlePutObject">
<Button variant="outline" @click="closeModal">{{ t('Close') }}</Button>
<Button variant="default" :disabled="objectKey.trim().length < 1" @click="handlePutObject">
{{ t('Create') }}
</AppButton>
</Button>
</div>
</div>
</AppModal>
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { joinRelativeURL } from 'ufo'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { AppButton, AppInput, AppModal } from '@/components/app'
import { AppModal } from '@/components/app'
import { Alert, AlertDescription } from '@/components/ui/alert'
const { t } = useI18n()
@@ -66,4 +69,4 @@ const handlePutObject = async () => {
$message.error(error?.message || t('Create Failed'))
}
}
</script>
</script>
+8 -6
View File
@@ -2,19 +2,19 @@
<AppModal v-model="visibleProxy" :title="t('Preview')" size="xl" :close-on-backdrop="false">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-end gap-2">
<AppButton variant="outline" size="sm" @click="closeModal">
<Button variant="outline" size="sm" @click="closeModal">
<Icon name="ri:close-line" class="size-4" />
{{ t('Close') }}
</AppButton>
</Button>
</div>
<div class="min-h-[300px] rounded-md border border-border/60 p-4">
<div class="min-h-[300px] rounded-md border p-4">
<AppSpinner v-if="loading" class="mx-auto size-8" />
<template v-else>
<div v-if="isImage" class="flex justify-center">
<img :src="previewUrl" alt="preview" class="max-h-[60vh]" />
</div>
<iframe v-else-if="isPdf" :src="previewUrl" class="h-[70vh] w-full" frameborder="0" />
<pre v-else-if="isText" class="max-h-[70vh] overflow-auto whitespace-pre-wrap break-words">{{ textContent }}</pre>
<pre v-else-if="isText" class="max-h-[70vh] overflow-auto whitespace-pre-wrap wrap-break-word">{{ textContent }}</pre>
<video v-else-if="isVideo" controls class="w-full">
<source :src="previewUrl" :type="contentType" />
{{ t('Your browser does not support the video tag') }}
@@ -33,7 +33,9 @@
</template>
<script setup lang="ts">
import { AppButton, AppModal, AppSpinner } from '@/components/app'
import { Button } from '@/components/ui/button'
import { AppModal, AppSpinner } from '@/components/app'
import { Icon } from '#components'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -119,4 +121,4 @@ const reset = () => {
}
const closeModal = () => emit('update:show', false)
</script>
</script>
+8 -6
View File
@@ -1,5 +1,5 @@
<template>
<AppButton
<Button
v-if="total > 0"
variant="ghost"
size="sm"
@@ -21,7 +21,7 @@
<div v-else>
{{ t('Task Completed', { completed: completed.length, failed: failed.length }) }}
</div>
</AppButton>
</Button>
<AppDrawer v-model="showDrawer" :title="t('Task Management')" size="lg">
<div class="flex flex-col gap-4">
@@ -41,9 +41,9 @@
<div v-if="total > 0" class="space-y-3">
<div class="flex items-center justify-between text-sm text-muted-foreground">
<div>{{ total - (pending.length + processing.length) }}/{{ total }}</div>
<AppButton variant="ghost" size="sm" class="h-auto px-2" @click="clearTasks">
<Button variant="ghost" size="sm" class="h-auto px-2" @click="clearTasks">
{{ t('Clear Records') }}
</AppButton>
</Button>
</div>
<AppProgress :value="percentage" :processing="percentage < 100" :height="3" />
</div>
@@ -76,7 +76,9 @@
</template>
<script setup lang="ts">
import { AppButton, AppDrawer, AppProgress, AppSpinner } from '@/components/app'
import { Button } from '@/components/ui/button'
import { AppDrawer, AppProgress, AppSpinner } from '@/components/app'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { computed, ref, watch } from 'vue'
@@ -147,4 +149,4 @@ const toggleDrawer = () => {
const clearTasks = () => {
props.onClearTasks()
}
</script>
</script>
+22 -20
View File
@@ -21,18 +21,18 @@
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<AppButton variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFile">
<Button variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFile">
<Icon name="ri:file-add-line" class="size-4" />
{{ t('Select File') }}
</AppButton>
<AppButton variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFolder">
</Button>
<Button variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFolder">
<Icon name="ri:folder-add-line" class="size-4" />
{{ t('Select Folder') }}
</AppButton>
<AppButton variant="secondary" size="sm" @click="closeModal">
</Button>
<Button variant="secondary" size="sm" @click="closeModal">
<Icon name="ri:close-line" class="size-4" />
{{ t('Close') }}
</AppButton>
</Button>
</div>
</div>
@@ -51,7 +51,7 @@
</AlertDescription>
</Alert>
<AppCard padded class="space-y-4 border border-border/60">
<AppCard padded class="space-y-4 border">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="space-y-1 text-sm text-muted-foreground">
<p v-if="totalFileCount > 0">
@@ -61,7 +61,7 @@
{{ t('Memory Usage') }}: {{ getMemoryUsageLevel() }}
</p>
</div>
<AppButton
<Button
variant="outline"
size="sm"
:disabled="!hasFiles || isFolderLoading || isAdding"
@@ -69,11 +69,11 @@
>
<Icon name="ri:delete-bin-line" class="size-4" />
{{ t('Clear All') }}
</AppButton>
</Button>
</div>
<div
class="rounded-md border border-dashed border-border/60 transition"
class="rounded-md border border-dashed transition"
:class="isDragOver ? 'border-primary bg-primary/5' : ''"
@dragenter.prevent="handleDragEnter"
@dragover.prevent="handleDragOver"
@@ -128,10 +128,10 @@
{{ formatBytes(item.size) }}
</td>
<td class="px-3 py-2 text-right">
<AppButton variant="ghost" size="sm" class="h-auto px-2" @click="removeItem(index)">
<Button variant="ghost" size="sm" class="h-auto px-2" @click="removeItem(index)">
<Icon name="ri:delete-bin-line" class="size-4" />
{{ t('Delete') }}
</AppButton>
</Button>
</td>
</tr>
</tbody>
@@ -139,7 +139,7 @@
</div>
</div>
<div v-if="isFolderLoading" class="space-y-2 rounded-md border border-dashed border-border/60 p-3">
<div v-if="isFolderLoading" class="space-y-2 rounded-md border border-dashed p-3">
<div class="flex items-center justify-between text-xs text-muted-foreground">
<span>{{ t('Reading Folder Files') }}</span>
<span>{{ folderLoadingProgress }}%</span>
@@ -147,7 +147,7 @@
<AppProgress :value="folderLoadingProgress" :processing="true" />
</div>
<div v-if="isAdding" class="space-y-2 rounded-md border border-dashed border-border/60 p-3">
<div v-if="isAdding" class="space-y-2 rounded-md border border-dashed p-3">
<div class="flex items-center justify-between text-xs text-muted-foreground">
<span>{{ t('Adding to Upload Queue') }}</span>
<span>{{ addProgress }}%</span>
@@ -157,25 +157,27 @@
</AppCard>
<div class="flex justify-end gap-2">
<AppButton variant="outline" :disabled="!hasFiles || isAdding || isFolderLoading">
<Button variant="outline" :disabled="!hasFiles || isAdding || isFolderLoading">
{{ t('Configure') }}
</AppButton>
<AppButton
variant="primary"
</Button>
<Button
variant="default"
:disabled="!hasFiles || isAdding || isFolderLoading"
:loading="isAdding"
@click="handleUpload"
>
{{ t('Start Upload') }}
</AppButton>
</Button>
</div>
</div>
</AppModal>
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import { AppButton, AppCard, AppModal, AppProgress } from '@/components/app'
import { AppCard, AppModal, AppProgress } from '@/components/app'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
+11 -9
View File
@@ -3,27 +3,27 @@
<div class="flex items-center justify-between gap-3">
<div class="truncate text-sm font-medium text-foreground">{{ task.file.name }}</div>
<div class="flex shrink-0 items-center gap-1.5">
<AppButton
<Button
v-if="task.status === 'uploading'"
variant="ghost"
size="xs"
size="sm"
class="h-auto px-2 text-xs"
@click="handlePauseTask"
>
{{ t('Pause') }}
</AppButton>
<AppButton
</Button>
<Button
v-if="task.status === 'paused'"
variant="ghost"
size="xs"
size="sm"
class="h-auto px-2 text-xs"
@click="handleResumeTask"
>
{{ t('Resume') }}
</AppButton>
<AppButton variant="ghost" size="xs" class="h-auto px-2 text-xs" @click="handleDeleteTask">
</Button>
<Button variant="ghost" size="sm" class="h-auto px-2 text-xs" @click="handleDeleteTask">
{{ t('Delete Record') }}
</AppButton>
</Button>
</div>
</div>
<AppProgress :value="task.progress" :processing="task.status === 'uploading'" :height="2" />
@@ -41,7 +41,9 @@
</template>
<script setup lang="ts">
import { AppButton, AppProgress } from '@/components/app'
import { Button } from '@/components/ui/button'
import { AppProgress } from '@/components/app'
import type { UploadTask } from '~/lib/upload-task-manager'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
import { formatBytes } from '~/utils/functions'
+22 -15
View File
@@ -3,21 +3,24 @@
<AppCard padded>
<AppDataTable :table="table" :is-loading="loading" :empty-title="t('No Versions')" />
<div class="mt-4 flex justify-end">
<AppButton variant="outline" @click="closeModal">{{ t('Close') }}</AppButton>
<Button variant="outline" @click="closeModal">{{ t('Close') }}</Button>
</div>
</AppCard>
</AppModal>
</template>
<script setup lang="tsx">
import { AppButton, AppCard, AppDataTable, AppModal } from '@/components/app'
import { Button } from '@/components/ui/button'
import { AppCard, AppModal } from '@/components/app'
import AppDataTable from '@/components/app/data-table/AppDataTable.vue'
import { useDataTable } from '@/components/app/data-table'
import { GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import dayjs from 'dayjs'
import { saveAs } from 'file-saver'
import JSZip from 'jszip'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ColumnDef } from '@tanstack/vue-table'
const props = defineProps<{
bucketName: string
@@ -33,6 +36,7 @@ const emit = defineEmits<{
const { t } = useI18n()
const message = useMessage()
const ActionButton = Button as unknown as any
const visibleProxy = computed({
get: () => props.visible,
@@ -44,16 +48,21 @@ const visibleProxy = computed({
const versions = ref<any[]>([])
const loading = ref(false)
const columns = computed(() => [
const objectApi = useObject({ bucket: props.bucketName })
const columns = computed<ColumnDef<any, any>[]>(() => [
{
id: 'versionId',
header: () => t('VersionId'),
cell: ({ row }: any) => shortVersionId(row.original.VersionId),
},
{
id: 'lastModified',
header: () => t('LastModified'),
cell: ({ row }: any) => (row.original.LastModified ? dayjs(row.original.LastModified).format('YYYY-MM-DD HH:mm:ss') : ''),
},
{
id: 'size',
header: () => t('Size'),
cell: ({ row }: any) => (typeof row.original.Size === 'number' ? formatBytes(row.original.Size) : ''),
},
@@ -62,15 +71,15 @@ const columns = computed(() => [
header: () => t('Action'),
cell: ({ row }: any) => (
<div class="flex gap-2">
<AppButton variant="outline" size="sm" onClick={() => previewVersion(row.original)}>
<ActionButton variant="outline" size="sm" onClick={() => previewVersion(row.original)}>
{t('Preview')}
</AppButton>
<AppButton variant="outline" size="sm" onClick={() => downloadVersion(row.original)}>
</ActionButton>
<ActionButton variant="outline" size="sm" onClick={() => downloadVersion(row.original)}>
{t('Download')}
</AppButton>
<AppButton variant="destructive" size="sm" onClick={() => deleteVersion(row.original)}>
</ActionButton>
<ActionButton variant="destructive" size="sm" onClick={() => deleteVersion(row.original)}>
{t('Delete')}
</AppButton>
</ActionButton>
</div>
),
},
@@ -90,9 +99,8 @@ watch(
const fetchVersions = async () => {
loading.value = true
const { getObjectVersions } = useObject({ bucket: props.bucketName })
try {
const response = await getObjectVersions(props.objectKey)
const response = await objectApi.getObjectVersions(props.objectKey)
versions.value = response.Versions ?? []
} catch (error) {
message.error(t('Failed to fetch versions'))
@@ -124,9 +132,8 @@ const downloadVersion = async (row: any) => {
}
const deleteVersion = async (row: any) => {
const { deleteObject } = useObject({ bucket: props.bucketName })
try {
await deleteObject(props.objectKey, row.VersionId)
await objectApi.deleteObject(props.objectKey, row.VersionId)
message.success(t('Delete Success'))
fetchVersions()
emit('refresh-parent')
+24 -21
View File
@@ -4,38 +4,38 @@
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-col">
<div class="flex items-center gap-2">
<AppButton variant="ghost" size="sm" @click="router.back">
<Button variant="ghost" size="sm" @click="router.back">
<Icon name="ri:arrow-left-line" class="size-4" />
</AppButton>
</Button>
<h2 class="text-lg font-semibold">{{ object?.Key }}</h2>
</div>
<p class="text-sm text-muted-foreground">{{ t('Object Detail Description', { bucket: bucketName }) }}</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<AppButton variant="outline" size="sm" @click="download">
<Button variant="outline" size="sm" @click="download">
<Icon name="ri:download-line" class="size-4" />
{{ t('Download') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="copySignedUrl">
</Button>
<Button variant="outline" size="sm" @click="copySignedUrl">
<Icon name="ri:file-copy-line" class="size-4" />
{{ t('Copy Temporary URL') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="() => (showPreview = true)">
</Button>
<Button variant="outline" size="sm" @click="() => (showPreview = true)">
<Icon name="ri:eye-line" class="size-4" />
{{ t('Preview') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="() => (showTagView = true)">
</Button>
<Button variant="outline" size="sm" @click="() => (showTagView = true)">
<Icon name="ri:price-tag-3-line" class="size-4" />
{{ t('Set Tags') }}
</AppButton>
<AppButton variant="destructive" size="sm" @click="confirmDelete">
</Button>
<Button variant="destructive" size="sm" @click="confirmDelete">
<Icon name="ri:delete-bin-5-line" class="size-4" />
{{ t('Delete') }}
</AppButton>
<AppButton variant="outline" size="sm" @click="refresh">
</Button>
<Button variant="outline" size="sm" @click="refresh">
<Icon name="ri:refresh-line" class="size-4" />
{{ t('Refresh') }}
</AppButton>
</Button>
</div>
</div>
</AppCard>
@@ -71,10 +71,10 @@
</AppTag>
</div>
<form class="flex flex-wrap gap-3" @submit.prevent="submitTagForm">
<AppInput v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
<AppInput v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
<AppButton type="submit" variant="primary">{{ t('Add') }}</AppButton>
<AppButton variant="outline" @click="showTagView = false">{{ t('Cancel') }}</AppButton>
<Input v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
<Input v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
<Button type="submit" variant="default">{{ t('Add') }}</Button>
<Button variant="outline" @click="showTagView = false">{{ t('Cancel') }}</Button>
</form>
</AppCard>
</AppModal>
@@ -84,8 +84,11 @@
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import { AppButton, AppCard, AppInput, AppModal, AppTag } from '@/components/app'
import { AppCard, AppModal, AppTag } from '@/components/app'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -114,7 +117,7 @@ const loadObjectInfo = async () => {
object.value = await getObjectInfo(props.objectKey)
signedUrl.value = object.value?.SignedUrl || ''
const response = await getObjectTags(props.objectKey)
tags.value = response.TagSet || []
tags.value = response
} catch (error) {
message.error(t('Failed to fetch object info'))
}
@@ -181,4 +184,4 @@ const download = () => {
}
defineExpose({ refresh })
</script>
</script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="p-6">
<div class="p-4">
<slot></slot>
</div>
</template>
+9 -6
View File
@@ -1,5 +1,8 @@
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppModal } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -120,13 +123,13 @@ const submitForm = async () => {
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label for="policy-name">{{ t('Policy Name') }}</Label>
<AppInput id="policy-name" v-model="form.name" autocomplete="off" />
<Input id="policy-name" v-model="form.name" autocomplete="off" />
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
</div>
<div class="grid gap-2">
<Label for="policy-content">{{ t('Policy Original') }}</Label>
<div class="max-h-[60vh] overflow-auto rounded-md border border-border/60">
<div class="max-h-[60vh] overflow-auto rounded-md border">
<json-editor id="policy-content" v-model="form.content" />
</div>
<p v-if="errors.content" class="text-sm text-destructive">{{ errors.content }}</p>
@@ -135,9 +138,9 @@ const submitForm = async () => {
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
<Button variant="outline" @click="closeModal()">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
</div>
</template>
</AppModal>
</template>
</template>
+39 -27
View File
@@ -10,7 +10,7 @@
<div class="grid gap-3 md:grid-cols-2">
<div class="grid gap-2">
<Label>{{ t('Priority') }}</Label>
<AppInput v-model="formData.level" type="number" min="1" />
<Input v-model="formData.level" type="number" min="1" />
</div>
<div class="grid gap-2">
<Label>{{ t('Mode') }}</Label>
@@ -18,53 +18,53 @@
</div>
<div class="grid gap-2">
<Label>{{ t('Endpoint') }}</Label>
<AppInput v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
<Input v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Bucket') }}</Label>
<AppInput v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
<Input v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
</div>
<div class="grid gap-2">
<Label>{{ t('Secret Key') }}</Label>
<AppInput v-model="formData.secrretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
<Input v-model="formData.secrretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Region') }}</Label>
<AppInput v-model="formData.region" :placeholder="t('Please enter region')" />
<Input v-model="formData.region" :placeholder="t('Please enter region')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Storage Class') }}</Label>
<AppInput v-model="formData.storageType" :placeholder="t('Please enter storage class')" />
<Input v-model="formData.storageType" :placeholder="t('Please enter storage class')" />
</div>
</div>
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
<AppButton variant="outline" size="sm" @click="addTag">
<Button variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</AppButton>
</Button>
</div>
<div v-if="formData.tags.length" class="space-y-3">
<div
v-for="(tag, index) in formData.tags"
:key="index"
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
>
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
<Input v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<AppButton
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<Button
variant="ghost"
size="sm"
class="text-destructive"
@@ -72,7 +72,7 @@
@click="removeTag(index)"
>
<Icon name="ri:delete-bin-line" class="size-4" />
</AppButton>
</Button>
</div>
</div>
</div>
@@ -107,7 +107,7 @@
<div class="space-y-3" v-if="formData.modeType === 'async'">
<div class="grid gap-2">
<Label>{{ t('Health Check Interval (seconds)') }}</Label>
<AppInput
<Input
v-model="formData.timecheck"
type="number"
min="1"
@@ -117,7 +117,7 @@
<div class="grid gap-2">
<Label>{{ t('Bandwidth Limit') }}</Label>
<div class="flex items-center gap-2">
<AppInput v-model="formData.bandwidth" type="number" min="0" class="w-32" />
<Input v-model="formData.bandwidth" type="number" min="0" class="w-32" />
<AppSelect v-model="formData.unit" :options="unitOptions" class="w-28" />
</div>
</div>
@@ -127,16 +127,19 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="handleSave">{{ t('Save') }}</AppButton>
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="handleSave">{{ t('Save') }}</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
import { AppCard, AppModal, AppSelect, AppSwitch } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -285,7 +288,6 @@ const handleSave = async () => {
ID: `replication-rule-${Date.now()}`,
Status: 'Enabled',
Priority: Number.parseInt(formData.level) || 1,
Filter: {},
SourceSelectionCriteria: {
SseKmsEncryptedObjects: { Status: 'Enabled' },
},
@@ -301,18 +303,28 @@ const handleSave = async () => {
},
}
const validTags = formData.tags.filter(tag => tag.key && tag.value)
const filter: any = {}
if (formData.prefix) {
newRule.Filter.Prefix = formData.prefix
filter.Prefix = formData.prefix
}
const validTags = formData.tags.filter(tag => tag.key && tag.value)
if (validTags.length === 1) {
newRule.Filter.Tag = { Key: validTags[0].key, Value: validTags[0].value }
const [singleTag] = validTags
if (singleTag) {
filter.Tag = { Key: singleTag.key, Value: singleTag.value }
}
} else if (validTags.length > 1) {
newRule.Filter.And = {
Prefix: formData.prefix || '',
filter.And = {
...(formData.prefix ? { Prefix: formData.prefix } : {}),
Tags: validTags.map(tag => ({ Key: tag.key, Value: tag.value })),
}
delete filter.Prefix
}
if (Object.keys(filter).length > 0) {
newRule.Filter = filter
}
const existingRules = oldConfig?.ReplicationConfiguration?.Rules ?? []
@@ -345,4 +357,4 @@ defineExpose({
visible.value = true
},
})
</script>
</script>
+9 -6
View File
@@ -8,26 +8,29 @@
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formModel.accessKey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
<Input v-model="formModel.accessKey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
</div>
<div class="grid gap-2">
<Label>{{ t('Secret Key') }}</Label>
<AppInput v-model="formModel.secretKey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
<Input v-model="formModel.secretKey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
</div>
</AppCard>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
<Button variant="outline" @click="closeModal()">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppModal } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -101,4 +104,4 @@ const submitForm = async () => {
submitting.value = false
}
}
</script>
</script>
+17 -14
View File
@@ -19,7 +19,7 @@
</div>
<div v-else class="space-y-5">
<AppCard class="cursor-pointer border border-border/60 transition hover:border-primary" @click="resetType">
<AppCard class="cursor-pointer border transition hover:border-primary" @click="resetType">
<div class="flex items-center gap-3">
<img :src="iconUrl" class="h-10 w-10" alt="" />
<div>
@@ -32,7 +32,7 @@
<AppCard padded class="space-y-4">
<div class="grid gap-2">
<Label>{{ t('Name') }} (A-Z,0-9,_)</Label>
<AppInput
<Input
v-model="formData.name"
:placeholder="t('Please enter name')"
autocomplete="off"
@@ -43,37 +43,37 @@
<div class="grid gap-2">
<Label>{{ t('Endpoint') }}</Label>
<AppInput v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
<Input v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
</div>
<div class="grid gap-2">
<Label>{{ t('Secret Key') }}</Label>
<AppInput v-model="formData.secretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
<Input v-model="formData.secretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Bucket') }}</Label>
<AppInput v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
<Input v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Region') }}</Label>
<AppInput v-model="formData.region" :placeholder="t('Please enter region')" />
<Input v-model="formData.region" :placeholder="t('Please enter region')" />
</div>
<div class="grid gap-2">
<Label>{{ t('Storage Class') }}</Label>
<AppInput v-model="formData.storageclass" :placeholder="t('Please Enter storage class')" />
<Input v-model="formData.storageclass" :placeholder="t('Please Enter storage class')" />
</div>
</AppCard>
</div>
@@ -81,17 +81,20 @@
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :disabled="!formData.type" :loading="submitting" @click="handleSave">
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
<Button variant="default" :disabled="!formData.type" :loading="submitting" @click="handleSave">
{{ t('Save') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppModal } from '@/components/app'
import { Label } from '@/components/ui/label'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -214,4 +217,4 @@ defineExpose({
resetForm()
},
})
</script>
</script>
@@ -4,7 +4,7 @@
v-bind="props"
:class="
cn(
'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
'relative flex flex-col h-screen items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
props.class
)
"
@@ -17,11 +17,11 @@
'[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]',
'[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]',
'[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]',
'[background-image:var(--white-gradient),var(--aurora)] dark:[background-image:var(--dark-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%]',
'[background-image:var(--white-gradient),var(--aurora)] dark:[background-image:var(--dark-gradient),var(--aurora)] bg-size-[300%,200%] bg-position-[50%_50%,50%_50%]',
'aurora-background-gradient-after',
'aurora-gradient-animation',
props.radialGradient &&
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
`mask-[radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)
"
></div>
@@ -51,8 +51,8 @@ const props = withDefaults(defineProps<AuroraBackgroundProps>(), {
after:inset-0
after:[background-image:var(--white-gradient),var(--aurora)]
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
after:[background-size:200%,_100%]
after:[background-attachment:fixed]
after:bg-size-[200%,100%]
after:bg-fixed
after:mix-blend-difference;
}
+31 -7
View File
@@ -16,21 +16,45 @@ function isValidConfig(config: any): config is ConfigItem {
}
const delegatedProps = computed(() => {
if (["ZodObject", "ZodArray"].includes(props.shape?.type))
return { schema: props.shape?.schema }
if (props.shape && ["ZodObject", "ZodArray"].includes(props.shape.type)) {
return { schema: props.shape.schema }
}
return undefined
})
const resolvedComponent = computed(() => {
if (isValidConfig(props.config)) {
if (typeof props.config.component === "string") {
return INPUT_COMPONENTS[props.config.component] ?? INPUT_COMPONENTS.string
}
return props.config.component
}
if (!props.shape) {
throw new Error("Shape is required for auto form field")
}
const handlerKey = DEFAULT_ZOD_HANDLERS[props.shape.type]
if (!handlerKey) {
throw new Error(`Unsupported schema type: ${String(props.shape.type)}`)
}
const component = INPUT_COMPONENTS[handlerKey] ?? INPUT_COMPONENTS.string
if (!component) {
throw new Error(`Component not registered for handler: ${handlerKey}`)
}
return component
})
const { isDisabled, isHidden, isRequired, overrideOptions } = useDependencies(props.fieldName)
</script>
<template>
<component
:is="isValidConfig(config)
? typeof config.component === 'string'
? INPUT_COMPONENTS[config.component!]
: config.component
: INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
:is="resolvedComponent"
v-if="!isHidden"
:field-name="fieldName"
:label="shape.schema?.description"
@@ -76,7 +76,7 @@ provide(FieldContextKey, fieldContext)
:config="config as ConfigItem"
/>
<div class="!my-4 flex justify-end">
<div class="my-4! flex justify-end">
<Button
type="button"
size="icon"
+7 -3
View File
@@ -9,10 +9,11 @@ export type ZodObjectOrWrapped
* Beautify a camelCase string.
* e.g. "myString" -> "My String"
*/
export function beautifyObjectName(string: string) {
export function beautifyObjectName(value?: string) {
const input = value ?? ""
// Remove bracketed indices
// if numbers only return the string
let output = string.replace(/\[\d+\]/g, "").replace(/([A-Z])/g, " $1")
let output = input.replace(/\[\d+\]/g, "").replace(/([A-Z])/g, " $1")
output = output.charAt(0).toUpperCase() + output.slice(1)
return output
}
@@ -27,7 +28,10 @@ export function getIndexIfArray(string: string) {
// Match the index
const match = string.match(indexRegex)
// Extract the index (number)
const index = match ? Number.parseInt(match[1]) : undefined
if (!match || match[1] === undefined) {
return undefined
}
const index = Number.parseInt(match[1])
return index
}
+1 -1
View File
@@ -9,7 +9,7 @@ const props = defineProps<{
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
:class="cn('flex flex-wrap items-center gap-1.5 wrap-break-word text-sm text-muted-foreground sm:gap-2.5', props.class)"
>
<slot />
</ol>
@@ -17,7 +17,7 @@ const delegatedProps = reactiveOmit(props, "class")
v-bind="delegatedProps"
:orientation="props.orientation"
:class="cn(
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
'bg-input relative m-0! self-stretch data-[orientation=vertical]:h-auto',
props.class,
)"
/>
@@ -20,13 +20,13 @@ const forwardedProps = useForwardProps(delegatedProps)
'h-9 w-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
'data-selected:bg-primary data-selected:text-primary-foreground data-selected:opacity-100 data-selected:hover:bg-primary data-selected:hover:text-primary-foreground data-selected:focus:bg-primary data-selected:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
'data-disabled:text-muted-foreground data-disabled:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
'data-unavailable:text-destructive-foreground data-unavailable:line-through',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
'data-outside-view:text-muted-foreground data-outside-view:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
+35 -22
View File
@@ -40,24 +40,51 @@ const props = withDefaults(defineProps<Pick<BaseChartProps<T>, "data" | "colors"
})
type KeyOfT = Extract<keyof T, string>
type Data = typeof props.data[number]
type Data = T
const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
const category = computed(() => props.category as KeyOfT)
const index = computed(() => props.index as KeyOfT)
const chartData = computed(() => props.data ?? [])
const isMounted = useMounted()
const activeSegmentKey = ref<string>()
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.data.filter(d => d[props.category]).filter(Boolean).length))
const legendItems = computed(() => props.data.map((item, i) => ({
name: item[props.index],
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(chartData.value.filter(d => d[category.value]).length))
const legendItems = computed(() => chartData.value.map((item, i) => ({
name: item[index.value],
color: colors.value[i],
inactive: false,
})))
const totalValue = computed(() => props.data.reduce((prev, curr) => {
return prev + curr[props.category]
}, 0))
const segmentEvents = computed(() => ({
[Donut.selectors.segment]: {
click: (d: Data, _ev: PointerEvent, elementIndex: number, elements: HTMLElement[] = []) => {
const segmentKey = d?.data?.[index.value as keyof Data] as unknown as string | undefined
if (!elements.length) {
return
}
if (segmentKey && activeSegmentKey.value === segmentKey) {
activeSegmentKey.value = undefined
elements.forEach(el => {
el.style.opacity = "1"
})
return
}
activeSegmentKey.value = segmentKey
elements.forEach(el => {
el.style.opacity = `${props.filterOpacity}`
})
const target = elements[elementIndex]
if (target) {
target.style.opacity = "1"
}
},
},
}))
const totalValue = computed(() => chartData.value.reduce((prev, curr) => prev + (Number(curr[category.value]) || 0), 0))
</script>
<template>
@@ -78,21 +105,7 @@ const totalValue = computed(() => props.data.reduce((prev, curr) => {
:arc-width="type === 'donut' ? 20 : 0"
:show-background="false"
:central-label="type === 'donut' ? valueFormatter(totalValue) : ''"
:events="{
[Donut.selectors.segment]: {
click: (d: Data, ev: PointerEvent, i: number, elements: HTMLElement[]) => {
if (d?.data?.[index] === activeSegmentKey) {
activeSegmentKey = undefined
elements.forEach(el => el.style.opacity = '1')
}
else {
activeSegmentKey = d?.data?.[index]
elements.forEach(el => el.style.opacity = `${filterOpacity}`)
elements[i].style.opacity = '1'
}
},
},
}"
:events="segmentEvents"
/>
<slot />
+6 -2
View File
@@ -21,7 +21,7 @@ function keepStyling() {
nextTick(() => {
const elements = elRef.value?.querySelectorAll(selector)
const classes = buttonVariants({ variant: "ghost", size: "sm" }).split(" ")
elements?.forEach(el => el.classList.add(...classes, "!inline-flex", "!mr-2"))
elements?.forEach(el => el.classList.add(...classes, "inline-flex!", "mr-2!"))
})
}
@@ -30,8 +30,12 @@ onMounted(() => {
})
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
const targetItem = props.items[i]
if (!targetItem) {
return
}
emits("legendItemClick", d, i)
const isBulletActive = !props.items[i].inactive
const isBulletActive = !targetItem.inactive
const isFilterApplied = props.items.some(i => i.inactive)
if (isFilterApplied && isBulletActive) {
// reset filter
+7 -3
View File
@@ -16,7 +16,7 @@ const props = defineProps<{
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap()
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[] = []) {
const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
if (props.index in d) {
if (wm.has(d)) {
@@ -42,8 +42,12 @@ function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
return wm.get(data)
}
else {
const style = getComputedStyle(elements[i])
const omittedData = [{ name: data.name, value: valueFormatter(data[props.index]), color: style.fill }]
const element = elements[i]
if (!element) {
return ""
}
const style = getComputedStyle(element)
const omittedData = [{ name: data?.name, value: valueFormatter(data?.[props.index]), color: style.fill }]
const componentDiv = document.createElement("div")
const TooltipComponent = props.customTooltip ?? ChartTooltip
createApp(TooltipComponent, { title: d[props.index], data: omittedData }).mount(componentDiv)
+1 -1
View File
@@ -16,7 +16,7 @@ const delegatedProps = reactiveOmit(props, "class")
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
:class="cn('overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground', props.class)"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
+1 -1
View File
@@ -16,7 +16,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<ComboboxItem
v-bind="forwarded"
:class="cn('relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
:class="cn('relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
>
<slot />
</ComboboxItem>
+1 -1
View File
@@ -13,7 +13,7 @@ const forwarded = useForwardPropsEmits(props, emits)
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<Command class="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<slot />
</Command>
</DialogContent>
+1 -1
View File
@@ -33,7 +33,7 @@ onUnmounted(() => {
<ListboxGroup
v-bind="delegatedProps"
:id="id"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
:class="cn('overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground', props.class)"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
+1 -1
View File
@@ -65,7 +65,7 @@ onUnmounted(() => {
v-bind="forwarded"
:id="id"
ref="itemRef"
:class="cn('relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
:class="cn('relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
@select="() => {
filterState.search = ''
}"
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<ContextMenuCheckboxItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
@@ -21,7 +21,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<ContextMenuItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
inset && 'pl-8',
props.class,
)"
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<ContextMenuRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
+5 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue"
import { useForwardPropsEmits } from "reka-ui"
import { computed } from "vue"
import { DrawerRoot } from "vaul-vue"
const props = withDefaults(defineProps<DrawerRootProps>(), {
@@ -10,10 +11,13 @@ const props = withDefaults(defineProps<DrawerRootProps>(), {
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
const forwardedProps = computed(() => ({
...(forwarded.value as Record<string, unknown>),
}))
</script>
<template>
<DrawerRoot v-bind="forwarded">
<DrawerRoot v-bind="forwardedProps">
<slot />
</DrawerRoot>
</template>
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class=" cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class,
)"
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
+1 -1
View File
@@ -11,7 +11,7 @@ const props = defineProps<{
<p
data-slot="field-description"
:class="cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'text-muted-foreground text-sm leading-normal font-normal group-has-data-[orientation=horizontal]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class,
+1 -1
View File
@@ -11,7 +11,7 @@ const props = defineProps<{
<div
data-slot="field-group"
:class="cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
props.class,
)"
>
+2 -2
View File
@@ -13,8 +13,8 @@ const props = defineProps<{
data-slot="field-label"
:class="cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&_>[data-slot=field]]:p-4',
'has-[[data-state=checked]]:bg-primary/5 has-[[data-state=checked]]:border-primary dark:has-[[data-state=checked]]:bg-primary/10',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
props.class,
)"
>
+4 -4
View File
@@ -6,15 +6,15 @@ export const fieldVariants = cva(
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
vertical: ["flex-col *:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"*:data-[slot=field-label]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto",
"@md/field-group:*:data-[slot=field-label]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
+4 -3
View File
@@ -48,7 +48,7 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits(['animationStart', 'animationComplete']);
const currentWord = ref(props.words[0]);
const currentWord = ref<string>(props.words[0] ?? "");
const isVisible = ref(true);
const timeoutId = ref<number | null>(null);
@@ -56,8 +56,9 @@ function startAnimation() {
isVisible.value = false;
setTimeout(() => {
const currentIndex = props.words.indexOf(currentWord.value);
const nextWord = props.words[currentIndex + 1] || props.words[0];
const words = props.words ?? [];
const currentIndex = words.indexOf(currentWord.value);
const nextWord = words[currentIndex + 1] ?? words[0] ?? "";
currentWord.value = nextWord;
isVisible.value = true;
}, 600);
+60 -25
View File
@@ -87,17 +87,17 @@ const defaultGlobeConfig: GlobeConfig = {
...props.globeConfig,
};
const githubGlobeRef = ref<HTMLCanvasElement>();
const globeData = ref<GlobeData[]>();
const githubGlobeRef = ref<HTMLCanvasElement | null>(null);
const globeData = ref<GlobeData[]>([]);
let numberOfRings: number[] = [];
let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let controls: OrbitControls;
let renderer: WebGLRenderer | null = null;
let scene: Scene | null = null;
let camera: PerspectiveCamera | null = null;
let controls: OrbitControls | null = null;
let globe: ThreeGlobe;
let globe: ThreeGlobe | null = null;
onMounted(() => {
setupScene();
@@ -110,14 +110,30 @@ onMounted(() => {
window.addEventListener('resize', onWindowResize, false);
watch(globeData, () => {
if (!globe || !globeData.value) return;
if (!globe || globeData.value.length === 0) return;
numberOfRings = genRandomNumbers(0, props.data.length, Math.floor((props.data.length * 4) / 5));
const arcLength = props.data?.length ?? 0;
if (arcLength === 0) return;
numberOfRings = genRandomNumbers(0, arcLength, Math.floor((arcLength * 4) / 5));
globe.ringsData(globeData.value.filter((d, i) => numberOfRings.includes(i)));
});
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize, false);
if (controls) {
controls.dispose();
controls = null;
}
renderer?.dispose();
renderer = null;
scene = null;
camera = null;
globe = null;
});
function setupScene() {
if (!githubGlobeRef.value) {
throw new Error('Canvas not initialized');
@@ -202,11 +218,13 @@ function initGlobe() {
globeMaterial.emissiveIntensity = defaultGlobeConfig.emissiveIntensity || 0.1;
globeMaterial.shininess = defaultGlobeConfig.shininess || 0.9;
scene.add(globe);
if (scene) {
scene.add(globe);
}
}
function onWindowResize() {
if (!githubGlobeRef.value) {
if (!githubGlobeRef.value || !renderer || !camera) {
return;
}
@@ -219,22 +237,31 @@ function onWindowResize() {
renderer.setSize(width, height);
}
function startAnimation() {
if (!globe || !globeData.value!) return;
if (!globe) return;
const arcs = props.data ?? [];
const strokeValues = [0.32, 0.28, 0.3] as const;
const getStroke = () => {
if (!strokeValues.length) {
return 0.3;
}
const index = Math.floor(Math.random() * strokeValues.length);
return strokeValues[index] ?? strokeValues[0];
};
globe
.arcsData(props.data)
.arcsData(arcs)
.arcStartLat((d: any) => d.startLat * 1)
.arcStartLng((d: any) => d.startLng * 1)
.arcEndLat((d: any) => d.endLat * 1)
.arcEndLng((d: any) => d.endLng * 1)
.arcColor((e: any) => e.color)
.arcAltitude((e: any) => e.arcAlt * 1)
.arcStroke((e: any) => [0.32, 0.28, 0.3][Math.round(Math.random() * 4)])
.arcStroke(() => getStroke())
.arcDashLength(defaultGlobeConfig.arcLength!)
.arcDashInitialGap((e: any) => e.order * 1)
.arcDashGap(15)
.arcDashAnimateTime(defaultGlobeConfig.arcTime!)
.pointsData(props.data)
.pointsData(arcs)
.pointColor((e: any) => e.color)
.pointsMerge(true)
.pointAltitude(0.0)
@@ -244,10 +271,14 @@ function startAnimation() {
.ringColor((e: any) => (t: any) => e.color(t))
.ringMaxRadius(defaultGlobeConfig.maxRings!)
.ringPropagationSpeed(3)
.ringRepeatPeriod((defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!);
.ringRepeatPeriod((defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / (defaultGlobeConfig.rings || 1));
}
function animate() {
if (!globe || !renderer || !scene || !camera) {
requestAnimationFrame(animate);
return;
}
globe.rotation.y += 0.01; // Rotate globe
renderer.render(scene, camera);
@@ -256,22 +287,26 @@ function animate() {
}
function buildData() {
const arcs = props.data;
let points = [];
const arcs = props.data ?? [];
const pointSize = props.globeConfig?.pointSize ?? defaultGlobeConfig.pointSize ?? 1;
const points: GlobeData[] = [];
for (let i = 0; i < arcs.length; i++) {
const arc = arcs[i];
const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
points.push({
size: props.globeConfig.pointSize,
if (!arc) continue;
const rgb = hexToRgb(arc.color ?? '#ffffff');
const basePoint = {
size: pointSize,
order: arc.order,
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
};
points.push({
...basePoint,
lat: arc.startLat,
lng: arc.startLng,
});
points.push({
size: props.globeConfig.pointSize,
order: arc.order,
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
...basePoint,
lat: arc.endLat,
lng: arc.endLng,
});
@@ -285,7 +320,7 @@ function buildData() {
globeData.value = filteredPoints;
}
function hexToRgb(color: string) {
function hexToRgb(color: string): { r: number; g: number; b: number } {
let hex = color.replace(/^#/, '');
// If the hex code is 3 characters, expand it to 6 characters
+1 -1
View File
@@ -34,7 +34,7 @@ export const itemVariants = cva(
)
export const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none group-has-data-[slot=item-description]/item:translate-y-0.5",
{
variants: {
variant: {
+1 -1
View File
@@ -12,7 +12,7 @@ const props = defineProps<{
:class="cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
'[&_svg:not([class*=\'size-\'])]:size-3',
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
'in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10',
props.class,
)"
>
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<MenubarCheckboxItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
+1 -1
View File
@@ -22,7 +22,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<MenubarItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
inset && 'pl-8',
props.class,
)"
+1 -1
View File
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<MenubarRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)"
>
@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<NavigationMenuIndicator
v-bind="forwardedProps"
:class="cn('top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in', props.class)"
:class="cn('top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in', props.class)"
>
<div class="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuIndicator>
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'origin-top-center relative mt-1.5 h-[--reka-navigation-menu-viewport-height] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[--reka-navigation-menu-viewport-width] left-[var(--reka-navigation-menu-viewport-left)]',
'origin-top-center relative mt-1.5 h-(--reka-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left)',
props.class,
)
"
+1 -1
View File
@@ -10,5 +10,5 @@ export { default as NavigationMenuTrigger } from "./NavigationMenuTrigger.vue"
export { default as NavigationMenuViewport } from "./NavigationMenuViewport.vue"
export const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
)
@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5', props.class)">
<div :class="cn('relative *:data-[slot=input]:has-data-[slot=increment]:pr-5 *:data-[slot=input]:has-data-[slot=decrement]:pl-5', props.class)">
<slot />
</div>
</template>
+2 -2
View File
@@ -5,9 +5,9 @@
cn(
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
// Radiant effect
'radiant-animation bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
'radiant-animation bg-clip-text bg-no-repeat bg-position-[0_0] bg-size-[var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
// Radiant gradient
'bg-gradient-to-r from-transparent via-black via-50% to-transparent dark:via-white',
'bg-linear-to-r from-transparent via-black via-50% to-transparent dark:via-white',
$props.class
)
"
@@ -17,18 +17,18 @@ const forwardedProps = useForwardProps(delegatedProps)
<RangeCalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal data-[selected]:opacity-100',
'h-9 w-9 p-0 font-normal data-selected:opacity-100',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selection Start
'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground',
'data-selection-start:bg-primary data-selection-start:text-primary-foreground data-selection-start:hover:bg-primary data-selection-start:hover:text-primary-foreground data-selection-start:focus:bg-primary data-selection-start:focus:text-primary-foreground',
// Selection End
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
'data-selection-end:bg-primary data-selection-end:text-primary-foreground data-selection-end:hover:bg-primary data-selection-end:hover:text-primary-foreground data-selection-end:focus:bg-primary data-selection-end:focus:text-primary-foreground',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
'data-outside-view:text-muted-foreground data-outside-view:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
'data-disabled:text-muted-foreground data-disabled:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
'data-unavailable:text-destructive-foreground data-unavailable:line-through',
props.class,
)"
v-bind="forwardedProps"
+1 -1
View File
@@ -15,7 +15,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
<template v-if="props.withHandle">
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical class="h-2.5 w-2.5" />
+1 -1
View File
@@ -41,7 +41,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
"
>
<SelectScrollUpButton />
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]')">
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width)')">
<slot />
</SelectViewport>
<SelectScrollDownButton />
+1 -1
View File
@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class,
)
"
+1 -1
View File
@@ -17,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<SelectTrigger
v-bind="forwardedProps"
:class="cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
props.class,
)"
>
+8 -8
View File
@@ -20,7 +20,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
<template>
<div
v-if="collapsible === 'none'"
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', props.class)"
:class="cn('flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground', props.class)"
v-bind="$attrs"
>
<slot />
@@ -31,7 +31,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
data-sidebar="sidebar"
data-mobile="true"
:side="side"
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
class="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
@@ -52,24 +52,24 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="cn(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
'duration-200 relative h-svh w-(--sidebar-width) bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
)"
/>
<div
:class="cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+_2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
props.class,
)"
v-bind="$attrs"
+1 -1
View File
@@ -11,7 +11,7 @@ const props = defineProps<{
<main
:class="cn(
'relative flex min-h-svh flex-1 flex-col bg-background',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
'peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
props.class,
)"
>
@@ -26,7 +26,7 @@ const width = computed(() => {
/>
<Skeleton
class="h-4 flex-1 max-w-[--skeleton-width]"
class="h-4 flex-1 max-w-(--skeleton-width)"
data-sidebar="menu-skeleton-text"
:style="{ '--skeleton-width': width }"
/>
+1 -1
View File
@@ -72,7 +72,7 @@ provideSidebarContext({
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
}"
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', props.class)"
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar', props.class)"
v-bind="$attrs"
>
<slot />
+1 -1
View File
@@ -18,7 +18,7 @@ const { toggleSidebar } = useSidebar()
title="Toggle Sidebar"
:class="cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
+2 -2
View File
@@ -36,7 +36,7 @@ export { default as SidebarTrigger } from "./SidebarTrigger.vue"
export { useSidebar } from "./utils"
export const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -47,7 +47,7 @@ export const sidebarMenuButtonVariants = cva(
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
+1 -1
View File
@@ -20,7 +20,7 @@ const forwarded = useForwardProps(delegatedProps)
:class="cn(
'inline-flex items-center justify-center rounded-full text-muted-foreground/50 w-10 h-10',
// Disabled
'group-data-[disabled]:text-muted-foreground group-data-[disabled]:opacity-50',
'group-data-disabled:text-muted-foreground group-data-disabled:opacity-50',
// Active
'group-data-[state=active]:bg-primary group-data-[state=active]:text-primary-foreground',
// Completed
+1 -1
View File
@@ -16,7 +16,7 @@ const forwarded = useForwardProps(delegatedProps)
<StepperItem
v-slot="slotProps"
v-bind="forwarded"
:class="cn('flex items-center gap-2 group data-[disabled]:pointer-events-none', props.class)"
:class="cn('flex items-center gap-2 group data-disabled:pointer-events-none', props.class)"
>
<slot v-bind="slotProps" />
</StepperItem>
+1 -1
View File
@@ -19,7 +19,7 @@ const forwarded = useForwardProps(delegatedProps)
:class="cn(
'bg-muted',
// Disabled
'group-data-[disabled]:bg-muted group-data-[disabled]:opacity-50',
'group-data-disabled:bg-muted group-data-disabled:opacity-50',
// Completed
'group-data-[state=completed]:bg-accent-foreground',
props.class,
+2 -2
View File
@@ -1,8 +1,8 @@
<template>
<div class="relative aspect-[2/1] w-full rounded-lg bg-white font-sans dark:bg-black">
<div class="relative aspect-2/1 w-full rounded-lg bg-white font-sans dark:bg-black">
<NuxtImg
:src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]"
class="pointer-events-none size-full select-none mask-[linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]"
alt="world map"
height="495"
width="1056"
+16 -18
View File
@@ -1,10 +1,8 @@
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
class="w-full items-center justify-between gap-2 rounded-none border-t border-sidebar-border px-4 py-3 text-left transition-[padding] duration-200 group-data-[collapsible=icon]:h-12 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
>
<Button variant="ghost"
class="w-full items-center justify-between gap-2 rounded-none px-4 py-3 text-left transition-[padding] duration-200 group-data-[collapsible=icon]:h-12 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0">
<div class="flex items-center gap-3">
<span class="flex h-9 w-9 items-center justify-center rounded-full border bg-muted">
<img src="~/assets/img/rustfs.png" alt="RustFS" class="h-8 w-8 rounded-full object-cover" />
@@ -24,34 +22,34 @@
</template>
<script lang="ts" setup>
import { Icon } from '#components';
import { Button } from '@/components/ui/button';
import { Icon } from '#components'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { defineProps, toRef, withDefaults } from 'vue';
import { useI18n } from 'vue-i18n';
} from '@/components/ui/dropdown-menu'
import { defineProps, toRef, withDefaults } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n();
const { logout } = useAuth();
const router = useRouter();
const { t } = useI18n()
const { logout } = useAuth()
const router = useRouter()
const props = withDefaults(
defineProps<{
isCollapsed?: boolean;
isCollapsed?: boolean
}>(),
{
isCollapsed: false,
}
);
)
const isCollapsed = toRef(props, 'isCollapsed');
const isCollapsed = toRef(props, 'isCollapsed')
const handleLogout = async () => {
await logout();
router.push('/auth/login');
};
await logout()
router.push('/auth/login')
}
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<AppModal v-model="visible" :title="group.name || t('Members')" size="lg" :close-on-backdrop="false">
<AppCard padded class="space-y-4">
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
<div class="flex items-center justify-between rounded-md border px-3 py-2">
<span class="text-sm text-muted-foreground">{{ t('Status') }}</span>
<AppSwitch v-model="statusBoolean" />
</div>
+9 -7
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Icon } from '#components'
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
import { AppCard, AppModal } from '@/components/app'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
@@ -106,7 +108,7 @@ const submitForm = async () => {
<AppCard padded class="space-y-6">
<div class="space-y-2">
<Label class="text-sm font-medium">{{ t('Name') }}</Label>
<AppInput v-model="formModel.group" autocomplete="off" />
<Input v-model="formModel.group" autocomplete="off" />
</div>
<div class="space-y-2">
@@ -161,13 +163,13 @@ const submitForm = async () => {
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="closeModal()">
<Button variant="outline" @click="closeModal()">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">
</Button>
<Button variant="default" :loading="submitting" @click="submitForm">
{{ t('Submit') }}
</AppButton>
</Button>
</div>
</template>
</AppModal>
</template>
</template>
+11 -9
View File
@@ -2,12 +2,12 @@
<AppCard padded class="space-y-4">
<div v-if="!editStatus" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="w-full sm:max-w-xs">
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
<Input v-model="searchTerm" :placeholder="t('Search Policy')" />
</div>
<AppButton variant="secondary" class="inline-flex items-center gap-2" @click="startEditing">
<Button variant="secondary" class="inline-flex items-center gap-2" @click="startEditing">
<Icon class="size-4" name="ri:add-line" />
{{ t('Edit Policy') }}
</AppButton>
</Button>
</div>
<div v-else class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
@@ -56,12 +56,12 @@
</div>
</div>
<div class="flex items-center gap-2 sm:self-start">
<AppButton variant="ghost" @click="cancelEditing">
<Button variant="ghost" @click="cancelEditing">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="secondary" @click="changePolicies">
</Button>
<Button variant="secondary" @click="changePolicies">
{{ t('Submit') }}
</AppButton>
</Button>
</div>
</div>
@@ -88,8 +88,10 @@
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Icon } from '#components'
import { AppButton, AppCard, AppInput } from '@/components/app'
import { AppCard } from '@/components/app'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
@@ -202,4 +204,4 @@ const changePolicies = async () => {
message.error(t('Edit Failed'))
}
}
</script>
</script>
@@ -3,11 +3,11 @@
<AppCard padded class="space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="w-full sm:max-w-xs">
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
<Input v-model="searchTerm" :placeholder="t('Search Policy')" />
</div>
<AppButton variant="secondary" :disabled="!checkedKeys.length || submitting" @click="changePolicies">
<Button variant="secondary" :disabled="!checkedKeys.length || submitting" @click="changePolicies">
{{ t('Submit') }}
</AppButton>
</Button>
</div>
<Table class="overflow-hidden rounded-lg border">
@@ -26,7 +26,10 @@
<TableBody v-if="filteredPolicies.length">
<TableRow v-for="policy in filteredPolicies" :key="policy.name">
<TableCell>
<AppCheckbox :checked="isSelected(policy.name)" @update:checked="value => toggleSelection(policy.name, value)" />
<AppCheckbox
:checked="isSelected(policy.name)"
@update:checked="(value: boolean) => toggleSelection(policy.name, value)"
/>
</TableCell>
<TableCell class="font-medium">{{ policy.name }}</TableCell>
</TableRow>
@@ -44,7 +47,10 @@
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppCheckbox, AppInput, AppModal } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppCheckbox, AppModal } from '@/components/app'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -149,4 +155,4 @@ const changePolicies = async () => {
defineExpose({
openDialog,
})
</script>
</script>
+1 -1
View File
@@ -36,7 +36,7 @@
<Checkbox
:checked="checkedKeys.includes(rowKey(row))"
aria-label="Select group"
@update:checked="value => toggleRow(rowKey(row), value)"
@update:checked="(value: boolean) => toggleRow(rowKey(row), value)"
/>
</TableCell>
<TableCell class="font-medium">{{ row.name }}</TableCell>
+1 -1
View File
@@ -50,7 +50,7 @@
<Checkbox
:checked="checkedKeys.includes(rowKey(row))"
aria-label="Select user"
@update:checked="value => toggleRow(rowKey(row), value)"
@update:checked="(value: boolean) => toggleRow(rowKey(row), value)"
/>
</TableCell>
<TableCell class="font-medium">{{ row.accessKey }}</TableCell>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<AppModal v-model="visible" :title="user.accessKey || t('Account')" size="lg" :close-on-backdrop="false">
<AppCard padded class="space-y-4">
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
<div class="flex items-center justify-between rounded-md border px-3 py-2">
<span class="text-sm text-muted-foreground">{{ t('Status') }}</span>
<AppSwitch v-model="statusBoolean" />
</div>
+18 -15
View File
@@ -4,12 +4,12 @@
<AppCard padded class="space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="w-full sm:max-w-xs">
<AppInput v-model="searchTerm" :placeholder="t('Search Account')" />
<Input v-model="searchTerm" :placeholder="t('Search Account')" />
</div>
<AppButton variant="secondary" class="inline-flex items-center gap-2" @click="addItem">
<Button variant="secondary" class="inline-flex items-center gap-2" @click="addItem">
<Icon class="size-4" name="ri:add-line" />
{{ t('Add Account') }}
</AppButton>
</Button>
</div>
<Table class="overflow-hidden rounded-lg border">
@@ -34,17 +34,17 @@
<TableCell class="text-center">{{ account.name || '-' }}</TableCell>
<TableCell>
<div class="flex justify-center gap-2">
<AppButton variant="ghost" size="sm" class="h-8 w-8 p-0" @click="openEditItem(account)">
<Button variant="ghost" size="sm" class="h-8 w-8 p-0" @click="openEditItem(account)">
<Icon class="size-4" name="ri:edit-2-line" />
</AppButton>
<AppButton
</Button>
<Button
variant="ghost"
size="sm"
class="h-8 w-8 p-0 text-destructive"
@click="confirmDelete(account)"
>
<Icon class="size-4" name="ri:delete-bin-5-line" />
</AppButton>
</Button>
</div>
</TableCell>
</TableRow>
@@ -65,11 +65,11 @@
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formModel.accessKey" autocomplete="off" />
<Input v-model="formModel.accessKey" autocomplete="off" />
</div>
<div class="space-y-2">
<Label>{{ t('Secret Key') }}</Label>
<AppInput v-model="formModel.secretKey" type="password" autocomplete="off" />
<Input v-model="formModel.secretKey" type="password" autocomplete="off" />
</div>
<div class="space-y-2">
<Label>{{ t('Expiration') }}</Label>
@@ -77,7 +77,7 @@
</div>
<div class="space-y-2">
<Label>{{ t('Name') }}</Label>
<AppInput v-model="formModel.name" autocomplete="off" />
<Input v-model="formModel.name" autocomplete="off" />
</div>
<div class="space-y-2 md:col-span-2">
<Label>{{ t('Description') }}</Label>
@@ -86,7 +86,7 @@
</div>
<div class="space-y-2">
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
<div class="flex items-center justify-between rounded-md border px-3 py-2">
<div>
<p class="text-sm font-medium">{{ t('Use main account policy') }}</p>
<p class="text-xs text-muted-foreground">
@@ -103,8 +103,8 @@
</div>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="cancelAdd">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
<Button variant="outline" @click="cancelAdd">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
</div>
</AppCard>
</template>
@@ -118,8 +118,11 @@
</template>
<script setup lang="ts">
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import { AppButton, AppCard, AppDateTimePicker, AppInput, AppSwitch, AppTextarea } from '@/components/app'
import { AppCard, AppDateTimePicker, AppSwitch, AppTextarea } from '@/components/app'
import { Label } from '@/components/ui/label'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import dayjs from 'dayjs'
@@ -331,4 +334,4 @@ const deleteItem = async (row: any) => {
message.error(t('Delete Failed'))
}
}
</script>
</script>
+13 -10
View File
@@ -3,7 +3,7 @@
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label>{{ t('Access Key') }}</Label>
<AppInput v-model="formModel.accessKey" disabled />
<Input v-model="formModel.accessKey" disabled />
</div>
<div class="space-y-2">
@@ -17,17 +17,17 @@
<div class="space-y-2">
<Label>{{ t('Name') }}</Label>
<AppInput v-model="formModel.name" autocomplete="off" />
<Input v-model="formModel.name" autocomplete="off" />
</div>
<div class="space-y-2">
<Label>{{ t('Description') }}</Label>
<AppInput v-model="formModel.description" autocomplete="off" />
<Input v-model="formModel.description" autocomplete="off" />
</div>
</div>
<div class="space-y-4">
<div class="flex flex-col gap-3 rounded-md border border-border/60 p-3">
<div class="flex flex-col gap-3 rounded-md border p-3">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium">{{ t('Use Main Account Policy') }}</p>
@@ -55,18 +55,21 @@
</div>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="cancelEdit">
<Button variant="outline" @click="cancelEdit">
{{ t('Cancel') }}
</AppButton>
<AppButton variant="primary" :loading="submitting" @click="submitForm">
</Button>
<Button variant="default" :loading="submitting" @click="submitForm">
{{ t('Submit') }}
</AppButton>
</Button>
</div>
</AppCard>
</template>
<script setup lang="ts">
import { AppButton, AppCard, AppDateTimePicker, AppInput, AppSwitch } from '@/components/app'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { AppCard, AppDateTimePicker, AppSwitch } from '@/components/app'
import { Label } from '@/components/ui/label'
import dayjs from 'dayjs'
import { computed, reactive, ref, watch } from 'vue'
@@ -194,4 +197,4 @@ const submitForm = async () => {
submitting.value = false
}
}
</script>
</script>

Some files were not shown because too many files have changed in this diff Show More