mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge pull request #587 from AutoMaker-Org/fix/sidebar-project-theme-ui-overlap
fix: enhance project context menu with theme submenu improvements
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState, memo, useCallback } from 'react';
|
import { useEffect, useRef, useState, memo, useCallback, useMemo } from 'react';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react';
|
import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -6,35 +6,67 @@ import { cn } from '@/lib/utils';
|
|||||||
import { type ThemeMode, useAppStore } from '@/store/app-store';
|
import { type ThemeMode, useAppStore } from '@/store/app-store';
|
||||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import type { Project } from '@/lib/electron';
|
import type { Project } from '@/lib/electron';
|
||||||
import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '@/components/layout/sidebar/constants';
|
import {
|
||||||
|
PROJECT_DARK_THEMES,
|
||||||
|
PROJECT_LIGHT_THEMES,
|
||||||
|
THEME_SUBMENU_CONSTANTS,
|
||||||
|
} from '@/components/layout/sidebar/constants';
|
||||||
import { useThemePreview } from '@/components/layout/sidebar/hooks';
|
import { useThemePreview } from '@/components/layout/sidebar/hooks';
|
||||||
|
|
||||||
// Constant for "use global theme" option
|
/**
|
||||||
|
* Constant representing the "use global theme" option.
|
||||||
|
* An empty string is used to indicate that no project-specific theme is set.
|
||||||
|
*/
|
||||||
const USE_GLOBAL_THEME = '' as const;
|
const USE_GLOBAL_THEME = '' as const;
|
||||||
|
|
||||||
// Constants for z-index values
|
/**
|
||||||
|
* Z-index values for context menu layering.
|
||||||
|
* Ensures proper stacking order when menus overlap.
|
||||||
|
*/
|
||||||
const Z_INDEX = {
|
const Z_INDEX = {
|
||||||
|
/** Base z-index for the main context menu */
|
||||||
CONTEXT_MENU: 100,
|
CONTEXT_MENU: 100,
|
||||||
|
/** Higher z-index for theme submenu to appear above parent menu */
|
||||||
THEME_SUBMENU: 101,
|
THEME_SUBMENU: 101,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Theme option type - using ThemeMode for type safety
|
/**
|
||||||
|
* Represents a selectable theme option in the theme submenu.
|
||||||
|
* Uses ThemeMode from app-store for type safety.
|
||||||
|
*/
|
||||||
interface ThemeOption {
|
interface ThemeOption {
|
||||||
|
/** The theme mode value (e.g., 'dark', 'light', 'dracula') */
|
||||||
value: ThemeMode;
|
value: ThemeMode;
|
||||||
|
/** Display label for the theme option */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Lucide icon component to display alongside the label */
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
/** CSS color value for the icon */
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable theme button component to avoid duplication (DRY principle)
|
/**
|
||||||
|
* Props for the ThemeButton component.
|
||||||
|
* Defines the interface for rendering individual theme selection buttons.
|
||||||
|
*/
|
||||||
interface ThemeButtonProps {
|
interface ThemeButtonProps {
|
||||||
|
/** The theme option data to display */
|
||||||
option: ThemeOption;
|
option: ThemeOption;
|
||||||
|
/** Whether this theme is currently selected */
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
/** Handler for pointer enter events (used for preview) */
|
||||||
onPointerEnter: () => void;
|
onPointerEnter: () => void;
|
||||||
|
/** Handler for pointer leave events (used to clear preview) */
|
||||||
onPointerLeave: (e: React.PointerEvent) => void;
|
onPointerLeave: (e: React.PointerEvent) => void;
|
||||||
|
/** Handler for click events (used to select theme) */
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable button component for individual theme options.
|
||||||
|
* Implements hover preview and selection functionality.
|
||||||
|
* Memoized to prevent unnecessary re-renders when parent state changes.
|
||||||
|
*/
|
||||||
const ThemeButton = memo(function ThemeButton({
|
const ThemeButton = memo(function ThemeButton({
|
||||||
option,
|
option,
|
||||||
isSelected,
|
isSelected,
|
||||||
@@ -63,17 +95,33 @@ const ThemeButton = memo(function ThemeButton({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reusable theme column component
|
/**
|
||||||
|
* Props for the ThemeColumn component.
|
||||||
|
* Defines the interface for rendering a column of related theme options (e.g., dark or light themes).
|
||||||
|
*/
|
||||||
interface ThemeColumnProps {
|
interface ThemeColumnProps {
|
||||||
|
/** Column header title (e.g., "Dark", "Light") */
|
||||||
title: string;
|
title: string;
|
||||||
|
/** Icon to display in the column header */
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
/** Array of theme options to display in this column */
|
||||||
themes: ThemeOption[];
|
themes: ThemeOption[];
|
||||||
|
/** Currently selected theme value, or null if using global theme */
|
||||||
selectedTheme: ThemeMode | null;
|
selectedTheme: ThemeMode | null;
|
||||||
|
/** Handler called when user hovers over a theme option for preview */
|
||||||
onPreviewEnter: (value: ThemeMode) => void;
|
onPreviewEnter: (value: ThemeMode) => void;
|
||||||
|
/** Handler called when user stops hovering over a theme option */
|
||||||
onPreviewLeave: (e: React.PointerEvent) => void;
|
onPreviewLeave: (e: React.PointerEvent) => void;
|
||||||
|
/** Handler called when user clicks to select a theme */
|
||||||
onSelect: (value: ThemeMode) => void;
|
onSelect: (value: ThemeMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable column component for displaying themed options.
|
||||||
|
* Renders a group of related themes (e.g., all dark themes or all light themes)
|
||||||
|
* with a header and scrollable list of ThemeButton components.
|
||||||
|
* Memoized to prevent unnecessary re-renders.
|
||||||
|
*/
|
||||||
const ThemeColumn = memo(function ThemeColumn({
|
const ThemeColumn = memo(function ThemeColumn({
|
||||||
title,
|
title,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -105,13 +153,36 @@ const ThemeColumn = memo(function ThemeColumn({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the ProjectContextMenu component.
|
||||||
|
* Defines the interface for the project right-click context menu.
|
||||||
|
*/
|
||||||
interface ProjectContextMenuProps {
|
interface ProjectContextMenuProps {
|
||||||
|
/** The project this context menu is for */
|
||||||
project: Project;
|
project: Project;
|
||||||
|
/** Screen coordinates where the context menu should appear */
|
||||||
position: { x: number; y: number };
|
position: { x: number; y: number };
|
||||||
|
/** Callback to close the context menu */
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** Callback when user selects "Edit Name & Icon" option */
|
||||||
onEdit: (project: Project) => void;
|
onEdit: (project: Project) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context menu component for project-specific actions.
|
||||||
|
*
|
||||||
|
* Provides options for:
|
||||||
|
* - Editing project name and icon
|
||||||
|
* - Setting project-specific theme (with live preview on hover)
|
||||||
|
* - Removing project from the workspace
|
||||||
|
*
|
||||||
|
* Features viewport-aware positioning for the theme submenu to prevent
|
||||||
|
* overflow, and implements delayed hover handling to improve UX when
|
||||||
|
* navigating between the trigger button and submenu.
|
||||||
|
*
|
||||||
|
* @param props - Component props
|
||||||
|
* @returns The rendered context menu or null if not visible
|
||||||
|
*/
|
||||||
export function ProjectContextMenu({
|
export function ProjectContextMenu({
|
||||||
project,
|
project,
|
||||||
position,
|
position,
|
||||||
@@ -130,9 +201,82 @@ export function ProjectContextMenu({
|
|||||||
const [showThemeSubmenu, setShowThemeSubmenu] = useState(false);
|
const [showThemeSubmenu, setShowThemeSubmenu] = useState(false);
|
||||||
const [removeConfirmed, setRemoveConfirmed] = useState(false);
|
const [removeConfirmed, setRemoveConfirmed] = useState(false);
|
||||||
const themeSubmenuRef = useRef<HTMLDivElement>(null);
|
const themeSubmenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme });
|
const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme });
|
||||||
|
|
||||||
|
// Handler to open theme submenu and cancel any pending close
|
||||||
|
const handleThemeMenuEnter = useCallback(() => {
|
||||||
|
// Cancel any pending close timeout
|
||||||
|
if (closeTimeoutRef.current) {
|
||||||
|
clearTimeout(closeTimeoutRef.current);
|
||||||
|
closeTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setShowThemeSubmenu(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handler to close theme submenu with a small delay
|
||||||
|
// This prevents the submenu from closing when mouse crosses the gap between trigger and submenu
|
||||||
|
const handleThemeMenuLeave = useCallback(() => {
|
||||||
|
// Add a small delay before closing to allow mouse to reach submenu
|
||||||
|
closeTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowThemeSubmenu(false);
|
||||||
|
setPreviewTheme(null);
|
||||||
|
}, 100); // 100ms delay is enough to cross the gap
|
||||||
|
}, [setPreviewTheme]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates theme submenu position to prevent viewport overflow.
|
||||||
|
*
|
||||||
|
* This memoized calculation determines the optimal vertical position and maximum
|
||||||
|
* height for the theme submenu based on the current viewport dimensions and
|
||||||
|
* the trigger button's position.
|
||||||
|
*
|
||||||
|
* @returns Object containing:
|
||||||
|
* - top: Vertical offset from default position (negative values shift submenu up)
|
||||||
|
* - maxHeight: Maximum height constraint to prevent overflow with scrolling
|
||||||
|
*/
|
||||||
|
const submenuPosition = useMemo(() => {
|
||||||
|
const { ESTIMATED_SUBMENU_HEIGHT, COLLISION_PADDING, THEME_BUTTON_OFFSET } =
|
||||||
|
THEME_SUBMENU_CONSTANTS;
|
||||||
|
|
||||||
|
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 800;
|
||||||
|
|
||||||
|
// Calculate where the submenu's bottom edge would be if positioned normally
|
||||||
|
const submenuBottomY = position.y + THEME_BUTTON_OFFSET + ESTIMATED_SUBMENU_HEIGHT;
|
||||||
|
|
||||||
|
// Check if submenu would overflow bottom of viewport
|
||||||
|
const wouldOverflowBottom = submenuBottomY > viewportHeight - COLLISION_PADDING;
|
||||||
|
|
||||||
|
// If it would overflow, calculate how much to shift it up
|
||||||
|
if (wouldOverflowBottom) {
|
||||||
|
// Calculate the offset needed to align submenu bottom with viewport bottom minus padding
|
||||||
|
const overflowAmount = submenuBottomY - (viewportHeight - COLLISION_PADDING);
|
||||||
|
return {
|
||||||
|
top: -overflowAmount,
|
||||||
|
maxHeight: Math.min(ESTIMATED_SUBMENU_HEIGHT, viewportHeight - COLLISION_PADDING * 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: submenu opens at top of parent (aligned with the theme button)
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
maxHeight: Math.min(
|
||||||
|
ESTIMATED_SUBMENU_HEIGHT,
|
||||||
|
viewportHeight - position.y - THEME_BUTTON_OFFSET - COLLISION_PADDING
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}, [position.y]);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (closeTimeoutRef.current) {
|
||||||
|
clearTimeout(closeTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: globalThis.MouseEvent) => {
|
const handleClickOutside = (event: globalThis.MouseEvent) => {
|
||||||
// Don't close if a confirmation dialog is open (dialog is in a portal)
|
// Don't close if a confirmation dialog is open (dialog is in a portal)
|
||||||
@@ -242,11 +386,8 @@ export function ProjectContextMenu({
|
|||||||
{/* Theme Submenu Trigger */}
|
{/* Theme Submenu Trigger */}
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
onMouseEnter={() => setShowThemeSubmenu(true)}
|
onMouseEnter={handleThemeMenuEnter}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={handleThemeMenuLeave}
|
||||||
setShowThemeSubmenu(false);
|
|
||||||
setPreviewTheme(null);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowThemeSubmenu(!showThemeSubmenu)}
|
onClick={() => setShowThemeSubmenu(!showThemeSubmenu)}
|
||||||
@@ -273,13 +414,18 @@ export function ProjectContextMenu({
|
|||||||
<div
|
<div
|
||||||
ref={themeSubmenuRef}
|
ref={themeSubmenuRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-full top-0 ml-1 min-w-[420px] rounded-lg',
|
'absolute left-full ml-1 min-w-[420px] rounded-lg',
|
||||||
'bg-popover text-popover-foreground',
|
'bg-popover text-popover-foreground',
|
||||||
'border border-border shadow-lg',
|
'border border-border shadow-lg',
|
||||||
'animate-in fade-in zoom-in-95 duration-100'
|
'animate-in fade-in zoom-in-95 duration-100'
|
||||||
)}
|
)}
|
||||||
style={{ zIndex: Z_INDEX.THEME_SUBMENU }}
|
style={{
|
||||||
|
zIndex: Z_INDEX.THEME_SUBMENU,
|
||||||
|
top: `${submenuPosition.top}px`,
|
||||||
|
}}
|
||||||
data-testid="project-theme-submenu"
|
data-testid="project-theme-submenu"
|
||||||
|
onMouseEnter={handleThemeMenuEnter}
|
||||||
|
onMouseLeave={handleThemeMenuLeave}
|
||||||
>
|
>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{/* Use Global Option */}
|
{/* Use Global Option */}
|
||||||
@@ -306,7 +452,13 @@ export function ProjectContextMenu({
|
|||||||
<div className="h-px bg-border my-2" />
|
<div className="h-px bg-border my-2" />
|
||||||
|
|
||||||
{/* Two Column Layout - Using reusable ThemeColumn component */}
|
{/* Two Column Layout - Using reusable ThemeColumn component */}
|
||||||
<div className="flex gap-2">
|
{/* Dynamic max height with scroll for viewport overflow handling */}
|
||||||
|
<div
|
||||||
|
className="flex gap-2 overflow-y-auto scrollbar-styled"
|
||||||
|
style={{
|
||||||
|
maxHeight: `${submenuPosition.maxHeight - THEME_SUBMENU_CONSTANTS.SUBMENU_HEADER_HEIGHT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ThemeColumn
|
<ThemeColumn
|
||||||
title="Dark"
|
title="Dark"
|
||||||
icon={Moon}
|
icon={Moon}
|
||||||
|
|||||||
@@ -30,17 +30,41 @@ import {
|
|||||||
import { DndContext, closestCenter } from '@dnd-kit/core';
|
import { DndContext, closestCenter } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { SortableProjectItem, ThemeMenuItem } from './';
|
import { SortableProjectItem, ThemeMenuItem } from './';
|
||||||
import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '../constants';
|
import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES, THEME_SUBMENU_CONSTANTS } from '../constants';
|
||||||
import { useProjectPicker, useDragAndDrop, useProjectTheme } from '../hooks';
|
import { useProjectPicker, useDragAndDrop, useProjectTheme } from '../hooks';
|
||||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the ProjectSelectorWithOptions component.
|
||||||
|
* Defines the interface for the project selector dropdown with additional options menu.
|
||||||
|
*/
|
||||||
interface ProjectSelectorWithOptionsProps {
|
interface ProjectSelectorWithOptionsProps {
|
||||||
|
/** Whether the sidebar is currently expanded */
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
|
/** Whether the project picker dropdown is currently open */
|
||||||
isProjectPickerOpen: boolean;
|
isProjectPickerOpen: boolean;
|
||||||
|
/** Callback to control the project picker dropdown open state */
|
||||||
setIsProjectPickerOpen: (value: boolean | ((prev: boolean) => boolean)) => void;
|
setIsProjectPickerOpen: (value: boolean | ((prev: boolean) => boolean)) => void;
|
||||||
|
/** Callback to show the delete project confirmation dialog */
|
||||||
setShowDeleteProjectDialog: (show: boolean) => void;
|
setShowDeleteProjectDialog: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A project selector component with search, drag-and-drop reordering, and options menu.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Searchable dropdown for quick project switching
|
||||||
|
* - Drag-and-drop reordering of projects
|
||||||
|
* - Project-specific theme selection with live preview
|
||||||
|
* - Project history navigation (previous/next)
|
||||||
|
* - Option to move project to trash
|
||||||
|
*
|
||||||
|
* The component uses viewport-aware positioning via THEME_SUBMENU_CONSTANTS
|
||||||
|
* for consistent submenu behavior across the application.
|
||||||
|
*
|
||||||
|
* @param props - Component props
|
||||||
|
* @returns The rendered project selector or null if sidebar is closed or no projects exist
|
||||||
|
*/
|
||||||
export function ProjectSelectorWithOptions({
|
export function ProjectSelectorWithOptions({
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
isProjectPickerOpen,
|
isProjectPickerOpen,
|
||||||
@@ -246,6 +270,7 @@ export function ProjectSelectorWithOptions({
|
|||||||
<DropdownMenuSubContent
|
<DropdownMenuSubContent
|
||||||
className="w-[420px] bg-popover/95 backdrop-blur-xl"
|
className="w-[420px] bg-popover/95 backdrop-blur-xl"
|
||||||
data-testid="project-theme-menu"
|
data-testid="project-theme-menu"
|
||||||
|
collisionPadding={THEME_SUBMENU_CONSTANTS.COLLISION_PADDING}
|
||||||
onPointerLeave={() => {
|
onPointerLeave={() => {
|
||||||
// Clear preview theme when leaving the dropdown
|
// Clear preview theme when leaving the dropdown
|
||||||
setPreviewTheme(null);
|
setPreviewTheme(null);
|
||||||
@@ -286,7 +311,8 @@ export function ProjectSelectorWithOptions({
|
|||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{/* Two Column Layout */}
|
{/* Two Column Layout */}
|
||||||
<div className="flex gap-2 p-2">
|
{/* Max height with scroll to ensure all themes are visible when menu is near screen edge */}
|
||||||
|
<div className="flex gap-2 p-2 max-h-[60vh] overflow-y-auto scrollbar-styled">
|
||||||
{/* Dark Themes Column */}
|
{/* Dark Themes Column */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="flex items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
import { darkThemes, lightThemes } from '@/config/theme-options';
|
import { darkThemes, lightThemes } from '@/config/theme-options';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared constants for theme submenu positioning and layout.
|
||||||
|
* Used across project-context-menu and project-selector-with-options components
|
||||||
|
* to ensure consistent viewport-aware positioning and styling.
|
||||||
|
*/
|
||||||
|
export const THEME_SUBMENU_CONSTANTS = {
|
||||||
|
/**
|
||||||
|
* Estimated total height of the theme submenu content in pixels.
|
||||||
|
* Includes all theme options, headers, padding, and "Use Global" button.
|
||||||
|
*/
|
||||||
|
ESTIMATED_SUBMENU_HEIGHT: 620,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Padding from viewport edges to prevent submenu overflow.
|
||||||
|
* Applied to both top and bottom edges when calculating available space.
|
||||||
|
*/
|
||||||
|
COLLISION_PADDING: 32,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical offset from context menu top to the "Project Theme" button.
|
||||||
|
* Used for calculating submenu position relative to trigger button.
|
||||||
|
*/
|
||||||
|
THEME_BUTTON_OFFSET: 50,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Height reserved for submenu header area (includes "Use Global" button and separator).
|
||||||
|
* Subtracted from maxHeight to get scrollable content area height.
|
||||||
|
*/
|
||||||
|
SUBMENU_HEADER_HEIGHT: 80,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const PROJECT_DARK_THEMES = darkThemes.map((opt) => ({
|
export const PROJECT_DARK_THEMES = darkThemes.map((opt) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: opt.label,
|
label: opt.label,
|
||||||
|
|||||||
Reference in New Issue
Block a user