Feat main content revamp (#584)

* Move width of certain pages

* fixup! Move width of certain pages
This commit is contained in:
Chris 2025-11-20 09:15:55 +02:00 committed by GitHub
parent eb10ea8355
commit 0213f79b0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 577 additions and 219 deletions

View file

@ -27,8 +27,8 @@ const About: React.FC<AboutProps> = ({ isDarkMode = false }) => {
}, []);
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full">
<div className="flex items-center mb-4">
<h2 className="text-2xl font-light">
{t('about.title', 'About')}

View file

@ -158,8 +158,8 @@ const Areas: React.FC = () => {
};
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full">
{/* Areas Header */}
<div className="flex items-center mb-8">
<h2 className="text-2xl font-light">{t('areas.title')}</h2>

View file

@ -455,8 +455,8 @@ const InboxItems: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full max-w-5xl mx-auto">
{/* Title row with info button on the right */}
<div className="flex items-center mb-8 justify-between">
<div className="flex items-center">

View file

@ -6,7 +6,6 @@ import {
TrashIcon,
FolderIcon,
TagIcon as TagIconOutline,
FunnelIcon,
ClockIcon,
EllipsisVerticalIcon,
XMarkIcon,
@ -18,6 +17,7 @@ import NoteModal from './Note/NoteModal';
import ConfirmDialog from './Shared/ConfirmDialog';
import DiscardChangesDialog from './Shared/DiscardChangesDialog';
import MarkdownRenderer from './Shared/MarkdownRenderer';
import IconSortDropdown from './Shared/IconSortDropdown';
import TagInput from './Tag/TagInput';
import { Note } from '../entities/Note';
import { createNote, updateNote } from '../utils/notesService';
@ -75,7 +75,6 @@ const Notes: React.FC = () => {
const [showProjectDropdown, setShowProjectDropdown] = useState(false);
const [showTagsInput, setShowTagsInput] = useState(false);
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
const [showSortDropdown, setShowSortDropdown] = useState(false);
const [showNoteOptionsDropdown, setShowNoteOptionsDropdown] =
useState(false);
const hasAutoSelected = useRef(false);
@ -86,7 +85,6 @@ const Notes: React.FC = () => {
ENABLE_NOTE_COLOR && previewNote ? previewNote.color : undefined;
const activeNoteColor =
(isEditing && editingNoteColor) || previewNoteColor || undefined;
const sortDropdownRef = useRef<HTMLDivElement>(null);
const noteOptionsDropdownRef = useRef<HTMLDivElement>(null);
// Get notes and projects from global store
@ -412,15 +410,9 @@ const Notes: React.FC = () => {
}
}, [sortedNotes, previewNote, uid]);
// Handle clicking outside sort dropdown to close it
// Handle clicking outside note options dropdown to close it
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
sortDropdownRef.current &&
!sortDropdownRef.current.contains(event.target as Node)
) {
setShowSortDropdown(false);
}
if (
noteOptionsDropdownRef.current &&
!noteOptionsDropdownRef.current.contains(event.target as Node)
@ -483,49 +475,17 @@ const Notes: React.FC = () => {
</h3>
<div className="flex items-center gap-2">
{/* Sort Filter Dropdown */}
<div className="relative" ref={sortDropdownRef}>
<button
onClick={() =>
setShowSortDropdown(
!showSortDropdown
)
}
className="p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
aria-label="Sort notes"
>
<FunnelIcon className="h-5 w-5" />
</button>
{showSortDropdown && (
<div className="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
Sort by
</div>
<div className="py-1">
{sortOptions.map((option) => (
<button
key={option.value}
onClick={() => {
handleSortChange(
option.value
);
setShowSortDropdown(
false
);
}}
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
orderBy ===
option.value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{option.label}
</button>
))}
</div>
</div>
<IconSortDropdown
options={sortOptions}
value={orderBy}
onChange={handleSortChange}
ariaLabel={t(
'notes.sortNotes',
'Sort notes'
)}
</div>
title={t('notes.sortNotes', 'Sort notes')}
dropdownLabel={t('notes.sortBy', 'Sort by')}
/>
<button
onClick={handleNewNote}
className="p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"

View file

@ -45,7 +45,8 @@ import { createNote } from '../../utils/notesService';
import { isAuthError } from '../../utils/authUtils';
import { getAutoSuggestNextActionsEnabled } from '../../utils/profileService';
import AutoSuggestNextActionBox from './AutoSuggestNextActionBox';
import SortFilterButton, { SortOption } from '../Shared/SortFilterButton';
import { SortOption } from '../Shared/SortFilterButton';
import IconSortDropdown from '../Shared/IconSortDropdown';
import LoadingSpinner from '../Shared/LoadingSpinner';
import { usePersistedModal } from '../../hooks/usePersistedModal';
import BannerBadge from '../Shared/BannerBadge';
@ -525,6 +526,40 @@ const ProjectDetails: React.FC = () => {
saveProjectPreferences(showCompleted, newOrderBy);
};
const renderShowCompletedToggle = () => (
<button
type="button"
onClick={() => handleShowCompletedChange(!showCompleted)}
className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300"
aria-pressed={showCompleted}
aria-label={
showCompleted
? t('projects.hideCompleted', 'Hide completed tasks')
: t('projects.showCompleted', 'Show completed tasks')
}
title={
showCompleted
? t('projects.hideCompleted', 'Hide completed tasks')
: t('projects.showCompleted', 'Show completed tasks')
}
>
<span>{t('common.showCompleted', 'Show completed')}</span>
<span
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</span>
</button>
);
const handleDeleteProject = async () => {
if (!project?.uid) {
return;
@ -1077,39 +1112,23 @@ const ProjectDetails: React.FC = () => {
>
<MagnifyingGlassIcon className="h-4 w-4 text-gray-600 dark:text-gray-200" />
</button>
{/* Show Completed Toggle */}
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">
Show completed
</span>
<button
onClick={() =>
handleShowCompletedChange(
!showCompleted
)
}
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-3.5'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Sort Filter */}
<SortFilterButton
<IconSortDropdown
options={sortOptions}
value={orderBy}
onChange={handleSortChange}
size="mobile"
ariaLabel={t(
'projects.sortTasks',
'Sort tasks'
)}
title={t(
'projects.sortTasks',
'Sort tasks'
)}
dropdownLabel={t(
'tasks.sortBy',
'Sort by'
)}
extraContent={renderShowCompletedToggle()}
/>
</div>
)}
@ -1179,39 +1198,20 @@ const ProjectDetails: React.FC = () => {
>
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
</button>
{/* Show Completed Toggle */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
Show completed
</span>
<button
onClick={() =>
handleShowCompletedChange(
!showCompleted
)
}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Sort Filter */}
<SortFilterButton
<IconSortDropdown
options={sortOptions}
value={orderBy}
onChange={handleSortChange}
size="desktop"
ariaLabel={t(
'projects.sortTasks',
'Sort tasks'
)}
title={t(
'projects.sortTasks',
'Sort tasks'
)}
dropdownLabel={t('tasks.sortBy', 'Sort by')}
extraContent={renderShowCompletedToggle()}
/>
</div>
)}

View file

@ -370,8 +370,8 @@ const Projects: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full">
<div className="flex items-center mb-8">
<h2 className="text-2xl font-light">
{t('projects.title')}

View file

@ -0,0 +1,106 @@
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { FunnelIcon, CheckIcon } from '@heroicons/react/24/outline';
import { SortOption } from './SortFilterButton';
interface IconSortDropdownProps {
options: SortOption[];
value: string;
onChange: (value: string) => void;
ariaLabel?: string;
title?: string;
className?: string;
buttonClassName?: string;
dropdownLabel?: string;
align?: 'left' | 'right';
extraContent?: ReactNode;
}
const IconSortDropdown: React.FC<IconSortDropdownProps> = ({
options,
value,
onChange,
ariaLabel = 'Sort items',
title = 'Sort items',
className = '',
buttonClassName = '',
dropdownLabel = 'Sort by',
align = 'right',
extraContent,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) {
return;
}
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () =>
document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen]);
return (
<div className={`relative ${className}`} ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className={`p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors ${buttonClassName}`}
aria-label={ariaLabel}
title={title}
>
<FunnelIcon className="h-5 w-5" />
</button>
{isOpen && (
<div
className={`absolute ${align === 'left' ? 'left-0' : 'right-0'} mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50`}
>
{dropdownLabel && (
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
{dropdownLabel}
</div>
)}
<div className="py-1">
{options.map((option) => (
<button
key={option.value}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
value === option.value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
<span className="flex items-center justify-between">
<span>{option.label}</span>
{value === option.value && (
<CheckIcon className="h-4 w-4" />
)}
</span>
</button>
))}
</div>
{extraContent && (
<div className="border-t border-gray-200 dark:border-gray-700 px-3 py-2">
{extraContent}
</div>
)}
</div>
)}
</div>
);
};
export default IconSortDropdown;

View file

@ -0,0 +1,246 @@
import React, { useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
MagnifyingGlassIcon,
InformationCircleIcon,
FunnelIcon,
} from '@heroicons/react/24/outline';
import { SortOption } from './SortFilterButton';
export interface ViewOptionsBarProps {
// Info/Description
showInfo?: boolean;
onToggleInfo?: () => void;
isInfoExpanded?: boolean;
// Search
showSearch?: boolean;
onToggleSearch?: () => void;
isSearchExpanded?: boolean;
// Show Completed Toggle
showCompletedToggle?: boolean;
showCompleted?: boolean;
onToggleCompleted?: () => void;
completedLabel?: string;
// Sort/Filter Dropdown
showSort?: boolean;
sortOptions?: SortOption[];
sortValue?: string;
onSortChange?: (value: string) => void;
sortLabel?: string;
// Custom buttons/elements to add
customElements?: React.ReactNode;
// Styling
className?: string;
position?: 'fixed' | 'relative'; // For upcoming view in Tasks
fixedPosition?: string; // Custom position classes for fixed positioning
}
const ViewOptionsBar: React.FC<ViewOptionsBarProps> = ({
showInfo = false,
onToggleInfo,
isInfoExpanded = false,
showSearch = false,
onToggleSearch,
isSearchExpanded = false,
showCompletedToggle = false,
showCompleted = false,
onToggleCompleted,
completedLabel,
showSort = false,
sortOptions = [],
sortValue = '',
onSortChange,
sortLabel,
customElements,
className = '',
position = 'relative',
fixedPosition = '',
}) => {
const { t } = useTranslation();
const [isSortDropdownOpen, setIsSortDropdownOpen] = React.useState(false);
const sortDropdownRef = useRef<HTMLDivElement>(null);
// Close sort dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
sortDropdownRef.current &&
!sortDropdownRef.current.contains(event.target as Node)
) {
setIsSortDropdownOpen(false);
}
};
if (isSortDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [isSortDropdownOpen]);
const handleSortSelect = (value: string) => {
if (onSortChange) {
onSortChange(value);
}
setIsSortDropdownOpen(false);
};
const containerClasses =
position === 'fixed'
? `${fixedPosition} z-20 ${className}`
: `flex items-center gap-2 ${className}`;
return (
<div className={containerClasses}>
{/* Info Button */}
{showInfo && onToggleInfo && (
<button
onClick={onToggleInfo}
className={`flex items-center hover:bg-blue-100/50 dark:hover:bg-blue-800/20 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg${isInfoExpanded ? ' bg-blue-50/70 dark:bg-blue-900/20' : ''} p-2`}
aria-expanded={isInfoExpanded}
aria-label={
isInfoExpanded
? t('common.hideInfo', 'Hide info')
: t('common.showInfo', 'Show info')
}
title={
isInfoExpanded
? t('common.hideInfo', 'Hide info')
: t('common.aboutView', 'About this view')
}
>
<InformationCircleIcon className="h-5 w-5 text-blue-500" />
<span className="sr-only">
{isInfoExpanded
? t('common.hideInfo', 'Hide info')
: t('common.aboutView', 'About this view')}
</span>
</button>
)}
{/* Search Button */}
{showSearch && onToggleSearch && (
<button
onClick={onToggleSearch}
className={`flex items-center transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg p-2 ${
isSearchExpanded
? 'bg-blue-50/70 dark:bg-blue-900/20'
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
aria-expanded={isSearchExpanded}
aria-label={
isSearchExpanded
? t('common.hideSearch', 'Hide search')
: t('common.showSearch', 'Show search')
}
title={
isSearchExpanded
? t('common.hideSearch', 'Hide search')
: t('common.search', 'Search')
}
>
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
<span className="sr-only">
{isSearchExpanded
? t('common.hideSearch', 'Hide search')
: t('common.search', 'Search')}
</span>
</button>
)}
{/* Show Completed Toggle */}
{showCompletedToggle && onToggleCompleted && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
{completedLabel ||
t('common.showCompleted', 'Show completed')}
</span>
<button
onClick={onToggleCompleted}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
aria-pressed={showCompleted}
aria-label={
showCompleted
? t('common.hideCompleted', 'Hide completed')
: t('common.showCompleted', 'Show completed')
}
title={
showCompleted
? t('common.hideCompleted', 'Hide completed')
: t('common.showCompleted', 'Show completed')
}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
)}
{/* Sort/Filter Dropdown */}
{showSort && sortOptions.length > 0 && onSortChange && (
<div className="relative" ref={sortDropdownRef}>
<button
onClick={() =>
setIsSortDropdownOpen(!isSortDropdownOpen)
}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
aria-label={
sortLabel ||
t('common.sortFilter', 'Sort and filter')
}
title={
sortLabel ||
t('common.sortFilter', 'Sort and filter')
}
>
<FunnelIcon className="h-5 w-5" />
</button>
{isSortDropdownOpen && (
<div className="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
{sortLabel || t('common.sortBy', 'Sort by')}
</div>
<div className="py-1">
{sortOptions.map((option) => (
<button
key={option.value}
onClick={() =>
handleSortSelect(option.value)
}
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
sortValue === option.value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{option.label}
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Custom Elements */}
{customElements}
</div>
);
};
export default ViewOptionsBar;

View file

@ -124,8 +124,8 @@ const Tags: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full">
{/* Tags Header */}
<div className="flex items-center mb-8">
<h2 className="text-2xl font-light">

View file

@ -790,8 +790,8 @@ const TasksToday: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full max-w-5xl mx-auto">
<div className="flex flex-col">
{/* Today Header with Icons on the Right */}
<div className="mb-4">

View file

@ -5,7 +5,6 @@ import { useSidebar } from '../contexts/SidebarContext';
import TaskList from './Task/TaskList';
import GroupedTaskList from './Task/GroupedTaskList';
import NewTask from './Task/NewTask';
import SortFilter from './Shared/SortFilter';
import { Task } from '../entities/Task';
import { getTitleAndIcon } from './Task/getTitleAndIcon';
import { getDescription } from './Task/getDescription';
@ -17,14 +16,12 @@ import {
import { useStore } from '../store/useStore';
import { useToast } from './Shared/ToastContext';
import { SortOption } from './Shared/SortFilterButton';
import IconSortDropdown from './Shared/IconSortDropdown';
import { TagIcon, XMarkIcon } from '@heroicons/react/24/solid';
import {
TagIcon,
XMarkIcon,
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid';
import {
InformationCircleIcon,
QueueListIcon,
InformationCircleIcon,
MagnifyingGlassIcon,
} from '@heroicons/react/24/outline';
import { getApiPath } from '../config/paths';
@ -480,23 +477,17 @@ const Tasks: React.FC = () => {
};
return (
<div
className={
isUpcomingView
? 'w-full px-2 sm:px-4 lg:px-6'
: 'flex justify-center px-4 lg:px-2'
}
>
<div
className={`w-full ${isUpcomingView ? 'max-w-none' : 'max-w-5xl'}`}
>
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full max-w-5xl mx-auto">
{/* Title row with info button and filters dropdown on the right */}
<div
className={`flex flex-col sm:flex-row items-start sm:items-center justify-between ${isUpcomingView ? 'mb-4 sm:mb-6' : 'mb-8'}`}
className={`flex items-center justify-between gap-2 min-w-0 ${
isUpcomingView ? 'mb-4 sm:mb-6' : 'mb-8'
}`}
>
<div className="flex items-center mb-2 sm:mb-0">
<div className="flex items-center flex-1 min-w-0 gap-2">
<h2
className={`${isUpcomingView ? 'text-lg sm:text-xl' : 'text-2xl'} font-light`}
className={`${isUpcomingView ? 'text-lg sm:text-xl' : 'text-2xl'} font-light truncate`}
>
{title}
</h2>
@ -517,7 +508,11 @@ const Tasks: React.FC = () => {
</div>
{/* Info expand/collapse button, search button, show completed toggle, and sort dropdown */}
<div
className={`flex items-center gap-2 w-full sm:w-auto justify-end mt-2 sm:mt-0 ${isUpcomingView ? 'md:fixed md:right-4 md:top-20 md:px-3 md:py-2 md:z-20' : 'flex-wrap'}`}
className={`flex items-center gap-2 flex-shrink-0 ${
isUpcomingView
? 'md:fixed md:right-4 md:top-20 md:px-3 md:py-2 md:z-20'
: ''
}`}
>
<button
onClick={() => setIsInfoExpanded((v) => !v)}
@ -563,42 +558,65 @@ const Tasks: React.FC = () => {
</span>
</button>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
Show completed
</span>
<button
onClick={() => setShowCompleted((v) => !v)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
aria-pressed={showCompleted}
aria-label={
showCompleted
? 'Hide completed tasks'
: 'Show completed tasks'
}
title={
showCompleted
? 'Hide completed tasks'
: 'Show completed tasks'
}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
<IconSortDropdown
options={sortOptions}
value={orderBy}
onChange={handleSortChange}
ariaLabel={t('tasks.sortTasks', 'Sort tasks')}
title={t('tasks.sortTasks', 'Sort tasks')}
dropdownLabel={t('tasks.sortBy', 'Sort by')}
extraContent={
<button
type="button"
onClick={() => setShowCompleted((v) => !v)}
className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300"
aria-pressed={showCompleted}
aria-label={
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
<SortFilter
sortOptions={sortOptions}
sortValue={orderBy}
onSortChange={handleSortChange}
? t(
'tasks.hideCompleted',
'Hide completed tasks'
)
: t(
'tasks.showCompleted',
'Show completed tasks'
)
}
title={
showCompleted
? t(
'tasks.hideCompleted',
'Hide completed tasks'
)
: t(
'tasks.showCompleted',
'Show completed tasks'
)
}
>
<span>
{t(
'tasks.showCompleted',
'Show completed'
)}
</span>
<span
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</span>
</button>
}
/>
</div>
</div>

View file

@ -19,7 +19,8 @@ import ProjectItem from './Project/ProjectItem';
import ConfirmDialog from './Shared/ConfirmDialog';
import { searchUniversal } from '../utils/searchService';
import { getApiPath } from '../config/paths';
import SortFilterButton, { SortOption } from './Shared/SortFilterButton';
import { SortOption } from './Shared/SortFilterButton';
import IconSortDropdown from './Shared/IconSortDropdown';
interface View {
id: number;
@ -473,11 +474,11 @@ const ViewDetail: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full max-w-5xl mx-auto">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8">
<div className="flex items-center flex-1 mb-2 sm:mb-0">
<div className="flex items-center justify-between gap-2 flex-wrap sm:flex-nowrap mb-8">
<div className="flex items-center flex-1 min-w-0 gap-2">
{isEditingName ? (
<input
ref={titleInputRef}
@ -497,13 +498,13 @@ const ViewDetail: React.FC = () => {
) : (
<h2
onClick={handleEditName}
className="text-2xl font-light text-gray-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
className="text-2xl font-light text-gray-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors truncate"
>
{view.name}
</h2>
)}
</div>
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
<div className="flex items-center gap-1.5 flex-shrink-0">
<button
onClick={() => setIsSearchExpanded((v) => !v)}
className={`flex items-center transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg p-2 ${
@ -525,38 +526,65 @@ const ViewDetail: React.FC = () => {
>
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
</button>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
Show completed
</span>
<button
onClick={() => setShowCompleted((v) => !v)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
aria-pressed={showCompleted}
aria-label={
showCompleted
? 'Hide completed tasks'
: 'Show completed tasks'
}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
<SortFilterButton
<IconSortDropdown
options={sortOptions}
value={orderBy}
onChange={setOrderBy}
size="desktop"
ariaLabel={t('views.sortTasks', 'Sort tasks')}
title={t('views.sortTasks', 'Sort tasks')}
dropdownLabel={t('tasks.sortBy', 'Sort by')}
extraContent={
<button
type="button"
onClick={() => setShowCompleted((v) => !v)}
className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300"
aria-pressed={showCompleted}
aria-label={
showCompleted
? t(
'views.hideCompleted',
'Hide completed tasks'
)
: t(
'views.showCompleted',
'Show completed tasks'
)
}
title={
showCompleted
? t(
'views.hideCompleted',
'Hide completed tasks'
)
: t(
'views.showCompleted',
'Show completed tasks'
)
}
>
<span>
{t(
'common.showCompleted',
'Show completed'
)}
</span>
<span
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
showCompleted
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showCompleted
? 'translate-x-4'
: 'translate-x-0.5'
}`}
/>
</span>
</button>
}
/>
<div className="relative" ref={criteriaDropdownRef}>
<button

View file

@ -122,8 +122,8 @@ const Views: React.FC = () => {
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
<div className="w-full">
{/* Views Header */}
<div className="flex items-center mb-8">
<h2 className="text-2xl font-light">{t('views.title')}</h2>