feat(ui): improve header layout and add theme preset selector
- Update Header with better user menu and navigation layout - Add ThemePresetSelector component for theme customization - Refine ChannelSidebar with improved visual hierarchy
This commit is contained in:
parent
0491b668c7
commit
9b351e612c
@ -5,6 +5,8 @@ import {useProjectInfo} from "@/hooks/useProjectInfo";
|
||||
import {Hash, PanelLeftClose, Plus, Search, Settings} from "lucide-react";
|
||||
import {CHANNEL_SIDEBAR} from "@/css/layout/styles";
|
||||
import {ProjectCreateMenuModal} from "@/app/project";
|
||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
||||
import { openGlobalSearch } from "@/components/search/global-search-events";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
@ -60,7 +62,7 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
|
||||
|
||||
const isSettingsActive = isActive("settings");
|
||||
|
||||
const showSettings = projectInfo?.role === "Owner" || projectInfo?.role === "Admin";
|
||||
const showSettings = isProjectAdminRole(projectInfo?.role);
|
||||
|
||||
const uncategorizedRooms = useMemo(
|
||||
() => rooms.filter((r) => !r.isMuted && !r.category),
|
||||
@ -98,9 +100,10 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={openGlobalSearch}
|
||||
className={CHANNEL_SIDEBAR.iconButton}
|
||||
style={{color: "var(--text-secondary)"}}
|
||||
title="Search"
|
||||
title="Search (Ctrl+Alt+F)"
|
||||
>
|
||||
<Search className="w-[14px] h-[14px]"/>
|
||||
</button>
|
||||
|
||||
@ -5,6 +5,8 @@ import { useProjectLayout } from "@/app/project/layout";
|
||||
import { useProjectsQuery } from "@/hooks/useProjectsQuery";
|
||||
import { useOptionalRoom } from "@/contexts/room";
|
||||
import { RoomSettingsModal } from "@/app/project/channel/RoomSettingsModal";
|
||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
||||
import { openGlobalSearch } from "@/components/search/global-search-events";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@ -42,15 +44,18 @@ const ME_NAV_SIBLINGS: BreadcrumbSibling[] = [
|
||||
{ label: "Invitations", path: "/me/invitations" },
|
||||
];
|
||||
|
||||
function getProjectNavSiblings(projectName: string): BreadcrumbSibling[] {
|
||||
return [
|
||||
function getProjectNavSiblings(projectName: string, showSettings: boolean): BreadcrumbSibling[] {
|
||||
const items = [
|
||||
{ label: "Repository", path: `/${projectName}/repos` },
|
||||
{ label: "Issues", path: `/${projectName}/issues` },
|
||||
{ label: "Skills", path: `/${projectName}/skills` },
|
||||
{ label: "Board", path: `/${projectName}/board` },
|
||||
{ label: "Chat", path: `/${projectName}/chat` },
|
||||
{ label: "Settings", path: `/${projectName}/settings` },
|
||||
];
|
||||
if (showSettings) {
|
||||
items.push({ label: "Settings", path: `/${projectName}/settings` });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getRepoTabSiblings(repoBasePath: string): BreadcrumbSibling[] {
|
||||
@ -67,6 +72,7 @@ function getRepoTabSiblings(repoBasePath: string): BreadcrumbSibling[] {
|
||||
function getSegmentSiblings(
|
||||
segment: BreadcrumbSegment,
|
||||
projects: Array<{ name: string; display_name: string }>,
|
||||
canManageProject = false,
|
||||
): BreadcrumbSibling[] | null {
|
||||
if (segment.isLast) return null;
|
||||
|
||||
@ -86,7 +92,7 @@ function getSegmentSiblings(
|
||||
|
||||
if (depth === 2) {
|
||||
if (parts[0] === "me") return ME_NAV_SIBLINGS;
|
||||
return getProjectNavSiblings(parts[0]);
|
||||
return getProjectNavSiblings(parts[0], canManageProject);
|
||||
}
|
||||
|
||||
if (depth >= 3 && parts[1] === "repo") {
|
||||
@ -168,9 +174,10 @@ const TOOLBAR_ICONS = [
|
||||
export const Header = memo(function Header() {
|
||||
const location = useLocation();
|
||||
const { segments, projects } = useBreadcrumbs();
|
||||
const { isProjectMember, showMembers, setShowMembers } = useProjectLayout();
|
||||
const { isProjectMember, projectInfo, showMembers, setShowMembers } = useProjectLayout();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const roomContext = useOptionalRoom();
|
||||
const canManageProject = isProjectAdminRole(projectInfo?.role);
|
||||
|
||||
const handleCopy = useCallback((e: React.MouseEvent, text: string) => {
|
||||
e.preventDefault();
|
||||
@ -195,7 +202,7 @@ export const Header = memo(function Header() {
|
||||
</Link>
|
||||
|
||||
{segments.map((segment, idx) => {
|
||||
const siblings = getSegmentSiblings(segment, projects);
|
||||
const siblings = getSegmentSiblings(segment, projects, canManageProject);
|
||||
|
||||
return (
|
||||
<span key={segment.path + idx} className="flex items-center gap-1 min-w-0">
|
||||
@ -297,9 +304,10 @@ export const Header = memo(function Header() {
|
||||
{TOOLBAR_ICONS.map((icon, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={icon.label === "Search" ? openGlobalSearch : undefined}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
title={icon.label}
|
||||
title={icon.label === "Search" ? "Search (Ctrl+Alt+F)" : icon.label}
|
||||
>
|
||||
<svg
|
||||
className="w-[18px] h-[18px]"
|
||||
|
||||
@ -4,6 +4,49 @@ import { getAllPresets } from "@/config/theme-presets";
|
||||
const STORAGE_KEY = "app-theme-preset";
|
||||
const DEFAULT_PRESET = "soft-mono";
|
||||
|
||||
const THEME_COLOR_ALIASES: Record<string, string> = {
|
||||
"color-background": "background",
|
||||
"color-foreground": "foreground",
|
||||
"color-card": "card",
|
||||
"color-card-foreground": "card-foreground",
|
||||
"color-popover": "popover",
|
||||
"color-popover-foreground": "popover-foreground",
|
||||
"color-primary": "primary",
|
||||
"color-primary-foreground": "primary-foreground",
|
||||
"color-secondary": "secondary",
|
||||
"color-secondary-foreground": "secondary-foreground",
|
||||
"color-muted": "muted",
|
||||
"color-muted-foreground": "muted-foreground",
|
||||
"color-accent": "accent",
|
||||
"color-accent-foreground": "accent-foreground",
|
||||
"color-destructive": "destructive",
|
||||
"color-border": "border",
|
||||
"color-input": "input",
|
||||
"color-ring": "ring",
|
||||
"color-sidebar": "sidebar",
|
||||
"color-sidebar-foreground": "sidebar-foreground",
|
||||
"color-sidebar-primary": "sidebar-primary",
|
||||
"color-sidebar-primary-foreground": "sidebar-primary-foreground",
|
||||
"color-sidebar-accent": "sidebar-accent",
|
||||
"color-sidebar-accent-foreground": "sidebar-accent-foreground",
|
||||
"color-sidebar-border": "sidebar-border",
|
||||
"color-sidebar-ring": "sidebar-ring",
|
||||
};
|
||||
|
||||
const DERIVED_THEME_TOKENS: Record<string, string> = {
|
||||
"accent-bg": "color-mix(in oklch, var(--accent) 8%, transparent)",
|
||||
"success-alpha10": "color-mix(in oklch, var(--success) 10%, transparent)",
|
||||
"warning-alpha10": "color-mix(in oklch, var(--warning) 10%, transparent)",
|
||||
"destructive-alpha10": "color-mix(in oklch, var(--destructive) 10%, transparent)",
|
||||
"status-offline": "var(--text-muted)",
|
||||
"text-tertiary": "color-mix(in oklch, var(--text-muted) 70%, var(--surface-ground))",
|
||||
"heatmap-0": "color-mix(in oklch, var(--surface-ground) 82%, var(--border-default))",
|
||||
"heatmap-1": "color-mix(in oklch, var(--success) 18%, var(--surface-ground))",
|
||||
"heatmap-2": "color-mix(in oklch, var(--success) 36%, var(--surface-ground))",
|
||||
"heatmap-3": "color-mix(in oklch, var(--success) 58%, var(--surface-ground))",
|
||||
"heatmap-4": "color-mix(in oklch, var(--success) 78%, var(--surface-ground))",
|
||||
};
|
||||
|
||||
export function useThemePreset() {
|
||||
const [presetId, setPresetIdState] = useState<string>(() => {
|
||||
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PRESET;
|
||||
@ -33,6 +76,14 @@ export function applyThemePreset(presetId: string) {
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value) root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
|
||||
Object.entries(THEME_COLOR_ALIASES).forEach(([alias, source]) => {
|
||||
root.style.setProperty(`--${alias}`, `var(--${source})`);
|
||||
});
|
||||
|
||||
Object.entries(DERIVED_THEME_TOKENS).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
export function ThemePresetSelector() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user