You've already forked obsidian-visualiser
Move all base components to an isolated folder
This commit is contained in:
22
components/base/Avatar.vue
Normal file
22
components/base/Avatar.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AvatarRoot class="inline-flex h-12 w-12 select-none items-center justify-center overflow-hidden align-middle">
|
||||
<AvatarImage class="h-full w-full object-cover" :src="src" asChild @loading-status-change="(status) => loading = status === 'loading'">
|
||||
<img :src="src" />
|
||||
</AvatarImage>
|
||||
<AvatarFallback :delay-ms="0" class="text-light-100 dark:text-dark-100 leading-1 flex h-full w-full items-center justify-center bg-light-25 dark:bg-dark-25 font-medium">
|
||||
<Loading v-if="loading" />
|
||||
<Icon v-else-if="!!icon" :icon="icon" class="w-8 h-8" />
|
||||
<span v-else-if="!!text">{{ text }}</span>
|
||||
</AvatarFallback>
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { src, icon, text } = defineProps<{
|
||||
src: string
|
||||
icon?: string
|
||||
text?: string
|
||||
}>();
|
||||
const loading = ref(true);
|
||||
</script>
|
||||
13
components/base/Button.vue
Normal file
13
components/base/Button.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<button class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40"
|
||||
:class="{'p-1': icon, 'h-[35px] px-[15px]': !icon}" >
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { icon = false } = defineProps<{
|
||||
icon?: boolean
|
||||
}>();
|
||||
</script>
|
||||
46
components/base/Collapsible.vue
Normal file
46
components/base/Collapsible.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<CollapsibleRoot v-model:open="model" :disabled="disabled">
|
||||
<div class="flex flex-row justify-center items-center">
|
||||
<span v-if="!!label">{{ label }}</span>
|
||||
<CollapsibleTrigger class="ms-4" asChild>
|
||||
<Button icon :disabled="disabled">
|
||||
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
|
||||
<Icon v-else icon="radix-icons:row-spacing" class="h-4 w-4" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<slot name="alwaysVisible"></slot>
|
||||
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]">
|
||||
<slot></slot>
|
||||
</CollapsibleContent>
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { label, disabled = false } = defineProps<{
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
}>();
|
||||
const model = defineModel<boolean>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes collapseOpen {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes collapseClose {
|
||||
from {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
components/base/Dialog.vue
Normal file
38
components/base/Dialog.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<DialogRoot v-if="!priority">
|
||||
<DialogTrigger asChild><Button>{{ label }}</Button></DialogTrigger>
|
||||
<DialogPortal v-if="!disabled">
|
||||
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<DialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<DialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</DialogTitle>
|
||||
<DialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</DialogDescription>
|
||||
<slot />
|
||||
<DialogClose v-if="iconClose" class="text-light-100 dark:text-dark-100 absolute top-2 right-2 inline-flex h-6 w-6 appearance-none items-center justify-center outline-none text-xl" aria-label="Close">
|
||||
<span aria-hidden>×</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
<AlertDialogRoot v-else>
|
||||
<AlertDialogTrigger asChild><Button>{{ label }}</Button></AlertDialogTrigger>
|
||||
<AlertDialogPortal v-if="!disabled">
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</AlertDialogTitle>
|
||||
<AlertDialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</AlertDialogDescription>
|
||||
<slot />
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { label, title, description, priority = false, disabled = false, iconClose = true } = defineProps<{
|
||||
label?: string
|
||||
title?: string
|
||||
description?: string
|
||||
priority?: boolean
|
||||
disabled?: boolean
|
||||
iconClose?: boolean
|
||||
}>();
|
||||
</script>
|
||||
21
components/base/HoverCard.vue
Normal file
21
components/base/HoverCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<HoverCardRoot :open-delay="delay">
|
||||
<HoverCardTrigger class="inline-block cursor-help outline-none" asChild>
|
||||
<slot></slot>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal v-if="!disabled">
|
||||
<HoverCardContent :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
|
||||
<slot name="content"></slot>
|
||||
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCardRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{
|
||||
delay?: number
|
||||
disabled?: boolean
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
}>();
|
||||
</script>
|
||||
9
components/base/Loading.vue
Normal file
9
components/base/Loading.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<span :class="{'w-6 h-6 border-4 after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 border-2 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 border-[6px] after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="rounded-full border-light-35 dark:border-dark-35 after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { size = 'normal' } = defineProps<{
|
||||
size?: 'small' | 'normal' | 'large'
|
||||
}>();
|
||||
</script>
|
||||
23
components/base/NumberPicker.vue
Normal file
23
components/base/NumberPicker.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Label class="my-2 flex">{{ label }}
|
||||
<NumberFieldRoot :min="min" :max="max" v-model="model" :disabled="disabled" :step="step" class="ms-4 flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
|
||||
<NumberFieldDecrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:minus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldDecrement>
|
||||
<NumberFieldInput class="text-sm tabular-nums w-20 appearance-none bg-transparent px-2 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
<NumberFieldIncrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:plus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldIncrement>
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const { min = 0, max = 100, disabled = false, step = 1, label } = defineProps<{
|
||||
min?: number
|
||||
max?: number
|
||||
disabled?: boolean
|
||||
step?: number
|
||||
label?: string
|
||||
}>();
|
||||
const model = defineModel<number>();
|
||||
</script>
|
||||
20
components/base/PinPicker.vue
Normal file
20
components/base/PinPicker.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<Label class="my-2">{{ label }}
|
||||
<PinInputRoot :disabled="disabled" :default-value="model?.split('')" @update:model-value="(v) => model = v.join('')" @complete="() => emit('complete')" class="flex gap-2 items-center mt-1">
|
||||
<PinInputInput :type="hidden ? 'password' : undefined" v-for="(id, index) in amount" :key="id" :index="index" class="border border-light-35 dark:border-dark-35 w-10 h-10 outline-none
|
||||
bg-light-20 dark:bg-dark-20 text-center text-light-100 dark:text-dark-100 placeholder:text-light-60 dark:placeholder:text-dark-60 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 caret-light-50 dark:caret-dark-50" />
|
||||
</PinInputRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { hidden = false, amount, label, disabled = false } = defineProps<{
|
||||
hidden?: boolean
|
||||
label?: string
|
||||
amount: number
|
||||
disabled?: boolean
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
const emit = defineEmits(['complete']);
|
||||
</script>
|
||||
13
components/base/Progress.vue
Normal file
13
components/base/Progress.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<ProgressRoot :max="max" v-model="model" class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full transition-[width] duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]" :style="`width: ${((model ?? 0) / max) * 100}%`" />
|
||||
</ProgressRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { max = 100, shape = 'normal' } = defineProps<{
|
||||
max?: number
|
||||
shape?: 'thin' | 'normal' | 'large'
|
||||
}>();
|
||||
const model = defineModel<number>();
|
||||
</script>
|
||||
30
components/base/RadioInput.vue
Normal file
30
components/base/RadioInput.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<RadioGroupRoot :disabled="disabled" v-model="model" class="flex flex-col gap-2 p-2">
|
||||
<Label v-for="option in options" class="flex items-center gap-2">
|
||||
<RadioGroupItem :disabled="(option as RadioOption).disabled ?? false"
|
||||
:value="(option as RadioOption).value ?? option"
|
||||
class="border border-light-60 dark:border-dark-35 bg-light-20 dark:bg-dark-25 w-5 h-5 outline-none cursor-default hover:border-light-70 dark:hover:border-dark-40
|
||||
focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
|
||||
<RadioGroupIndicator>
|
||||
<Icon icon="radix-icons:check" class="relative w-5 h-5 -top-px -left-px" />
|
||||
</RadioGroupIndicator>
|
||||
</RadioGroupItem>
|
||||
{{ (option as RadioOption).label ?? option }}
|
||||
</Label>
|
||||
</RadioGroupRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
export interface RadioOption {
|
||||
label: string
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const { disabled = false, options } = defineProps<{
|
||||
disabled?: boolean
|
||||
options: string[] | RadioOption[]
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
</script>
|
||||
39
components/base/Select.vue
Normal file
39
components/base/Select.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<Label>{{ label }}
|
||||
<SelectRoot :v-model="model">
|
||||
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
||||
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
||||
data-[placeholder]:text-light-50 dark:data-[placeholder]:text-dark-50 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
hover:border-light-50 dark:hover:border-dark-50">
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectPortal :disabled="disabled">
|
||||
<SelectContent :position="position"
|
||||
class="min-w-[160px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-40">
|
||||
<SelectScrollUpButton>
|
||||
<Icon icon="radix-icons:chevron-up" />
|
||||
</SelectScrollUpButton>
|
||||
<SelectViewport class="p-1">
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton>
|
||||
<Icon icon="radix-icons:chevron-down" />
|
||||
</SelectScrollDownButton>
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</SelectRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { placeholder, disabled = false, position = 'popper', label } = defineProps<{
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
position?: 'item-aligned' | 'popper'
|
||||
label?: string
|
||||
}>();
|
||||
const model = defineModel();
|
||||
</script>
|
||||
14
components/base/SelectGroup.vue
Normal file
14
components/base/SelectGroup.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<SelectGroup :disabled="disabled" class="">
|
||||
<SelectLabel class="">{{ label }}</SelectLabel>
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SelectGroup } from 'radix-vue';
|
||||
const { label, disabled = false } = defineProps<{
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}>();
|
||||
</script>
|
||||
18
components/base/SelectItem.vue
Normal file
18
components/base/SelectItem.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<SelectItem :value="value" :disabled="disabled" class="">
|
||||
<SelectItemText class="">{{ label }}</SelectItemText>
|
||||
<SelectItemIndicator class="">
|
||||
<Icon icon="radix-icons:check" />
|
||||
</SelectItemIndicator>
|
||||
</SelectItem>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { SelectItem } from 'radix-vue';
|
||||
const { disabled = false, value } = defineProps<{
|
||||
disabled?: boolean
|
||||
value: NonNullable<any>
|
||||
label: string
|
||||
}>();
|
||||
</script>
|
||||
7
components/base/SelectSeparator.vue
Normal file
7
components/base/SelectSeparator.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<SelectSeparator class="" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SelectSeparator } from 'radix-vue';
|
||||
</script>
|
||||
25
components/base/SliderInput.vue
Normal file
25
components/base/SliderInput.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||
<SliderRoot class="mx-4 relative flex items-center select-none touch-none w-[160px] h-5"
|
||||
:default-value="model ? [model] : undefined" :v-model="[model]" :disabled="disabled"
|
||||
@update:model-value="(value) => model = value ? value[0] : min" :min="min" :max="max" :step="step">
|
||||
<SliderTrack class="bg-light-30 dark:bg-dark-30 relative h-1 w-full data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10">
|
||||
<SliderRange class="absolute bg-light-40 dark:bg-dark-40 h-full data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30" />
|
||||
</SliderTrack>
|
||||
<SliderThumb
|
||||
class="block w-5 h-5 bg-light-50 dark:bg-dark-50 outline-none focus:shadow-raw transition-[box-shadow] focus:shadow-light-60 dark:focus:shadow-dark-60 border border-light-50 dark:border-dark-50
|
||||
hover:border-light-60 dark:hover:border-dark-60 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20" />
|
||||
</SliderRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { min = 0, max = 100, step = 1, label, disabled = false } = defineProps<{
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
}>();
|
||||
const model = defineModel<number>()
|
||||
</script>
|
||||
22
components/base/Switch.vue
Normal file
22
components/base/Switch.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||
<SwitchRoot v-model:checked="model" :disabled="disabled"
|
||||
class="mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
|
||||
<SwitchThumb
|
||||
class="block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 data-[state=checked]:translate-x-[26px]
|
||||
data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30 data-[disabled]:border-light-30 dark:data-[disabled]:border-dark-30" />
|
||||
</SwitchRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { label, disabled, onIcon, offIcon } = defineProps<{
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
onIcon?: string
|
||||
offIcon?: string
|
||||
}>();
|
||||
const model = defineModel<boolean>();
|
||||
</script>
|
||||
19
components/base/TextInput.vue
Normal file
19
components/base/TextInput.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<Label class="my-2">{{ label }}
|
||||
<input :placeholder="placeholder" :disabled="disabled"
|
||||
class="mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
|
||||
:type="type" v-model="model" :data-disabled="disabled || undefined">
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { type = 'text', label, disabled = false, placeholder } = defineProps<{
|
||||
type?: 'text' | 'password' | 'email' | 'tel' | 'url'
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
</script>
|
||||
21
components/base/TimerProgress.vue
Normal file
21
components/base/TimerProgress.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ProgressRoot class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full w-0 transition-[width] ease-linear" :style="`transition-duration: ${delay}ms; width: ${progress ? 100 : 0}%`" />
|
||||
</ProgressRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { delay = 1500, decreasing = false, shape = 'normal' } = defineProps<{
|
||||
delay?: number
|
||||
decreasing?: boolean
|
||||
shape?: 'thin' | 'normal' | 'large'
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['finish']);
|
||||
|
||||
const progress = ref(false);
|
||||
nextTick(() => {
|
||||
progress.value = true;
|
||||
setTimeout(emit, delay, 'finish');
|
||||
});
|
||||
</script>
|
||||
83
components/base/Toast.vue
Normal file
83
components/base/Toast.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<ToastRoot :duration="duration" class="ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30" v-model:open="model">
|
||||
<div class="grid grid-cols-8 p-3">
|
||||
<ToastTitle v-if="title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ title }}</h4></ToastTitle>
|
||||
<ToastClose v-if="closeable" aria-label="Close" class="text-2xl -translate-y-1/2 translate-x-1/2 cursor-pointer"><span aria-hidden>×</span></ToastClose>
|
||||
<ToastDescription v-if="content" class="text-md col-span-8 text-light-70 dark:text-dark-70" asChild><span>{{ content }}</span></ToastDescription>
|
||||
</div>
|
||||
<TimerProgress v-if="timer" shape="thin" :delay="duration" class="mb-0 mt-0 w-full" />
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>();
|
||||
|
||||
const { closeable = true, duration, title, content, timer = true } = defineProps<{
|
||||
closeable?: boolean
|
||||
duration?: number
|
||||
title?: string
|
||||
content?: string
|
||||
timer?: boolean
|
||||
}>();
|
||||
const timeout = ref<NodeJS.Timeout>();
|
||||
|
||||
watch(model, (value) => {
|
||||
if(duration)
|
||||
{
|
||||
if(value === true)
|
||||
{
|
||||
timeout.value = setTimeout(() => model.value = false, duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
clearTimeout(timeout.value);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ToastRoot[data-state='open'] {
|
||||
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.ToastRoot[data-state='closed'] {
|
||||
animation: hide .1s ease-in;
|
||||
}
|
||||
.ToastRoot[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
.ToastRoot[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform .2s ease-out;
|
||||
}
|
||||
.ToastRoot[data-swipe='end'] {
|
||||
animation: swipeRight .1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes hide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes swipeRight {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
components/base/Tooltip.vue
Normal file
84
components/base/Tooltip.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<TooltipRoot :delay-duration="delay" :disabled="disabled">
|
||||
<TooltipTrigger asChild>
|
||||
<span tabindex="0"><slot></slot></span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent class="TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||
{{ message }}
|
||||
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { message, delay = 300, side } = defineProps<{
|
||||
message: string
|
||||
delay?: number,
|
||||
disabled?: boolean
|
||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.TooltipContent {
|
||||
animation-duration: .3s;
|
||||
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.TooltipContent[data-side="top"] {
|
||||
animation-name: slideUp;
|
||||
}
|
||||
.TooltipContent[data-side="bottom"] {
|
||||
animation-name: slideDown;
|
||||
}
|
||||
.TooltipContent[data-side="left"] {
|
||||
animation-name: slideLeft;
|
||||
}
|
||||
.TooltipContent[data-side="right"] {
|
||||
animation-name: slideRight;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user