refactor(stage-web): ui refinements (#89)
This commit is contained in:
@@ -295,32 +295,74 @@ settings:
|
||||
field:
|
||||
primary-color:
|
||||
label: Primary color
|
||||
reset:
|
||||
label: Reset to Default
|
||||
rgb-on:
|
||||
title: I Want It Dynamic!
|
||||
title: Custom Colors
|
||||
title: Color Scheme
|
||||
developer:
|
||||
title: Developer
|
||||
theme-presets:
|
||||
preset:
|
||||
chinese:
|
||||
description: >-
|
||||
Traditional Chinese colors, derived from ancient textiles,
|
||||
porcelain and paintings
|
||||
title: Chinese Traditional Colors
|
||||
japanese:
|
||||
description: Traditional Japanese color palette
|
||||
title: Japanese Colors
|
||||
monet:
|
||||
description: Impressionist palette inspired by Claude Monet's works
|
||||
title: Monet Colors
|
||||
morandi:
|
||||
presets:
|
||||
- colors:
|
||||
- Airi Green
|
||||
description: The default greenish theme color, brought by Airi to you!
|
||||
title: Default Color
|
||||
- colors:
|
||||
- Taupe
|
||||
- Beige
|
||||
- Ash Grey
|
||||
- Light Taupe
|
||||
- Ivory
|
||||
- Olive Grey
|
||||
- Sand
|
||||
- Warm Grey
|
||||
description: Soft, muted tones inspired by Giorgio Morandi's paintings
|
||||
title: Morandi Colors
|
||||
nordic:
|
||||
- colors:
|
||||
- Sky Blue
|
||||
- Mist
|
||||
- Sand
|
||||
- Moss Green
|
||||
- Water Lily
|
||||
- Wheat
|
||||
- Slate Blue
|
||||
- Sage
|
||||
description: Impressionist palette inspired by Claude Monet's works
|
||||
title: Monet Colors
|
||||
- colors:
|
||||
- Tan
|
||||
- Warm Taupe
|
||||
- Umber
|
||||
- Coffee
|
||||
- Bronze
|
||||
- Gold
|
||||
- Mustard
|
||||
- Amber
|
||||
description: Traditional Japanese color palette
|
||||
title: Japanese Colors
|
||||
- colors:
|
||||
- Nordic Blue
|
||||
- Ice
|
||||
- Fjord
|
||||
- Steel
|
||||
- Glacier
|
||||
- Slate
|
||||
- Cloud
|
||||
- Stone
|
||||
description: Scandinavian minimalist color scheme
|
||||
title: Nordic Colors
|
||||
title: Theme Presets
|
||||
title: Themes
|
||||
- colors:
|
||||
- Rosy Dawn
|
||||
- Chinese Red
|
||||
- Smoky Brown
|
||||
- Bamboo Green
|
||||
- Dark Purple
|
||||
- Golden Yellow
|
||||
- Azure Blue
|
||||
- Ochre
|
||||
description: Traditional Chinese colors, derived from ancient textiles, porcelain and paintings
|
||||
title: Chinese Traditional Colors
|
||||
title: Color Scheme Presets
|
||||
title: Appearance
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
|
||||
@@ -281,31 +281,80 @@ settings:
|
||||
fields:
|
||||
field:
|
||||
primary-color:
|
||||
label: 主题色
|
||||
reset:
|
||||
label: 重设为默认主题色
|
||||
label: 强调色
|
||||
rgb-on:
|
||||
title: RGB ON
|
||||
title: 自定义主题色
|
||||
title: RGB ON!
|
||||
title: 配色
|
||||
developer:
|
||||
title: 开发者
|
||||
theme-presets:
|
||||
preset:
|
||||
chinese:
|
||||
title: 中国传统颜色
|
||||
japanese:
|
||||
title: 日本颜色
|
||||
monet:
|
||||
title: 莫奈颜色
|
||||
morandi:
|
||||
presets:
|
||||
- colors:
|
||||
- Airi 绿
|
||||
description: The default greenish theme color, brought by Airi to you!
|
||||
title: 默认颜色
|
||||
- colors:
|
||||
- Taupe
|
||||
- Beige
|
||||
- Ash Grey
|
||||
- Light Taupe
|
||||
- Ivory
|
||||
- Olive Grey
|
||||
- Sand
|
||||
- Warm Grey
|
||||
description: Soft, muted tones inspired by Giorgio Morandi's paintings
|
||||
title: Morandi 颜色
|
||||
nordic:
|
||||
- colors:
|
||||
- Sky Blue
|
||||
- Mist
|
||||
- Sand
|
||||
- Moss Green
|
||||
- Water Lily
|
||||
- Wheat
|
||||
- Slate Blue
|
||||
- Sage
|
||||
description: Impressionist palette inspired by Claude Monet's works
|
||||
title: 莫奈颜色
|
||||
- colors:
|
||||
- Tan
|
||||
- Warm Taupe
|
||||
- Umber
|
||||
- Coffee
|
||||
- Bronze
|
||||
- Gold
|
||||
- Mustard
|
||||
- Amber
|
||||
description: Traditional Japanese color palette
|
||||
title: 日本颜色
|
||||
- colors:
|
||||
- Nordic Blue
|
||||
- Ice
|
||||
- Fjord
|
||||
- Steel
|
||||
- Glacier
|
||||
- Slate
|
||||
- Cloud
|
||||
- Stone
|
||||
description: Scandinavian minimalist color scheme
|
||||
title: 北欧颜色
|
||||
title: 主题预设
|
||||
title: 舞台主题
|
||||
- colors:
|
||||
- 霞光红
|
||||
- 枣红
|
||||
- 黄栌
|
||||
- 竹青
|
||||
- 乌梅紫
|
||||
- 缃色
|
||||
- 青冥
|
||||
- 赭石
|
||||
description: Traditional Chinese colors, derived from ancient textiles, porcelain and paintings
|
||||
title: 中国传统颜色
|
||||
title: 预设
|
||||
title: 外观
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
title: 通用
|
||||
theme: 主题颜色
|
||||
theme: 主题
|
||||
title: 设置
|
||||
voices: 声线
|
||||
stage:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
text: string
|
||||
iconOn: string
|
||||
iconOff: string
|
||||
}>()
|
||||
const model = defineModel<boolean>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
class="setting-bar cursor-pointer text-sm"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
>
|
||||
<input v-model="model" :aria-checked="model" type="checkbox" hidden>
|
||||
{{ $t(text) }}
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div v-if="model" :class="iconOn" transition="all ease-in-out duration-250" />
|
||||
<div v-else :class="iconOff" transition="all ease-in-out duration-250" />
|
||||
</Transition>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.setting-bar {
|
||||
--at-apply: transition-all ease-in-out duration-250 w-full flex items-center justify-between rounded-lg px-4 py-3
|
||||
outline-none;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@ const settings = useSettings()
|
||||
|
||||
<template>
|
||||
<div v-if="colors.length" flex gap-2>
|
||||
<TooltipProvider v-for="{ name, hex } in colors" :key="hex || 'default'">
|
||||
<TooltipProvider v-for="{ hex, name } in colors" :key="hex || 'default'">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger
|
||||
transition="all ease-in-out duration-250"
|
||||
@@ -28,7 +28,7 @@ const settings = useSettings()
|
||||
<TooltipPortal>
|
||||
<TooltipContent bg="white dark:neutral-800" rounded-lg px-3 py-1.5 text-sm shadow-md>
|
||||
{{ name }}
|
||||
<TooltipArrow class="fill-white dark:fill-neutral-800" />
|
||||
<TooltipArrow fill-white dark:fill-neutral-800 />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
icon: string
|
||||
innerClass?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Collapsable default>
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
class="setting-bar"
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex gap-1.5>
|
||||
<div :class="icon" size-6 />
|
||||
{{ t(title) }}
|
||||
{{ $t(title) }}
|
||||
</div>
|
||||
<div
|
||||
i-solar:alt-arrow-down-bold-duotone
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
[],
|
||||
["#A5978B", "#D8CAAF", "#B8B4A7", "#C4BCB1", "#E5DED8", "#9A8F7D", "#BEB5A7", "#C9C0B6"],
|
||||
["#7A9EAF", "#B8C7CC", "#D4B79C", "#8B9D77", "#C7D5CB", "#E6D0B1", "#94A7B1", "#B4C8C3"],
|
||||
["#D9B48F", "#B5917A", "#8C7A6B", "#A17F5F", "#B98C46", "#C7A252", "#DAB300", "#D19826"],
|
||||
["#9BA7B0", "#C1CBD4", "#A5ADB6", "#8B959E", "#D4DCE4", "#7F8A94", "#B3BCC6", "#98A4AE"],
|
||||
["#E4C6D0", "#A61B29", "#5D513C", "#789262", "#1C0D1A", "#F7C242", "#62A9DD", "#8C4B3C"]
|
||||
]
|
||||
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import CheckBar from '../../../components/Settings/CheckBar.vue'
|
||||
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
|
||||
import Section from '../../../components/Settings/Section.vue'
|
||||
import COLOR_PRESETS from './color-presets.json'
|
||||
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
const dark = useDark()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ $t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ $t('settings.pages.themes.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Section title="settings.sections.section.general.title" icon="i-solar:filters-bold-duotone">
|
||||
<!-- Theme Setting -->
|
||||
<CheckBar
|
||||
v-model="dark"
|
||||
icon-on="i-solar:moon-stars-bold-duotone"
|
||||
icon-off="i-solar:sun-fog-bold-duotone"
|
||||
text="settings.theme"
|
||||
/>
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
class="setting-bar text-sm"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
>
|
||||
{{ $t('settings.language.title') }}
|
||||
<select
|
||||
v-model="settings.language"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer bg-transparent text-right outline-none
|
||||
>
|
||||
<option value="en-US">
|
||||
{{ $t('settings.language.english') }}
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
{{ $t('settings.language.chinese') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="settings.pages.themes.sections.section.custom-color.title" icon="i-solar:pallete-2-bold-duotone">
|
||||
<div flex items-center justify-between>
|
||||
<span text-lg font-semibold>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
|
||||
<label relative flex cursor-pointer items-center gap-2>
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
type="range" min="0" max="360" step="0.01" class="theme-hue-slider"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="settings.themeColorsHueDynamic ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer'"
|
||||
>
|
||||
<div class="color-bar">
|
||||
<span bg-primary-50>50</span>
|
||||
<span bg-primary-100>100</span>
|
||||
<span bg-primary-200>200</span>
|
||||
<span bg-primary-300>300</span>
|
||||
<span bg-primary-400>400</span>
|
||||
<span bg-primary-500>500</span>
|
||||
<div text-white>
|
||||
<span bg-primary-600>600</span>
|
||||
<span bg-primary-700>700</span>
|
||||
<span bg-primary-800>800</span>
|
||||
<span bg-primary-900>900</span>
|
||||
<span bg-primary-950>950</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-bar transparency-grid">
|
||||
<span bg="primary-500/5">500/5</span>
|
||||
<span bg="primary-500/10">500/10</span>
|
||||
<span bg="primary-500/20">500/20</span>
|
||||
<span bg="primary-500/30">500/30</span>
|
||||
<span bg="primary-500/40">500/40</span>
|
||||
<span bg="primary-500/50">500/50</span>
|
||||
<span bg="primary-500/60">500/60</span>
|
||||
<span bg="primary-500/70">500/70</span>
|
||||
<span bg="primary-500/80">500/80</span>
|
||||
<span bg="primary-500/90">500/90</span>
|
||||
<span bg="primary-500">500</span>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="settings.pages.themes.sections.section.theme-presets.title" icon="i-solar:magic-stick-2-bold-duotone">
|
||||
<div
|
||||
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
||||
class="setting-bar"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
>
|
||||
<div>
|
||||
<span font-medium>{{ $rt(title) }}</span>
|
||||
<div text="sm neutral-500">
|
||||
{{ $rt(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<ColorPalette :colors="(colors as any[]).map((name, j) => ({ hex: COLOR_PRESETS[i][j], name: $rt(name) }))" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="settings.pages.themes.sections.section.developer.title" icon="i-solar:code-bold-duotone">
|
||||
<CheckBar
|
||||
v-model="settings.disableTransitions"
|
||||
icon-on="i-solar:people-nearby-bold-duotone"
|
||||
icon-off="i-solar:running-2-line-duotone"
|
||||
text="settings.animations.stage-transitions.title"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-hue-slider {
|
||||
--at-apply: appearance-none h-10 rounded-lg;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
oklch(85% 0.2 0),
|
||||
oklch(85% 0.2 60),
|
||||
oklch(85% 0.2 120),
|
||||
oklch(85% 0.2 180),
|
||||
oklch(85% 0.2 240),
|
||||
oklch(85% 0.2 300),
|
||||
oklch(85% 0.2 360)
|
||||
);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
--at-apply: appearance-none w-2 h-12 rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 shadow-md border-2
|
||||
border-white hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
--at-apply: w-2 h-12 rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 shadow-md border-2 border-white border-box
|
||||
hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
|
||||
.color-bar {
|
||||
--at-apply: flex of-hidden rounded-lg lh-10 text-center text-black;
|
||||
|
||||
* {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,27 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { language, disableTransitions } = storeToRefs(useSettings())
|
||||
const dark = useDark()
|
||||
|
||||
function handleLanguageChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
language.value = target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center
|
||||
gap-2
|
||||
flex items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:duration="100"
|
||||
@@ -30,7 +17,7 @@ function handleLanguageChange(event: Event) {
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 text-3xl>
|
||||
{{ t('settings.title') }}
|
||||
{{ $t('settings.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
@@ -40,8 +27,8 @@ function handleLanguageChange(event: Event) {
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:title="t('settings.pages.modules.title')"
|
||||
:description="t('settings.pages.modules.description')"
|
||||
:title="$t('settings.pages.modules.title')"
|
||||
:description="$t('settings.pages.modules.description')"
|
||||
icon="i-lucide:blocks"
|
||||
to="/settings/modules"
|
||||
/>
|
||||
@@ -51,8 +38,8 @@ function handleLanguageChange(event: Event) {
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="50"
|
||||
:title="t('settings.pages.models.title')"
|
||||
:description="t('settings.pages.models.description')"
|
||||
:title="$t('settings.pages.models.title')"
|
||||
:description="$t('settings.pages.models.description')"
|
||||
icon="i-lucide:person-standing"
|
||||
to="/settings/models"
|
||||
/>
|
||||
@@ -62,8 +49,8 @@ function handleLanguageChange(event: Event) {
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="t('settings.pages.memory.title')"
|
||||
:description="t('settings.pages.memory.description')"
|
||||
:title="$t('settings.pages.memory.title')"
|
||||
:description="$t('settings.pages.memory.description')"
|
||||
icon="i-lucide:sprout"
|
||||
to="/settings/memory"
|
||||
/>
|
||||
@@ -73,8 +60,8 @@ function handleLanguageChange(event: Event) {
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="t('settings.pages.providers.title')"
|
||||
:description="t('settings.pages.providers.description')"
|
||||
:title="$t('settings.pages.providers.title')"
|
||||
:description="$t('settings.pages.providers.description')"
|
||||
icon="i-lucide:brain"
|
||||
to="/settings/providers"
|
||||
/>
|
||||
@@ -84,140 +71,12 @@ function handleLanguageChange(event: Event) {
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="t('settings.pages.themes.title')"
|
||||
:description="t('settings.pages.themes.description')"
|
||||
:title="$t('settings.pages.themes.title')"
|
||||
:description="$t('settings.pages.themes.description')"
|
||||
icon="i-lucide:paintbrush"
|
||||
to="/settings/themes"
|
||||
to="/settings/appearance"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
>
|
||||
<h2 text-2xl>
|
||||
{{ t('settings.sections.section.general.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
v-motion
|
||||
grid="~ cols-[150px_1fr]"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250" items-center gap-1.5 rounded-lg px-4
|
||||
py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="200"
|
||||
>
|
||||
<div text="sm">
|
||||
<span>{{ t('settings.language.title') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full justify-end>
|
||||
<select
|
||||
class="w-32"
|
||||
bg="transparent"
|
||||
text="sm right neutral-800 dark:neutral-100"
|
||||
transition="all ease-in-out duration-250"
|
||||
outline="none"
|
||||
cursor-pointer
|
||||
@change="handleLanguageChange"
|
||||
>
|
||||
<option value="en-US">
|
||||
{{ t('settings.language.english') }}
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
{{ t('settings.language.chinese') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Theme Setting -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="250"
|
||||
>
|
||||
<input
|
||||
v-model="dark"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="dark"
|
||||
:aria-checked="dark"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.theme') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="dark"
|
||||
i-solar:moon-stars-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:sun-fog-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<!-- Developer Settings -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="300"
|
||||
>
|
||||
<input
|
||||
v-model="disableTransitions"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="disableTransitions"
|
||||
:aria-checked="disableTransitions"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.animations.stage-transitions.title') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="disableTransitions"
|
||||
i-solar:people-nearby-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:running-2-line-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div v-motion text="60" i-lucide:cog />
|
||||
</div>
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { DEFAULT_THEME_COLORS_HUE, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { converter } from 'culori'
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'radix-vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
|
||||
interface ColorPreset {
|
||||
name: string
|
||||
description: string
|
||||
colors: Array<{
|
||||
hex: string
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
|
||||
const colorPresets = computed<ColorPreset[]>(() => [
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.description'),
|
||||
colors: [
|
||||
{ hex: '#A5978B', name: 'Taupe' },
|
||||
{ hex: '#D8CAAF', name: 'Beige' },
|
||||
{ hex: '#B8B4A7', name: 'Ash Grey' },
|
||||
{ hex: '#C4BCB1', name: 'Light Taupe' },
|
||||
{ hex: '#E5DED8', name: 'Ivory' },
|
||||
{ hex: '#9A8F7D', name: 'Olive Grey' },
|
||||
{ hex: '#BEB5A7', name: 'Sand' },
|
||||
{ hex: '#C9C0B6', name: 'Warm Grey' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.monet.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.monet.description'),
|
||||
colors: [
|
||||
{ hex: '#7A9EAF', name: 'Sky Blue' },
|
||||
{ hex: '#B8C7CC', name: 'Mist' },
|
||||
{ hex: '#D4B79C', name: 'Sand' },
|
||||
{ hex: '#8B9D77', name: 'Moss Green' },
|
||||
{ hex: '#C7D5CB', name: 'Water Lily' },
|
||||
{ hex: '#E6D0B1', name: 'Wheat' },
|
||||
{ hex: '#94A7B1', name: 'Slate Blue' },
|
||||
{ hex: '#B4C8C3', name: 'Sage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.description'),
|
||||
colors: [
|
||||
{ hex: '#D9B48F', name: 'Tan' },
|
||||
{ hex: '#B5917A', name: 'Warm Taupe' },
|
||||
{ hex: '#8C7A6B', name: 'Umber' },
|
||||
{ hex: '#A17F5F', name: 'Coffee' },
|
||||
{ hex: '#B98C46', name: 'Bronze' },
|
||||
{ hex: '#C7A252', name: 'Gold' },
|
||||
{ hex: '#DAB300', name: 'Mustard' },
|
||||
{ hex: '#D19826', name: 'Amber' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.description'),
|
||||
colors: [
|
||||
{ hex: '#9BA7B0', name: 'Nordic Blue' },
|
||||
{ hex: '#C1CBD4', name: 'Ice' },
|
||||
{ hex: '#A5ADB6', name: 'Fjord' },
|
||||
{ hex: '#8B959E', name: 'Steel' },
|
||||
{ hex: '#D4DCE4', name: 'Glacier' },
|
||||
{ hex: '#7F8A94', name: 'Slate' },
|
||||
{ hex: '#B3BCC6', name: 'Cloud' },
|
||||
{ hex: '#98A4AE', name: 'Stone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.description'),
|
||||
colors: [
|
||||
{ hex: '#E4C6D0', name: '霞光红 (Rosy Dawn)' },
|
||||
{ hex: '#A61B29', name: '枣红 (Chinese Red)' },
|
||||
{ hex: '#5D513C', name: '黄栌 (Smoky Brown)' },
|
||||
{ hex: '#789262', name: '竹青 (Bamboo Green)' },
|
||||
{ hex: '#1C0D1A', name: '乌梅紫 (Dark Purple)' },
|
||||
{ hex: '#F7C242', name: '缃色 (Golden Yellow)' },
|
||||
{ hex: '#62A9DD', name: '青冥 (Azure Blue)' },
|
||||
{ hex: '#8C4B3C', name: '赭石 (Ochre)' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
function resetToDefault() {
|
||||
settings.themeColorsHue = DEFAULT_THEME_COLORS_HUE
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
function applyPrimaryColorFromHex(color: string) {
|
||||
// Convert hex color to OKLCH using culori's converter
|
||||
const oklch = converter('oklch')(color)
|
||||
|
||||
if (!oklch)
|
||||
return
|
||||
|
||||
// Extract hue from OKLCH color
|
||||
const { h } = oklch
|
||||
|
||||
if (!h)
|
||||
return
|
||||
|
||||
// Update theme settings
|
||||
settings.themeColorsHue = h
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a color is currently selected based on its hue value
|
||||
* @param hexColor Hex color code to check
|
||||
* @returns True if the color's hue matches the current theme hue
|
||||
*/
|
||||
function isColorSelected(hexColor: string): boolean {
|
||||
// If dynamic coloring is enabled, no preset color is manually selected
|
||||
if (settings.themeColorsHueDynamic)
|
||||
return false
|
||||
|
||||
// Convert hex color to OKLCH
|
||||
const oklch = converter('oklch')(hexColor)
|
||||
if (!oklch || !oklch.h)
|
||||
return false
|
||||
|
||||
// Compare hue values with a small tolerance for floating point comparison
|
||||
const hueDifference = Math.abs(oklch.h - settings.themeColorsHue)
|
||||
return hueDifference < 0.01 || hueDifference > 359.99
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.themes.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex-col>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:pallete-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4>
|
||||
<div class="mb-2 text-sm font-medium">
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="0.01"
|
||||
class="theme-hue-slider h-10 w-full"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="{ 'opacity-25 cursor-not-allowed': settings.themeColorsHueDynamic }"
|
||||
>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg>
|
||||
<div bg="primary-50" class="primary-color-bar" text-black>
|
||||
50
|
||||
</div>
|
||||
<div bg="primary-100" class="primary-color-bar" text-black>
|
||||
100
|
||||
</div>
|
||||
<div bg="primary-200" class="primary-color-bar" text-black>
|
||||
200
|
||||
</div>
|
||||
<div bg="primary-300" class="primary-color-bar" text-black>
|
||||
300
|
||||
</div>
|
||||
<div bg="primary-400" class="primary-color-bar" text-black>
|
||||
400
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
<div bg="primary-600" class="primary-color-bar" text-white>
|
||||
600
|
||||
</div>
|
||||
<div bg="primary-700" class="primary-color-bar" text-white>
|
||||
700
|
||||
</div>
|
||||
<div bg="primary-800" class="primary-color-bar" text-white>
|
||||
800
|
||||
</div>
|
||||
<div bg="primary-900" class="primary-color-bar" text-white>
|
||||
900
|
||||
</div>
|
||||
<div bg="primary-950" class="primary-color-bar" text-white>
|
||||
950
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg class="transparency-grid">
|
||||
<div bg="primary-500/5" class="primary-color-bar" text-black>
|
||||
500/5
|
||||
</div>
|
||||
<div bg="primary-500/10" class="primary-color-bar" text-black>
|
||||
500/10
|
||||
</div>
|
||||
<div bg="primary-500/20" class="primary-color-bar" text-black>
|
||||
500/20
|
||||
</div>
|
||||
<div bg="primary-500/30" class="primary-color-bar" text-black>
|
||||
500/30
|
||||
</div>
|
||||
<div bg="primary-500/40" class="primary-color-bar" text-black>
|
||||
500/40
|
||||
</div>
|
||||
<div bg="primary-500/50" class="primary-color-bar" text-black>
|
||||
500/50
|
||||
</div>
|
||||
<div bg="primary-500/60" class="primary-color-bar" text-black>
|
||||
500/60
|
||||
</div>
|
||||
<div bg="primary-500/70" class="primary-color-bar" text-black>
|
||||
500/70
|
||||
</div>
|
||||
<div bg="primary-500/80" class="primary-color-bar" text-black>
|
||||
500/80
|
||||
</div>
|
||||
<div bg="primary-500/90" class="primary-color-bar" text-black>
|
||||
500/90
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mt-4 class="flex items-center justify-end gap-4">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium">{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="rounded-md bg-neutral-100 px-3 py-1.5 text-sm transition-colors dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.reset.label') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:magic-stick-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.theme-presets.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div p-4 flex="~ col gap-4">
|
||||
<div
|
||||
v-for="preset in colorPresets"
|
||||
:key="preset.name"
|
||||
flex="~ row"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3
|
||||
>
|
||||
<div>
|
||||
<div text-base font-medium>
|
||||
{{ preset.name }}
|
||||
</div>
|
||||
<div text="sm neutral-500">
|
||||
{{ preset.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row" gap-2>
|
||||
<TooltipProvider v-for="color in preset.colors" :key="color.hex">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
class="size-6 cursor-pointer rounded-full transition-all duration-250 ease-in-out" :class="[
|
||||
isColorSelected(color.hex)
|
||||
? 'scale-150 z-10 mx-1'
|
||||
: 'hover:scale-110',
|
||||
]"
|
||||
@click="applyPrimaryColorFromHex(color.hex)"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent class="rounded-lg bg-white px-3 py-1.5 text-sm shadow-md dark:bg-neutral-800">
|
||||
{{ color.name }}
|
||||
<TooltipArrow class="fill-white dark:fill-neutral-800" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.primary-color-bar {
|
||||
--at-apply: w-full h-full flex-1 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.theme-hue-slider {
|
||||
--at-apply: rounded-lg appearance-none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
oklch(85% 0.2 0),
|
||||
oklch(85% 0.2 60),
|
||||
oklch(85% 0.2 120),
|
||||
oklch(85% 0.2 180),
|
||||
oklch(85% 0.2 240),
|
||||
oklch(85% 0.2 300),
|
||||
oklch(85% 0.2 360)
|
||||
);
|
||||
}
|
||||
|
||||
.theme-hue-slider::-webkit-slider-thumb {
|
||||
--at-apply: w-2 h-12 appearance-none rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 cursor-pointer shadow-md
|
||||
border-2 border-white hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.theme-hue-slider::-moz-range-thumb {
|
||||
--at-apply: w-2 h-12 bg-neutral-500/80 rounded-md dark: bg-neutral-400/80 cursor-pointer shadow-md border-2
|
||||
border-white border-box hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
Reference in New Issue
Block a user