ui improve

This commit is contained in:
pa
2026-01-04 12:12:16 +09:00
committed by Natsumi
parent 00745b54f1
commit 5de3f82673
23 changed files with 402 additions and 414 deletions
+25 -8
View File
@@ -55,9 +55,9 @@
--el-bg-color-page: var(--color-zinc-50); --el-bg-color-page: var(--color-zinc-50);
--el-bg-color-overlay: var(--color-white); --el-bg-color-overlay: var(--color-white);
--el-text-color-secondary: var(--color-zinc-500); --el-text-color-secondary: var(--color-zinc-700);
--el-text-color-placeholder: var(--color-zinc-400); --el-text-color-placeholder: var(--color-zinc-500);
--el-text-color-disabled: var(--color-zinc-300); --el-text-color-disabled: var(--color-zinc-400);
--el-border-color: var(--color-zinc-200); --el-border-color: var(--color-zinc-200);
--el-border-color-light: var(--color-zinc-200); --el-border-color-light: var(--color-zinc-200);
@@ -297,6 +297,10 @@ html.dark,
color: var(--color-neutral-200); color: var(--color-neutral-200);
} }
.el-table {
background-color: var(--el-bg-color-page);
}
.el-table .el-table__empty-block { .el-table .el-table__empty-block {
background-color: var(--el-bg-color-page); background-color: var(--el-bg-color-page);
} }
@@ -631,17 +635,30 @@ html.dark .x-friend-item > .detail > .extra,
width: 2px; width: 2px;
translate: 0 -50%; translate: 0 -50%;
border-radius: 4px; border-radius: 4px;
background: color-mix(in oklch, var(--el-color-primary) 32%, transparent); background: var(--el-color-primary);
transition: transition:
background-color 0.4s, background-color 0.4s,
left 0.2s; left 0.2s;
} }
.el-menu-item.is-active::before { .el-menu-item.is-active::before {
left: 2px; left: 4px;
transition: left 0.2s cubic-bezier(0.175, 0.885, 0.32, 2.552); transition: left 0.2s cubic-bezier(0.175, 0.885, 0.32, 2.552);
} }
.el-menu.el-menu--popup .el-menu-item::before {
opacity: 0;
}
.el-menu.el-menu--popup .el-menu-item.is-active::before {
opacity: 1;
}
.el-menu.el-menu--popup .el-menu-item > i {
margin-right: 0.75rem;
font-size: 18px;
}
.el-sub-menu .el-menu-item.notify::after { .el-sub-menu .el-menu-item.notify::after {
left: 18px; left: 18px;
} }
@@ -922,7 +939,7 @@ hr.x-vertical-divider {
width: 4px; width: 4px;
height: 4px; height: 4px;
content: ''; content: '';
background: var(--el-text-color-placeholder); background: var(--el-text-color-secondary);
border-radius: 50%; border-radius: 50%;
} }
@@ -1273,8 +1290,8 @@ i.x-status-icon.red {
} }
.x-tag-age-verification { .x-tag-age-verification {
color: var(--el-color-primary-dark-2); color: #3b82f6;
border-color: var(--el-color-primary-dark-2) !important; border-color: #3b82f6 !important;
} }
.x-tag-border-left { .x-tag-border-left {
-1
View File
@@ -188,7 +188,6 @@
<style scoped> <style scoped>
.data-table-wrapper { .data-table-wrapper {
margin: 0 3px;
font-feature-settings: font-feature-settings:
'tnum' 1, 'tnum' 1,
'lnum' 1; 'lnum' 1;
+2 -12
View File
@@ -14,11 +14,7 @@
@click="handleShowWorldDialog"> @click="handleShowWorldDialog">
<el-icon :class="['is-loading']" class="mr-1" v-if="isTraveling"><Loading /></el-icon> <el-icon :class="['is-loading']" class="mr-1" v-if="isTraveling"><Loading /></el-icon>
<span class="min-w-0 truncate">{{ text }}</span> <span class="min-w-0 truncate">{{ text }}</span>
<span <span v-if="groupName" class="ml-0.5 whitespace-nowrap x-link" @click.stop="handleShowGroupDialog">
v-if="groupName"
class="ml-0.5 whitespace-nowrap"
:class="{ 'x-link': link }"
@click.stop="handleShowGroupDialog">
({{ groupName }}) ({{ groupName }})
</span> </span>
</div> </div>
@@ -229,7 +225,7 @@
function handleShowGroupDialog() { function handleShowGroupDialog() {
let location = currentInstanceId(); let location = currentInstanceId();
if (!location || !props.link) { if (!location) {
return; return;
} }
const L = parseLocation(location); const L = parseLocation(location);
@@ -244,10 +240,4 @@
.transparent { .transparent {
color: transparent; color: transparent;
} }
:global(html.dark .x-location),
:global(:root.dark .x-location),
:global(:root[data-theme='dark'] .x-location) {
color: var(--color-zinc-300);
}
</style> </style>
-6
View File
@@ -134,10 +134,4 @@
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
:global(html.dark .x-location-world),
:global(:root.dark .x-location-world),
:global(:root[data-theme='dark'] .x-location-world) {
color: var(--color-zinc-100);
}
</style> </style>
+140 -11
View File
@@ -131,7 +131,7 @@
placement="right" placement="right"
trigger="click" trigger="click"
popper-style="padding:4px;border-radius:8px;" popper-style="padding:4px;border-radius:8px;"
:offset="-10" :offset="6"
:show-arrow="false" :show-arrow="false"
:width="200" :width="200"
:hide-after="0"> :hide-after="0">
@@ -158,7 +158,9 @@
placement="right-start" placement="right-start"
trigger="hover" trigger="hover"
popper-style="padding:4px;border-radius:8px;" popper-style="padding:4px;border-radius:8px;"
:width="200"> :offset="8"
:width="200"
:hide-after="0">
<div class="nav-menu-theme"> <div class="nav-menu-theme">
<button <button
v-for="theme in themes" v-for="theme in themes"
@@ -170,6 +172,62 @@
<span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span> <span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span>
<span v-if="themeMode === theme" class="nav-menu-theme__check"></span> <span v-if="themeMode === theme" class="nav-menu-theme__check"></span>
</button> </button>
<el-divider></el-divider>
<el-popover
v-model:visible="themeColorMenuVisible"
placement="right-start"
trigger="hover"
popper-style="padding:4px;border-radius:8px;"
:offset="8"
:width="200"
:show-arrow="false"
:hide-after="0"
:teleported="false">
<div class="nav-menu-theme nav-menu-theme--colors">
<button
v-for="color in colorFamilies"
:key="color.name"
type="button"
class="nav-menu-theme__item"
:class="{ 'is-active': currentPrimary === color.base }"
:disabled="isApplyingPrimaryColor"
@click="handleThemeColorSelect(color)">
<span class="nav-menu-theme__label nav-menu-theme__label--swatch">
<span
class="nav-menu-theme__swatch"
:style="{ backgroundColor: color.base }"></span>
<span class="nav-menu-theme__label-text">{{ color.name }}</span>
</span>
<span v-if="currentPrimary === color.base" class="nav-menu-theme__check">
</span>
</button>
<el-divider></el-divider>
<div class="nav-menu-theme__custom">
<span class="nav-menu-theme__custom-label">{{
t('view.settings.appearance.theme_color.header')
}}</span>
<el-color-picker
:model-value="currentPrimary"
size="small"
:disabled="isApplyingPrimaryColor"
:teleported="false"
@change="handleCustomThemeColorChange" />
</div>
</div>
<template #reference>
<button type="button" class="nav-menu-theme__item" @click.prevent>
<span class="nav-menu-theme__label">{{
t('view.settings.appearance.theme_color.header')
}}</span>
<span class="nav-menu-settings__arrow"></span>
</button>
</template>
</el-popover>
</div> </div>
<template #reference> <template #reference>
<button type="button" class="nav-menu-settings__item" @click.prevent> <button type="button" class="nav-menu-settings__item" @click.prevent>
@@ -230,12 +288,12 @@
useAuthStore, useAuthStore,
useSearchStore, useSearchStore,
useUiStore, useUiStore,
useUserStore,
useVRCXUpdaterStore useVRCXUpdaterStore
} from '../stores'; } from '../stores';
import { THEME_CONFIG, links, navDefinitions } from '../shared/constants'; import { THEME_CONFIG, links, navDefinitions } from '../shared/constants';
import { getSentry } from '../plugin'; import { getSentry } from '../plugin';
import { openExternalLink } from '../shared/utils'; import { openExternalLink } from '../shared/utils';
import { useThemePrimaryColor } from '../composables/useElementTheme';
import configRepository from '../service/config'; import configRepository from '../service/config';
@@ -287,12 +345,9 @@
const { logout } = useAuthStore(); const { logout } = useAuthStore();
const appearanceSettingsStore = useAppearanceSettingsStore(); const appearanceSettingsStore = useAppearanceSettingsStore();
const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore); const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
const { showUserDialog } = userStore;
const settingsMenuVisible = ref(false); const settingsMenuVisible = ref(false);
const themeMenuVisible = ref(false); const themeMenuVisible = ref(false);
const themeColorMenuVisible = ref(false);
const supportMenuVisible = ref(false); const supportMenuVisible = ref(false);
const navMenuRef = ref(null); const navMenuRef = ref(null);
const navLayout = ref([]); const navLayout = ref([]);
@@ -374,6 +429,15 @@
const themes = computed(() => Object.keys(THEME_CONFIG)); const themes = computed(() => Object.keys(THEME_CONFIG));
const {
currentPrimary,
isApplying: isApplyingPrimaryColor,
applyCustomPrimaryColor,
initPrimaryColor,
colorFamilies,
selectPaletteColor
} = useThemePrimaryColor();
watch( watch(
() => activeMenuIndex.value, () => activeMenuIndex.value,
(value) => { (value) => {
@@ -404,10 +468,6 @@
const generateFolderId = () => `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`; const generateFolderId = () => `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`;
const showCurrentUserDialog = () => {
showUserDialog(currentUser.value?.id);
};
const sanitizeLayout = (layout) => { const sanitizeLayout = (layout) => {
const usedKeys = new Set(); const usedKeys = new Set();
const normalized = []; const normalized = [];
@@ -485,6 +545,27 @@
appearanceSettingsStore.saveThemeMode(theme); appearanceSettingsStore.saveThemeMode(theme);
}; };
const handleCustomThemeColorChange = async (color) => {
if (!color) {
await initPrimaryColor();
themeColorMenuVisible.value = false;
themeMenuVisible.value = false;
settingsMenuVisible.value = false;
return;
}
await applyCustomPrimaryColor(color);
themeColorMenuVisible.value = false;
themeMenuVisible.value = false;
settingsMenuVisible.value = false;
};
const handleThemeColorSelect = async (colorFamily) => {
await selectPaletteColor(colorFamily);
themeColorMenuVisible.value = false;
themeMenuVisible.value = false;
settingsMenuVisible.value = false;
};
const openGithub = () => { const openGithub = () => {
openExternalLink('https://github.com/vrcx-team/VRCX'); openExternalLink('https://github.com/vrcx-team/VRCX');
}; };
@@ -597,6 +678,7 @@
settingsMenuVisible.value = false; settingsMenuVisible.value = false;
supportMenuVisible.value = false; supportMenuVisible.value = false;
themeMenuVisible.value = false; themeMenuVisible.value = false;
themeColorMenuVisible.value = false;
}; };
const triggerNavAction = (entry, navIndex = entry?.index) => { const triggerNavAction = (entry, navIndex = entry?.index) => {
@@ -643,6 +725,7 @@
supportMenuVisible.value = false; supportMenuVisible.value = false;
} else { } else {
themeMenuVisible.value = false; themeMenuVisible.value = false;
themeColorMenuVisible.value = false;
} }
}); });
@@ -699,6 +782,7 @@
}; };
onMounted(async () => { onMounted(async () => {
await initPrimaryColor();
await loadNavMenuConfig(); await loadNavMenuConfig();
if (!NIGHTLY || !sentryErrorReporting.value) return; if (!NIGHTLY || !sentryErrorReporting.value) return;
@@ -981,5 +1065,50 @@
.nav-menu-theme__item.is-active { .nav-menu-theme__item.is-active {
background-color: var(--el-menu-hover-bg-color); background-color: var(--el-menu-hover-bg-color);
} }
.nav-menu-theme__label--swatch {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.nav-menu-theme__label-text {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-transform: capitalize;
}
.nav-menu-theme__swatch {
inline-size: 14px;
block-size: 14px;
border-radius: 4px;
border: 1px solid var(--el-border-color-lighter);
flex: none;
}
.nav-menu-theme__custom {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
border-radius: 6px;
}
.nav-menu-theme__custom-label {
font-size: 13px;
color: var(--el-text-color-regular);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
}
}
.nav-menu-theme--colors {
max-height: 360px;
overflow: hidden auto;
} }
</style> </style>
@@ -5,7 +5,7 @@
v-model="avatarDialog.visible" v-model="avatarDialog.visible"
:show-close="false" :show-close="false"
top="10vh" top="10vh"
width="930px"> width="940px">
<div v-loading="avatarDialog.loading"> <div v-loading="avatarDialog.loading">
<div style="display: flex"> <div style="display: flex">
<img <img
@@ -4,7 +4,7 @@
v-model="groupDialog.visible" v-model="groupDialog.visible"
:show-close="false" :show-close="false"
top="10vh" top="10vh"
width="930px" width="940px"
class="x-dialog x-group-dialog"> class="x-dialog x-group-dialog">
<div v-loading="groupDialog.loading" class="group-body"> <div v-loading="groupDialog.loading" class="group-body">
<div style="display: flex"> <div style="display: flex">
@@ -16,7 +16,7 @@
clearable></el-input> clearable></el-input>
</div> </div>
<DataTable :loading="loading" v-bind="dataTable" style="margin-top: 10px"> <DataTable :loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="110"> <el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="130">
<template #default="scope"> <template #default="scope">
<el-tooltip placement="left"> <el-tooltip placement="left">
<template #content> <template #content>
@@ -5,7 +5,7 @@
v-model="userDialog.visible" v-model="userDialog.visible"
:show-close="false" :show-close="false"
top="10vh" top="10vh"
width="930px"> width="940px">
<div v-loading="userDialog.loading"> <div v-loading="userDialog.loading">
<UserSummaryHeader <UserSummaryHeader
:get-user-state-text="getUserStateText" :get-user-state-text="getUserStateText"
@@ -5,7 +5,7 @@
v-model="isDialogVisible" v-model="isDialogVisible"
top="10vh" top="10vh"
:show-close="false" :show-close="false"
width="930px"> width="940px">
<div v-loading="worldDialog.loading"> <div v-loading="worldDialog.loading">
<div style="display: flex"> <div style="display: flex">
<img <img
+75 -28
View File
@@ -5,25 +5,44 @@ import colors from 'tailwindcss/colors';
import configRepository from '../service/config'; import configRepository from '../service/config';
// Tailwind indigo-500 in OKLCH // Tailwind indigo-500 in OKLCH
const DEFAULT_PRIMARY = 'oklch(58.5% 0.233 277.117)'; export const DEFAULT_PRIMARY_COLOR = 'oklch(58.5% 0.233 277.117)';
const DARK_WEIGHT = 0.2; const DARK_WEIGHT = 0.2;
const CONFIG_KEY = 'VRCX_elPrimaryColor'; const CONFIG_KEY = 'VRCX_elPrimaryColor';
const STYLE_ID = 'el-dynamic-theme'; const STYLE_ID = 'el-dynamic-theme';
let elementThemeInstance = null; let elementThemeInstance = null;
const INVALID_TAILWIND_COLOR_KEYS = new Set([
'inherit',
'current',
'transparent',
'black',
'white',
'lightBlue',
'warmGray',
'trueGray',
'coolGray',
'blueGray'
]);
/** /**
* Keep okLCH as-is; otherwise normalize hex; fallback to default. * Normalize a theme color and prevent CSS injection.
* @param {string} color
* @param {string} fallback
*/ */
function toPrimaryColor(color, fallback = DEFAULT_PRIMARY) { function toPrimaryColor(color, fallback = DEFAULT_PRIMARY_COLOR) {
if (typeof color === 'string' && color.trim()) { if (typeof color !== 'string') {
if (color.trim().startsWith('oklch(')) {
return color.trim();
}
}
return fallback; return fallback;
}
const normalized = color.trim();
if (!normalized) {
return fallback;
}
if (!CSS?.supports?.('color', normalized)) {
return fallback;
}
return normalized;
} }
/** /**
@@ -42,10 +61,9 @@ function setElementPlusColors(primary, palette = null) {
} }
// Derive Element Plus light steps either from a palette or by mixing with white. // Derive Element Plus light steps either from a palette or by mixing with white.
const safePalette = palette || null; const lightValues = palette
const lightValues = safePalette
? ['400', '300', '200', '100', '50', '50', '50', '50', '50'].map( ? ['400', '300', '200', '100', '50', '50', '50', '50', '50'].map(
(key) => safePalette[key] || primary (key) => palette[key] || primary
) )
: Array.from({ length: 9 }, (_, idx) => { : Array.from({ length: 9 }, (_, idx) => {
const whitePercent = (idx + 1) * 10; const whitePercent = (idx + 1) * 10;
@@ -73,25 +91,27 @@ function setElementPlusColors(primary, palette = null) {
`${darkSelector} {\n --el-color-primary-light-9: ${darkLight9};\n}`; `${darkSelector} {\n --el-color-primary-light-9: ${darkLight9};\n}`;
} }
function findTailwindPalette(primary) { const TAILWIND_COLOR_FAMILIES = Object.entries(colors)
const entries = Object.values(colors); .filter(([name, palette]) => {
for (const palette of entries) { return (
if ( !INVALID_TAILWIND_COLOR_KEYS.has(name) &&
palette && palette &&
typeof palette === 'object' && typeof palette === 'object' &&
palette['500'] === primary palette['500']
) { );
return palette; })
} .map(([name, palette]) => ({
} name,
return null; base: palette['500'],
} palette
}))
.sort((a, b) => a.name.localeCompare(b.name));
/** /**
* Shared Element Plus theme controller. * Shared Element Plus theme controller.
* @param {string} defaultColor * @param {string} defaultColor
*/ */
export function useElementTheme(defaultColor = DEFAULT_PRIMARY) { export function useElementTheme(defaultColor = DEFAULT_PRIMARY_COLOR) {
if (elementThemeInstance) { if (elementThemeInstance) {
return elementThemeInstance; return elementThemeInstance;
} }
@@ -102,7 +122,7 @@ export function useElementTheme(defaultColor = DEFAULT_PRIMARY) {
const applyPrimaryColor = async (color, palette = null) => { const applyPrimaryColor = async (color, palette = null) => {
const nextColor = toPrimaryColor(color, currentPrimary.value); const nextColor = toPrimaryColor(color, currentPrimary.value);
const effectivePalette = palette || findTailwindPalette(nextColor); const effectivePalette = palette || null;
isApplying.value = true; isApplying.value = true;
setElementPlusColors(nextColor, effectivePalette); setElementPlusColors(nextColor, effectivePalette);
currentPrimary.value = nextColor; currentPrimary.value = nextColor;
@@ -124,7 +144,7 @@ export function useElementTheme(defaultColor = DEFAULT_PRIMARY) {
const storedColor = const storedColor =
(await configRepository.getString(CONFIG_KEY)) || (await configRepository.getString(CONFIG_KEY)) ||
fallbackColor || fallbackColor ||
DEFAULT_PRIMARY; DEFAULT_PRIMARY_COLOR;
await applyPrimaryColor(storedColor); await applyPrimaryColor(storedColor);
}; };
@@ -138,4 +158,31 @@ export function useElementTheme(defaultColor = DEFAULT_PRIMARY) {
return elementThemeInstance; return elementThemeInstance;
} }
export { toPrimaryColor }; export function useThemePrimaryColor() {
const { currentPrimary, isApplying, applyPrimaryColor, initPrimaryColor } =
useElementTheme(DEFAULT_PRIMARY_COLOR);
const colorFamilies = TAILWIND_COLOR_FAMILIES;
const selectPaletteColor = async (colorFamily) => {
if (!colorFamily) {
return;
}
await applyPrimaryColor(colorFamily.base, colorFamily.palette);
};
const applyCustomPrimaryColor = async (color) => {
if (!color) {
return;
}
await applyPrimaryColor(color);
};
return {
currentPrimary,
isApplying,
initPrimaryColor,
applyCustomPrimaryColor,
colorFamilies,
selectPaletteColor
};
}
+4 -1
View File
@@ -68,7 +68,7 @@
<el-table-column width="20"></el-table-column> <el-table-column width="20"></el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.no')" :label="t('table.friendList.no')"
width="70" width="100"
prop="$friendNumber" prop="$friendNumber"
:sortable="true" :sortable="true"
fixed="left"> fixed="left">
@@ -141,10 +141,12 @@
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.bioLink')" width="130" prop="bioLinks"> <el-table-column :label="t('table.friendList.bioLink')" width="130" prop="bioLinks">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center">
<el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index"> <el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index">
<template #content> <template #content>
<span v-text="link"></span> <span v-text="link"></span>
</template> </template>
<img <img
:src="getFaviconUrl(link)" :src="getFaviconUrl(link)"
style=" style="
@@ -157,6 +159,7 @@
@click.stop="openExternalLink(link)" @click.stop="openExternalLink(link)"
loading="lazy" /> loading="lazy" />
</el-tooltip> </el-tooltip>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
+1
View File
@@ -28,6 +28,7 @@
</div> </div>
<DataTable v-bind="friendLogTable" :data="friendLogDisplayData"> <DataTable v-bind="friendLogTable" :data="friendLogDisplayData">
<el-table-column width="20"></el-table-column>
<el-table-column :label="t('table.friendLog.date')" prop="created_at" width="200"> <el-table-column :label="t('table.friendLog.date')" prop="created_at" width="200">
<template #default="scope"> <template #default="scope">
<el-tooltip placement="right"> <el-tooltip placement="right">
+1
View File
@@ -41,6 +41,7 @@
</div> </div>
<DataTable v-bind="gameLogTable" :data="gameLogDisplayData"> <DataTable v-bind="gameLogTable" :data="gameLogDisplayData">
<el-table-column width="20"></el-table-column>
<el-table-column :label="t('table.gameLog.date')" prop="created_at" width="140"> <el-table-column :label="t('table.gameLog.date')" prop="created_at" width="140">
<template #default="scope"> <template #default="scope">
<el-tooltip placement="right"> <el-tooltip placement="right">
-1
View File
@@ -1,6 +1,5 @@
<template> <template>
<div class="x-container" ref="moderationRef"> <div class="x-container" ref="moderationRef">
<!-- 工具栏 -->
<div class="tool-slot"> <div class="tool-slot">
<el-select <el-select
v-model="playerModerationTable.filters[0].value" v-model="playerModerationTable.filters[0].value"
+1 -1
View File
@@ -54,7 +54,7 @@
:data="notificationDisplayData" :data="notificationDisplayData"
ref="notificationTableRef" ref="notificationTableRef"
class="notification-table"> class="notification-table">
<el-table-column :label="t('table.notification.date')" prop="created_at" width="110"> <el-table-column :label="t('table.notification.date')" prop="created_at" width="130">
<template #default="scope"> <template #default="scope">
<el-tooltip placement="right"> <el-tooltip placement="right">
<template #content> <template #content>
@@ -125,9 +125,6 @@
}}</el-button> }}</el-button>
</div> </div>
</div> </div>
<div class="options-container">
<ThemePicker />
</div>
<div class="options-container"> <div class="options-container">
<span class="header">{{ t('view.settings.appearance.timedate.header') }}</span> <span class="header">{{ t('view.settings.appearance.timedate.header') }}</span>
<div class="options-container-item"> <div class="options-container-item">
@@ -394,7 +391,6 @@
import { getLanguageName, languageCodes } from '../../../../localization'; import { getLanguageName, languageCodes } from '../../../../localization';
import SimpleSwitch from '../SimpleSwitch.vue'; import SimpleSwitch from '../SimpleSwitch.vue';
import ThemePicker from '../ThemePicker.vue';
const { t } = useI18n(); const { t } = useI18n();
@@ -1,207 +0,0 @@
<template>
<div class="theme-picker">
<div class="theme-picker__header">
<div>
<span class="header">{{ t('view.settings.appearance.theme_color.header') }}</span>
</div>
<div class="theme-picker__current ml-25">
<span class="theme-picker__chip" :style="{ backgroundColor: currentPrimary }"></span>
<button type="button" class="theme-picker__toggle" @click="isOpen = !isOpen">
{{ isOpen ? 'Collapse' : 'Expand' }}
</button>
</div>
</div>
<div v-show="isOpen" class="theme-picker__panel">
<div class="theme-picker__grid">
<button
v-for="color in colorFamilies"
:key="color.name"
type="button"
class="theme-picker__item"
:class="{ 'is-active': color.base === currentPrimary }"
:disabled="isApplying"
@click="selectColor(color)">
<span class="theme-picker__swatch" :style="{ backgroundColor: color.base }"></span>
<span class="theme-picker__badge">{{ color.name }}</span>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import colors from 'tailwindcss/colors';
import { useElementTheme } from '../../../composables/useElementTheme';
// Tailwind indigo-500
const defaultPrimary = 'oklch(58.5% 0.233 277.117)';
const { currentPrimary, isApplying, applyPrimaryColor, initPrimaryColor } = useElementTheme(defaultPrimary);
const { t } = useI18n();
const invalidKeys = new Set([
'inherit',
'current',
'transparent',
'black',
'white',
'lightBlue',
'warmGray',
'trueGray',
'coolGray',
'blueGray'
]);
const isOpen = ref(false);
const colorFamilies = computed(() =>
Object.entries(colors)
.filter(([name, palette]) => {
return !invalidKeys.has(name) && palette && typeof palette === 'object' && palette['500'];
})
.map(([name, palette]) => {
const base = palette['500'];
const light = palette['300'];
const vivid = palette['600'];
const dark = palette['700'];
return {
name,
base,
light,
vivid,
dark,
palette
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
const selectColor = async (color) => {
await applyPrimaryColor(color.base, color.palette);
};
onMounted(async () => {
await initPrimaryColor(defaultPrimary);
});
</script>
<style>
.theme-picker {
padding: 6px 0;
background: transparent;
}
.theme-picker__header {
display: flex;
justify-content: flex-start;
gap: 12px;
align-items: center;
margin-bottom: 10px;
}
.theme-picker__current {
display: inline-flex;
align-items: center;
gap: 8px;
background: transparent;
color: var(--el-text-color-primary);
padding: 0;
border-radius: 0;
border: none;
}
.theme-picker__toggle {
border: none;
background: transparent;
color: var(--el-text-color-secondary);
font-size: 12px;
cursor: pointer;
padding: 0;
}
.theme-picker__toggle:hover {
color: var(--el-text-color-primary);
}
.theme-picker__chip {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--color-zinc-100);
}
.theme-picker__panel {
max-width: 400px;
}
.theme-picker__grid {
display: flex;
flex-direction: column;
flex-wrap: wrap;
max-height: 360px;
gap: 10px 18px;
}
.theme-picker__item {
all: unset;
display: inline-flex;
align-items: center;
gap: 12px;
justify-content: space-between;
width: calc(50% - 9px);
min-width: 0;
cursor: pointer;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
padding: 8px 12px;
background: var(--el-bg-color);
transition: border-color 0.15s ease;
}
.theme-picker__item:hover {
border-color: var(--el-color-primary);
}
.theme-picker__item.is-active {
border-color: var(--el-color-primary);
}
.theme-picker__item:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.theme-picker__swatch {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--color-zinc-100);
flex: none;
}
.theme-picker__badge {
font-size: 12px;
font-weight: 600;
color: var(--el-text-color-primary);
text-transform: capitalize;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.theme-picker__header {
flex-direction: column;
align-items: flex-start;
}
.theme-picker__current {
align-self: flex-start;
}
}
</style>
+4 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="x-friend-item" @click="$emit('click')"> <div class="x-friend-item" @click="showUserDialog(friend.id)">
<template v-if="friend.ref"> <template v-if="friend.ref">
<div <div
class="avatar" class="avatar"
@@ -63,7 +63,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAppearanceSettingsStore, useFriendStore } from '../../../stores'; import { useAppearanceSettingsStore, useFriendStore, useUserStore } from '../../../stores';
import { userImage, userStatusClass } from '../../../shared/utils'; import { userImage, userStatusClass } from '../../../shared/utils';
const props = defineProps({ const props = defineProps({
@@ -71,10 +71,11 @@
isGroupByInstance: Boolean isGroupByInstance: Boolean
}); });
defineEmits(['click', 'confirm-delete-friend']); defineEmits(['confirm-delete-friend']);
const { hideNicknames } = storeToRefs(useAppearanceSettingsStore()); const { hideNicknames } = storeToRefs(useAppearanceSettingsStore());
const { isRefreshFriendsLoading } = storeToRefs(useFriendStore()); const { isRefreshFriendsLoading } = storeToRefs(useFriendStore());
const { showUserDialog } = useUserStore();
const { t } = useI18n(); const { t } = useI18n();
const isFriendTraveling = computed(() => props.friend.ref?.location === 'traveling'); const isFriendTraveling = computed(() => props.friend.ref?.location === 'traveling');
@@ -63,7 +63,6 @@
v-for="friend in group" v-for="friend in group"
:key="friend.id" :key="friend.id"
:friend="friend" :friend="friend"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend"></friend-item> @confirm-delete-friend="confirmDeleteFriend"></friend-item>
</div> </div>
</div> </div>
@@ -73,7 +72,6 @@
v-for="friend in vipFriendsByGroupStatus" v-for="friend in vipFriendsByGroupStatus"
:key="friend.id" :key="friend.id"
:friend="friend" :friend="friend"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend"> @confirm-delete-friend="confirmDeleteFriend">
</friend-item> </friend-item>
</template> </template>
@@ -91,7 +89,7 @@
<div v-show="!isSidebarGroupByInstanceCollapsed"> <div v-show="!isSidebarGroupByInstanceCollapsed">
<div v-for="friendArr in friendsInSameInstance" :key="friendArr[0].ref.$location.tag"> <div v-for="friendArr in friendsInSameInstance" :key="friendArr[0].ref.$location.tag">
<div style="margin-bottom: 3px"> <div class="mb-1 flex items-center">
<Location class="extra" :location="getFriendsLocations(friendArr)" style="display: inline" /> <Location class="extra" :location="getFriendsLocations(friendArr)" style="display: inline" />
<span class="extra" style="margin-left: 5px">{{ `(${friendArr.length})` }}</span> <span class="extra" style="margin-left: 5px">{{ `(${friendArr.length})` }}</span>
</div> </div>
@@ -102,7 +100,6 @@
:friend="friend" :friend="friend"
is-group-by-instance is-group-by-instance
:style="{ 'margin-bottom': idx === friendArr.length - 1 ? '5px' : undefined }" :style="{ 'margin-bottom': idx === friendArr.length - 1 ? '5px' : undefined }"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend"> @confirm-delete-friend="confirmDeleteFriend">
</friend-item> </friend-item>
</div> </div>
@@ -126,7 +123,6 @@
v-for="friend in onlineFriendsByGroupStatus" v-for="friend in onlineFriendsByGroupStatus"
:key="friend.id" :key="friend.id"
:friend="friend" :friend="friend"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend" /> @confirm-delete-friend="confirmDeleteFriend" />
</div> </div>
<div <div
@@ -144,7 +140,6 @@
v-for="friend in activeFriends" v-for="friend in activeFriends"
:key="friend.id" :key="friend.id"
:friend="friend" :friend="friend"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend"></friend-item> @confirm-delete-friend="confirmDeleteFriend"></friend-item>
</div> </div>
<div <div
@@ -162,7 +157,6 @@
v-for="friend in offlineFriends" v-for="friend in offlineFriends"
:key="friend.id" :key="friend.id"
:friend="friend" :friend="friend"
@click="showUserDialog(friend.id)"
@confirm-delete-friend="confirmDeleteFriend"></friend-item> @confirm-delete-friend="confirmDeleteFriend"></friend-item>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="gallery-page x-container"> <div class="gallery-page x-container">
<div class="gallery-page__header"> <div class="gallery-page__header">
<el-button text size="small" :icon="ArrowLeft" class="gallery-page__back" @click="goBack"> <el-button text :icon="ArrowLeft" class="gallery-page__back" @click="goBack">
{{ t('nav_tooltip.tools') }} {{ t('nav_tooltip.tools') }}
</el-button> </el-button>
<span class="header">{{ t('dialog.gallery_icons.header') }}</span> <span class="header">{{ t('dialog.gallery_icons.header') }}</span>
+1 -1
View File
@@ -330,7 +330,7 @@
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: var(--el-color-primary-light-8); background-color: var(--el-color-primary-light-9);
} }
.el-icon-arrow-right { .el-icon-arrow-right {
@@ -194,37 +194,47 @@
position: relative; position: relative;
overflow: visible; overflow: visible;
border-radius: 8px; border-radius: 8px;
&:hover { }
.event-card:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: var(--el-box-shadow-light); box-shadow: var(--el-box-shadow-light);
} }
&.grouped-card {
.event-card.grouped-card {
margin-bottom: 0; margin-bottom: 0;
} }
&.grid-card {
.event-card.grid-card {
flex: 0 0 280px; flex: 0 0 280px;
max-width: 280px; max-width: 280px;
} }
&.group-dialog-grid-card {
.event-card.group-dialog-grid-card {
flex: 0 0 320px; flex: 0 0 320px;
max-width: 320px; max-width: 320px;
} }
:deep(.el-card__body) {
.event-card :deep(.el-card__body) {
overflow: visible; overflow: visible;
} }
.banner {
.event-card .banner {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
.timeline-view & { }
.timeline-view .event-card .banner {
height: 125px; height: 125px;
} }
.grid-view & {
.grid-view .event-card .banner {
height: 100px; height: 100px;
} }
}
.following-badge { .event-card .following-badge {
position: absolute; position: absolute;
top: -8px; top: -8px;
right: -9px; right: -9px;
@@ -241,61 +251,75 @@
z-index: 10; z-index: 10;
cursor: pointer; cursor: pointer;
} }
.is-following {
.event-card .following-badge.is-following {
background-color: var(--group-calendar-badge-following, var(--el-color-success)); background-color: var(--group-calendar-badge-following, var(--el-color-success));
} }
.event-content {
.event-card .event-content {
font-size: 12px; font-size: 12px;
.timeline-view & { }
.timeline-view .event-card .event-content {
padding: 4px 12px 12px 12px; padding: 4px 12px 12px 12px;
} }
.grid-view & {
.grid-view .event-card .event-content {
padding: 8px 12px 12px 12px; padding: 8px 12px 12px 12px;
} }
.event-title {
.event-card .event-title {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.grid-view & { }
.grid-view .event-card .event-title {
margin-bottom: 8px; margin-bottom: 8px;
} }
.event-group-name { .event-card .event-group-name {
cursor: pointer; cursor: pointer;
.grid-view & { }
.grid-view .event-card .event-group-name {
display: none; display: none;
} }
}
.event-title-content { .event-card .event-title-content {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
line-height: 1.2; line-height: 1.2;
cursor: pointer; cursor: pointer;
.timeline-view & { }
.timeline-view .event-card .event-title-content {
margin-bottom: 2px; margin-bottom: 2px;
} }
&:hover {
.event-card .event-title-content:hover {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
}
} .event-card .event-info {
.event-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.timeline-view & > :first-child { }
.timeline-view .event-card .event-info > :first-child {
font-size: 14px; font-size: 14px;
} }
.grid-view & {
.grid-view .event-card .event-info {
font-size: 11px; font-size: 11px;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
} }
.event-time {
.event-card .event-time {
font-weight: 500; font-weight: 500;
color: var(--el-color-primary); color: var(--el-color-primary);
} }
}
}
}
:deep(.el-card) { :deep(.el-card) {
border-radius: 8px; border-radius: 8px;
width: 100%; width: 100%;