Add theme selection, dark/light mode selection

This commit is contained in:
2026-01-15 02:01:41 +05:00
parent 3cfaad10cf
commit c0524494e9
94 changed files with 8035 additions and 72 deletions

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot
v-slot="slotProps"
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DropdownMenuRoot>
</template>

View File

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

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuGroupProps } from "reka-ui"
import { DropdownMenuGroup } from "reka-ui"
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup
data-slot="dropdown-menu-group"
v-bind="props"
>
<slot />
</DropdownMenuGroup>
</template>

View File

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

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
import {
DropdownMenuRadioGroup,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
</template>

View File

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

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSeparator,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
import {
DropdownMenuSub,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot v-bind="slotProps" />
</DropdownMenuSub>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import {
DropdownMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { DropdownMenuTriggerProps } from "reka-ui"
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
const props = defineProps<DropdownMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
</template>

View File

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

View File

@ -1,11 +1,20 @@
<script setup lang="ts">
import { Search } from 'lucide-vue-next';
import ModeToggle from './ModeToggle.vue';
import ThemeSelector from './ThemeSelector.vue';
const colorMode = useColorMode()
const logoPath = computed(() => {
return colorMode.value === 'dark' ? 'logo-dark.png' : 'logo.png'
})
</script>
<template>
<div class="flex items-center justify-center w-full p-4 gap-8 border-b">
<div>
<NuxtImg src="logo.jpg" width="48" height="48" />
<ClientOnly>
<NuxtImg :src="logoPath" width="48" height="48" />
</ClientOnly>
</div>
<div>
<Button variant="ghost">
@ -18,13 +27,15 @@ import { Search } from 'lucide-vue-next';
Forum
</Button>
</div>
<div>
<div class="flex gap-4">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<Search />
</InputGroupAddon>
</InputGroup>
<ModeToggle />
<ThemeSelector />
</div>
</div>
</template>

View File

@ -0,0 +1,62 @@
<template>
<div class="flex flex-col items-center gap-4 mt-32">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight text-balance">
The next-generation anime platform
</h1>
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-center max-w-2/5">
Track, share, and discover your favorite anime and manga with Anyame
</h3>
</div>
<div class="flex items-center justify-center">
<div class="grid grid-cols-2 gap-16">
<div class="flex max-w-96 gap-8">
<NuxtImg src="discover-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Discover your obsessions
</h4>
<p class="font-semibold text-sm text-muted-foreground">
What are your highest rated genres or most watched voice actors?
Follow your watching habits over time with in-depth statistics.
</p>
</div>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="anywhere-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Bring Anyame anywhere
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Keep track of your progress on-the-go with one of many Anyame apps across iOS, Android,
macOS, and Windows.
</p>
</div>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="conversation-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Join the conversation
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Share your thoughts with our thriving community, make friends, socialize, and receive
recommendations.
</p>
</div>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="tweak-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Tweak it to your liking
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Customize your scoring system, title format, color scheme, and much more! Also, we have a
dark mode.
</p>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-vue-next';
const colorMode = useColorMode()
function toggleMode() {
if (colorMode.preference === 'system' || colorMode.preference === 'light') {
colorMode.preference = 'dark'
} else {
colorMode.preference = 'light'
}
}
</script>
<template>
<Button variant="outline" @click="toggleMode">
<Moon class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Sun class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</template>

View File

@ -0,0 +1,149 @@
<template>
<Select v-model="selectedPreset" @update:model-value="handleThemeChange">
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Select theme..." />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Themes</SelectLabel>
<!-- Theme Categories -->
<SelectLabel class="pl-8 text-xs font-normal text-muted-foreground">
Classic
</SelectLabel>
<SelectItem v-for="theme in classicThemes" :key="theme" :value="theme">
<div class="flex items-center gap-2">
<div class="h-3 w-3 rounded-full" :class="themeColor(theme)" />
<span>{{ formatThemeName(theme) }}</span>
</div>
</SelectItem>
<SelectLabel class="pl-8 text-xs font-normal text-muted-foreground mt-2">
Vibrant
</SelectLabel>
<SelectItem v-for="theme in vibrantThemes" :key="theme" :value="theme">
<div class="flex items-center gap-2">
<div class="h-3 w-3 rounded-full" :class="themeColor(theme)" />
<span>{{ formatThemeName(theme) }}</span>
</div>
</SelectItem>
<SelectLabel class="pl-8 text-xs font-normal text-muted-foreground mt-2">
Special
</SelectLabel>
<SelectItem v-for="theme in specialThemes" :key="theme" :value="theme">
<div class="flex items-center gap-2">
<div class="h-3 w-3 rounded-full" :class="themeColor(theme)" />
<span>{{ formatThemeName(theme) }}</span>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>
<script setup lang="ts">
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
import type { ThemePreset } from '@/composables/useTheme'
import type { AcceptableValue } from 'reka-ui'
const { themePreset, setThemePreset } = useTheme()
const selectedPreset = ref<ThemePreset>(themePreset.value)
const classicThemes: ThemePreset[] = [
'default', 'modern', 'nature', 'mocha', 'graphite',
'notebook', 'ocean', 'pastel', 'retro', 'sage',
'vintage', 'mono'
]
const vibrantThemes: ThemePreset[] = [
'cyberpunk', 'claymorphism', 'bold-tech', 'bubblegum',
'candyland', 'neo', 'quantum', 'softpop', 'solardusk',
'starry', 'sunset', 'tangerine', 'violet', 'amber',
'amethyst'
]
const specialThemes: ThemePreset[] = [
'darkmatter', 'cleanslate', 'eleganyluxury', 'kodama',
'midnight', 'catpuccin', 'claude', 'cosmicnight', 'doom64',
'caffeine', 'northern', 'perpetuity', 'supabase', 't3chat',
'twitter', 'vercel'
]
const handleThemeChange = (value: AcceptableValue) => {
console.log(value);
setThemePreset(value as ThemePreset)
}
watch(() => themePreset.value, (newPreset) => {
selectedPreset.value = newPreset
})
const formatThemeName = (theme: string) => {
return theme
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase())
}
const themeColor = (theme: ThemePreset) => {
const colors: Record<string, string> = {
'default': 'bg-blue-500',
'modern': 'bg-slate-600',
'mono': 'bg-zinc-600',
'cleanslate': 'bg-slate-300',
'notebook': 'bg-amber-100 border border-amber-300',
'vintage': 'bg-amber-800',
'graphite': 'bg-gray-700',
'darkmatter': 'bg-gray-900',
'midnight': 'bg-blue-900',
'cosmicnight': 'bg-indigo-950',
'doom64': 'bg-purple-900',
'cyberpunk': 'bg-pink-500',
'bold-tech': 'bg-rose-600',
'neo': 'bg-emerald-400',
'quantum': 'bg-teal-500',
'nature': 'bg-green-600',
'sage': 'bg-emerald-600',
'northern': 'bg-sky-700',
'ocean': 'bg-cyan-500',
'claymorphism': 'bg-orange-300',
'softpop': 'bg-fuchsia-300',
'eleganyluxury': 'bg-amber-900',
'amethyst': 'bg-purple-700',
'claude': 'bg-violet-800',
'kodama': 'bg-lime-400',
'catpuccin': 'bg-mauve-500',
'bubblegum': 'bg-pink-300',
'mocha': 'bg-amber-800',
'caffeine': 'bg-brown-700',
'tangerine': 'bg-orange-500',
'candyland': 'bg-rose-400',
'sunset': 'bg-orange-500',
'solardusk': 'bg-red-700',
'starry': 'bg-indigo-700',
'perpetuity': 'bg-cyan-700',
'pastel': 'bg-pink-200',
'retro': 'bg-yellow-400',
'supabase': 'bg-green-400',
'twitter': 'bg-sky-500',
'vercel': 'bg-black',
't3chat': 'bg-blue-600',
'amber': 'bg-amber-500',
'violet': 'bg-violet-600',
}
return colors[theme] || 'bg-gray-400'
}
</script>

View File

@ -0,0 +1,7 @@
<template>
<slot />
</template>
<script setup lang="ts">
const { } = useTheme()
</script>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<SelectRootProps>()
const emits = defineEmits<SelectRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<SelectRoot
v-slot="slotProps"
data-slot="select"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</SelectRoot>
</template>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import type { SelectContentEmits, SelectContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
SelectContent,
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import { SelectScrollDownButton, SelectScrollUpButton } from "."
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<SelectContentProps & { class?: HTMLAttributes["class"] }>(),
{
position: "popper",
},
)
const emits = defineEmits<SelectContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SelectPortal>
<SelectContent
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"
>
<SelectScrollUpButton />
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1')">
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectGroupProps } from "reka-ui"
import { SelectGroup } from "reka-ui"
const props = defineProps<SelectGroupProps>()
</script>
<template>
<SelectGroup
data-slot="select-group"
v-bind="props"
>
<slot />
</SelectGroup>
</template>

View File

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

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectItemTextProps } from "reka-ui"
import { SelectItemText } from "reka-ui"
const props = defineProps<SelectItemTextProps>()
</script>
<template>
<SelectItemText
data-slot="select-item-text"
v-bind="props"
>
<slot />
</SelectItemText>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { SelectLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { SelectLabel } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectLabel
data-slot="select-label"
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
>
<slot />
</SelectLabel>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollDownButton
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronUp } from "lucide-vue-next"
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollUpButton
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { SelectSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { SelectSeparator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<SelectSeparator
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

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

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectValueProps } from "reka-ui"
import { SelectValue } from "reka-ui"
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue
data-slot="select-value"
v-bind="props"
>
<slot />
</SelectValue>
</template>

View File

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

111
app/composables/useTheme.ts Normal file
View File

@ -0,0 +1,111 @@
export type ColorScheme = 'dark' | 'light' | 'system'
export type ThemePreset =
| 'default' | 'darkmatter' | 'cyberpunk' | 'claymorphism' | 'cleanslate'
| 'modern' | 'nature' | 'mocha' | 'graphite' | 'eleganyluxury'
| 'kodama' | 'midnight' | 'mono' | 'catpuccin' | 'claude'
| 'cosmicnight' | 'doom64' | 'amber' | 'amethyst' | 'bold-tech'
| 'bubblegum' | 'caffeine' | 'candyland' | 'neo' | 'northern'
| 'notebook' | 'ocean' | 'pastel' | 'perpetuity' | 'quantum'
| 'retro' | 'sage' | 'softpop' | 'solardusk' | 'starry'
| 'sunset' | 'supabase' | 't3chat' | 'tangerine' | 'twitter'
| 'vercel' | 'vintage' | 'violet'
interface ThemeState {
colorScheme: ColorScheme
themePreset: ThemePreset
}
const DEFAULT_COLOR_SCHEME: ColorScheme = 'system'
const DEFAULT_THEME_PRESET: ThemePreset = 'default'
const COLOR_SCHEME_KEY = 'vite-ui-color-scheme'
const THEME_PRESET_KEY = 'vite-ui-theme-preset'
export const useTheme = () => {
const colorScheme = ref<ColorScheme>(DEFAULT_COLOR_SCHEME)
const themePreset = ref<ThemePreset>(DEFAULT_THEME_PRESET)
const initFromStorage = () => {
if (import.meta.client) {
colorScheme.value = (localStorage.getItem(COLOR_SCHEME_KEY) as ColorScheme) || DEFAULT_COLOR_SCHEME
themePreset.value = (localStorage.getItem(THEME_PRESET_KEY) as ThemePreset) || DEFAULT_THEME_PRESET
applyTheme()
}
}
const applyTheme = () => {
if (!import.meta.client) return
const root = document.documentElement
root.classList.remove('light', 'dark')
let effectiveScheme = colorScheme.value
if (effectiveScheme === 'system') {
effectiveScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
root.classList.add(effectiveScheme)
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = `/themes/${themePreset.value}.css`
link.title = 'theme-preset'
link.id = 'theme-preset-stylesheet'
link.onload = () => {
document.querySelectorAll('link[title="theme-preset"]').forEach(oldLink => {
if (oldLink.id !== 'theme-preset-stylesheet') {
document.head.removeChild(oldLink)
}
})
}
const existingLink = document.getElementById('theme-preset-stylesheet')
if (existingLink) {
existingLink.remove()
}
document.head.appendChild(link)
}
const watchSystemTheme = () => {
if (!import.meta.client) return
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = () => {
if (colorScheme.value === 'system') {
applyTheme()
}
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}
const setColorScheme = (scheme: ColorScheme) => {
colorScheme.value = scheme
if (import.meta.client) {
localStorage.setItem(COLOR_SCHEME_KEY, scheme)
applyTheme()
}
}
const setThemePreset = (preset: ThemePreset) => {
themePreset.value = preset
if (import.meta.client) {
localStorage.setItem(THEME_PRESET_KEY, preset)
applyTheme()
}
}
onMounted(() => {
initFromStorage()
watchSystemTheme()
})
return {
colorScheme: readonly(colorScheme),
themePreset: readonly(themePreset),
setColorScheme,
setThemePreset
}
}

View File

@ -1,15 +1,17 @@
<template>
<ClientOnly>
<Sonner />
</ClientOnly>
<div class="w-full">
<header ref="header">
<AppHeader />
</header>
<main class="h-screen">
<slot />
</main>
</div>
<ThemeProvider>
<ClientOnly>
<Sonner />
</ClientOnly>
<div class="w-full">
<header ref="header">
<AppHeader />
</header>
<main class="h-screen">
<slot />
</main>
</div>
</ThemeProvider>
</template>
<script setup>
@ -17,7 +19,8 @@ import 'vue-sonner/style.css'
import Sonner from '@/components/ui/sonner/Sonner.vue';
import AppHeader from '@/components/ui/internal/AppHeader.vue';
import Headroom from "headroom.js";
import Headroom from "headroom.js"; import ThemeProvider from '~/components/ui/provider/ThemeProvider.vue';
const header = ref(null);
@ -36,6 +39,17 @@ onMounted(() => {
});
headroom.init();
});
const colorMode = useColorMode()
useHead({
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: computed(() => colorMode.value === 'dark' ? 'favicon-dark.ico' : 'favicon.ico')
}
]
})
</script>
<style>

