diff --git a/app/components/ui/dropdown-menu/DropdownMenu.vue b/app/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..e1c9ee3 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..1253078 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuContent.vue b/app/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..7c43014 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuGroup.vue b/app/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..da634ec --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuItem.vue b/app/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..f56cae3 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuLabel.vue b/app/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..8bca83c --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..fe82cad --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..e03c40c --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..1b936c3 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..60be75c --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSub.vue b/app/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..7472e77 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..d7c6b08 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..1683aaf --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..75cd747 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/dropdown-menu/index.ts b/app/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..955fe3a --- /dev/null +++ b/app/components/ui/dropdown-menu/index.ts @@ -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" diff --git a/app/components/ui/internal/AppHeader.vue b/app/components/ui/internal/AppHeader.vue index 671faeb..42ac8ce 100644 --- a/app/components/ui/internal/AppHeader.vue +++ b/app/components/ui/internal/AppHeader.vue @@ -1,11 +1,20 @@ diff --git a/app/components/ui/internal/HeroSection.vue b/app/components/ui/internal/HeroSection.vue new file mode 100644 index 0000000..e53910a --- /dev/null +++ b/app/components/ui/internal/HeroSection.vue @@ -0,0 +1,62 @@ + diff --git a/app/components/ui/internal/ModeToggle.vue b/app/components/ui/internal/ModeToggle.vue new file mode 100644 index 0000000..c009022 --- /dev/null +++ b/app/components/ui/internal/ModeToggle.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/components/ui/internal/ThemeSelector.vue b/app/components/ui/internal/ThemeSelector.vue new file mode 100644 index 0000000..0f916a9 --- /dev/null +++ b/app/components/ui/internal/ThemeSelector.vue @@ -0,0 +1,149 @@ + + + diff --git a/app/components/ui/provider/ThemeProvider.vue b/app/components/ui/provider/ThemeProvider.vue new file mode 100644 index 0000000..207dc1a --- /dev/null +++ b/app/components/ui/provider/ThemeProvider.vue @@ -0,0 +1,7 @@ + + + diff --git a/app/components/ui/select/Select.vue b/app/components/ui/select/Select.vue new file mode 100644 index 0000000..c94bbe8 --- /dev/null +++ b/app/components/ui/select/Select.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/components/ui/select/SelectContent.vue b/app/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..adf04ec --- /dev/null +++ b/app/components/ui/select/SelectContent.vue @@ -0,0 +1,51 @@ + + + diff --git a/app/components/ui/select/SelectGroup.vue b/app/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..e981c6c --- /dev/null +++ b/app/components/ui/select/SelectGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/select/SelectItem.vue b/app/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..9371764 --- /dev/null +++ b/app/components/ui/select/SelectItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/components/ui/select/SelectItemText.vue b/app/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..b6700b1 --- /dev/null +++ b/app/components/ui/select/SelectItemText.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/select/SelectLabel.vue b/app/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..5b6650c --- /dev/null +++ b/app/components/ui/select/SelectLabel.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/select/SelectScrollDownButton.vue b/app/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..7dc7670 --- /dev/null +++ b/app/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/components/ui/select/SelectScrollUpButton.vue b/app/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..07fe87e --- /dev/null +++ b/app/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/components/ui/select/SelectSeparator.vue b/app/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..4b5c885 --- /dev/null +++ b/app/components/ui/select/SelectSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/components/ui/select/SelectTrigger.vue b/app/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..667908b --- /dev/null +++ b/app/components/ui/select/SelectTrigger.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/components/ui/select/SelectValue.vue b/app/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..d5ce58b --- /dev/null +++ b/app/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/select/index.ts b/app/components/ui/select/index.ts new file mode 100644 index 0000000..96eae60 --- /dev/null +++ b/app/components/ui/select/index.ts @@ -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" diff --git a/app/composables/useTheme.ts b/app/composables/useTheme.ts new file mode 100644 index 0000000..c33d8ec --- /dev/null +++ b/app/composables/useTheme.ts @@ -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(DEFAULT_COLOR_SCHEME) + const themePreset = ref(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 + } +} diff --git a/app/layouts/default.vue b/app/layouts/default.vue index a2c4502..c78dd4e 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -1,15 +1,17 @@