View File

@ -1,62 +1,35 @@
<script setup lang="ts">
import { Search, SlidersHorizontal } from 'lucide-vue-next';
import HeroSection from '@/components/ui/internal/HeroSection.vue';
</script>
<template>
<div class="space-y-16">
<div class="flex flex-col items-center gap-4 mt-32">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight text-balance">
The next-generation anime platform
</h1>
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-center max-w-2/5">
Track, share, and discover your favorite anime and manga with Anyame
</h3>
</div>
<div class="flex items-center justify-center">
<div class="grid grid-cols-2 gap-16">
<div class="flex max-w-96 gap-8">
<NuxtImg src="discover-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Discover your obsessions
</h4>
<p class="font-semibold text-sm text-muted-foreground">
What are your highest rated genres or most watched voice actors?
Follow your watching habits over time with in-depth statistics.
</p>
</div>
<HeroSection />
<div class="flex flex-col items-center m-8 gap-8">
<div class="flex gap-4 max-w-1/4 md:min-w-3xl sm:min-w-64">
<InputGroup>
<InputGroupInput placeholder="Search" />
<InputGroupAddon>
<Search />
</InputGroupAddon>
</InputGroup>
<Button>
<SlidersHorizontal />
</Button>
</div>
<div class="w-full">
<div class="flex w-full justify-between">
<Button variant="ghost" class="text-2xl font-bold tracking-tight">
TRENDING NOW
</Button>
<Button variant="ghost" class="text-muted-foreground text-xl font-semibold tracking-tight">
View all
</Button>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="anywhere-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Bring Anyame anywhere
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Keep track of your progress on-the-go with one of many Anyame apps across iOS, Android,
macOS, and Windows.
</p>
</div>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="conversation-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Join the conversation
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Share your thoughts with our thriving community, make friends, socialize, and receive
recommendations.
</p>
</div>
</div>
<div class="flex max-w-96 gap-8">
<NuxtImg src="tweak-icon.svg" width="96" height="96" />
<div>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight">
Tweak it to your liking
</h4>
<p class="font-semibold text-sm text-muted-foreground">
Customize your scoring system, title format, color scheme, and much more! Also, we have a
dark mode.
</p>
</div>
<div>
Test
</div>
</div>
</div>