Compare commits
No commits in common. "09c35752c6267828bc59c65d8ba57a32c76094c6" and "36309c24603771b0566e7b623df34164a9e17d67" have entirely different histories.
09c35752c6
...
36309c2460
@ -3,10 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
<meta name="theme-color" content="#09090b" />
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<title>UMBRA</title>
|
<title>UMBRA</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|||||||
@ -21,7 +21,7 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-dvh items-center justify-center">
|
<div className="flex h-screen items-center justify-center">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -39,7 +39,7 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-dvh items-center justify-center">
|
<div className="flex h-screen items-center justify-center">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export default function AdminDashboardPage() {
|
|||||||
dashboard ? dashboard.total_users - dashboard.active_users : null;
|
dashboard ? dashboard.total_users - dashboard.active_users : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 md:px-6 py-6 space-y-6 animate-fade-in">
|
<div className="px-6 py-6 space-y-6 animate-fade-in">
|
||||||
{/* Stats grid */}
|
{/* Stats grid */}
|
||||||
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-5">
|
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default function AdminPortal() {
|
|||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Portal header with tab navigation */}
|
{/* Portal header with tab navigation */}
|
||||||
<div className="shrink-0 border-b bg-card">
|
<div className="shrink-0 border-b bg-card">
|
||||||
<div className="px-4 md:px-6 h-16 flex items-center gap-4">
|
<div className="px-6 h-16 flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2 mr-6">
|
<div className="flex items-center gap-2 mr-6">
|
||||||
<div className="p-1.5 rounded-md bg-red-500/10">
|
<div className="p-1.5 rounded-md bg-red-500/10">
|
||||||
<ShieldCheck className="h-5 w-5 text-red-400" />
|
<ShieldCheck className="h-5 w-5 text-red-400" />
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export default function ConfigPage() {
|
|||||||
const totalPages = data ? Math.ceil(data.total / PER_PAGE) : 1;
|
const totalPages = data ? Math.ceil(data.total / PER_PAGE) : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 md:px-6 py-6 space-y-6 animate-fade-in">
|
<div className="px-6 py-6 space-y-6 animate-fade-in">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-row items-center justify-between flex-wrap gap-3">
|
<CardHeader className="flex-row items-center justify-between flex-wrap gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export default function IAMPage() {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 md:px-6 py-6 space-y-6 animate-fade-in">
|
<div className="px-6 py-6 space-y-6 animate-fade-in">
|
||||||
{/* Stats row */}
|
{/* Stats row */}
|
||||||
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -16,7 +15,6 @@ import { useCalendars } from '@/hooks/useCalendars';
|
|||||||
import { useSettings } from '@/hooks/useSettings';
|
import { useSettings } from '@/hooks/useSettings';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select } from '@/components/ui/select';
|
|
||||||
import CalendarSidebar from './CalendarSidebar';
|
import CalendarSidebar from './CalendarSidebar';
|
||||||
import EventDetailPanel from './EventDetailPanel';
|
import EventDetailPanel from './EventDetailPanel';
|
||||||
import type { CreateDefaults } from './EventDetailPanel';
|
import type { CreateDefaults } from './EventDetailPanel';
|
||||||
@ -163,7 +161,13 @@ export default function CalendarPage() {
|
|||||||
const panelOpen = panelMode !== 'closed';
|
const panelOpen = panelMode !== 'closed';
|
||||||
|
|
||||||
// Track desktop breakpoint to prevent dual EventDetailPanel mount
|
// Track desktop breakpoint to prevent dual EventDetailPanel mount
|
||||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
const [isDesktop, setIsDesktop] = useState(() => window.matchMedia('(min-width: 1024px)').matches);
|
||||||
|
useEffect(() => {
|
||||||
|
const mql = window.matchMedia('(min-width: 1024px)');
|
||||||
|
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
||||||
|
mql.addEventListener('change', handler);
|
||||||
|
return () => mql.removeEventListener('change', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Continuously resize calendar during panel open/close CSS transition
|
// Continuously resize calendar during panel open/close CSS transition
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -479,7 +483,7 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
<div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden">
|
<div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Custom toolbar */}
|
{/* Custom toolbar */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-2 md:gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
@ -492,17 +496,7 @@ export default function CalendarPage() {
|
|||||||
Today
|
Today
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Select
|
<div className="flex items-center rounded-md border border-border overflow-hidden">
|
||||||
value={currentView}
|
|
||||||
onChange={(e) => changeView(e.target.value as CalendarView)}
|
|
||||||
className="h-8 text-sm w-auto md:hidden"
|
|
||||||
>
|
|
||||||
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
|
|
||||||
<option key={view} value={view}>{label}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden">
|
|
||||||
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
|
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
|
||||||
<button
|
<button
|
||||||
key={view}
|
key={view}
|
||||||
@ -522,12 +516,12 @@ export default function CalendarPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-lg font-semibold font-heading hidden sm:block">{calendarTitle}</h2>
|
<h2 className="text-lg font-semibold font-heading">{calendarTitle}</h2>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* Event search */}
|
{/* Event search */}
|
||||||
<div className="relative hidden md:block">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search events..."
|
placeholder="Search events..."
|
||||||
@ -564,7 +558,8 @@ export default function CalendarPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="sm" onClick={handleCreateNew}>
|
<Button size="sm" onClick={handleCreateNew}>
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Create Event</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Event
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -86,13 +86,13 @@ export default function DashboardPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="px-4 md:px-6 py-6">
|
<div className="px-6 py-6">
|
||||||
<div className="animate-pulse space-y-2">
|
<div className="animate-pulse space-y-2">
|
||||||
<div className="h-8 w-48 rounded bg-muted" />
|
<div className="h-8 w-48 rounded bg-muted" />
|
||||||
<div className="h-4 w-32 rounded bg-muted" />
|
<div className="h-4 w-32 rounded bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-4 md:px-6 pb-6">
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||||
<DashboardSkeleton />
|
<DashboardSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +110,7 @@ export default function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header — greeting + date + quick add */}
|
{/* Header — greeting + date + quick add */}
|
||||||
<div className="px-4 md:px-6 pt-6 pb-2 flex items-center justify-between">
|
<div className="px-6 pt-6 pb-2 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-heading text-3xl font-bold tracking-tight animate-fade-in">
|
<h1 className="font-heading text-3xl font-bold tracking-tight animate-fade-in">
|
||||||
{getGreeting(settings?.preferred_name || undefined)}
|
{getGreeting(settings?.preferred_name || undefined)}
|
||||||
@ -156,7 +156,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto px-4 md:px-6 pb-6">
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{/* Week Timeline */}
|
{/* Week Timeline */}
|
||||||
{upcomingData && (
|
{upcomingData && (
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export default function AppLayout() {
|
|||||||
<LockProvider>
|
<LockProvider>
|
||||||
<AlertsProvider>
|
<AlertsProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<div className="flex h-dvh overflow-hidden bg-background">
|
<div className="flex h-screen overflow-hidden bg-background">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
|
|||||||
@ -285,10 +285,10 @@ export default function LocationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight">Locations</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight">Locations</h1>
|
||||||
|
|
||||||
<div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
|
<div className="flex-1 min-w-0">
|
||||||
<CategoryFilterBar
|
<CategoryFilterBar
|
||||||
categories={orderedCategories}
|
categories={orderedCategories}
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
@ -305,7 +305,8 @@ export default function LocationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={() => setShowForm(true)} size="sm" aria-label="Add location">
|
<Button onClick={() => setShowForm(true)} size="sm" aria-label="Add location">
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Location</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Location
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -319,7 +320,7 @@ export default function LocationsPage() {
|
|||||||
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-4 md:px-6 pb-6 pt-5">
|
<div className="px-6 pb-6 pt-5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<EntityTable<Location>
|
<EntityTable<Location>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -356,17 +357,6 @@ export default function LocationsPage() {
|
|||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
visibilityMode={visibilityMode}
|
visibilityMode={visibilityMode}
|
||||||
mobileCardRender={(location) => (
|
|
||||||
<div className={`rounded-lg border p-3 transition-colors ${selectedLocationId === location.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="font-medium text-sm truncate flex-1">{location.name}</span>
|
|
||||||
{location.category && <span className="text-[10px] text-muted-foreground">{location.category}</span>}
|
|
||||||
</div>
|
|
||||||
{location.address && (
|
|
||||||
<p className="text-xs text-muted-foreground truncate">{location.address}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export default function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center justify-between shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center justify-between shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Bell className="h-5 w-5 text-accent" aria-hidden="true" />
|
<Bell className="h-5 w-5 text-accent" aria-hidden="true" />
|
||||||
<h1 className="text-xl font-semibold font-heading">Notifications</h1>
|
<h1 className="text-xl font-semibold font-heading">Notifications</h1>
|
||||||
@ -227,7 +227,7 @@ export default function NotificationsPage() {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
onClick={() => handleNotificationClick(notification)}
|
onClick={() => handleNotificationClick(notification)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-3 px-4 md:px-6 py-3.5 transition-colors hover:bg-card-elevated group cursor-pointer',
|
'flex items-start gap-3 px-6 py-3.5 transition-colors hover:bg-card-elevated group cursor-pointer',
|
||||||
!notification.is_read && 'bg-card'
|
!notification.is_read && 'bg-card'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -555,9 +555,9 @@ export default function PeoplePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight">People</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight">People</h1>
|
||||||
<div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
|
<div className="flex-1 min-w-0">
|
||||||
<CategoryFilterBar
|
<CategoryFilterBar
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
pinnedLabel="Favourites"
|
pinnedLabel="Favourites"
|
||||||
@ -587,7 +587,8 @@ export default function PeoplePage() {
|
|||||||
aria-label="Add person"
|
aria-label="Add person"
|
||||||
className="rounded-r-none"
|
className="rounded-r-none"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Person</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Person
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -621,7 +622,7 @@ export default function PeoplePage() {
|
|||||||
<div className="flex-1 overflow-hidden flex flex-col">
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
{/* Stat bar */}
|
{/* Stat bar */}
|
||||||
{!isLoading && people.length > 0 && (
|
{!isLoading && people.length > 0 && (
|
||||||
<div className="px-4 md:px-6 pt-4 pb-2 flex items-start gap-6 shrink-0">
|
<div className="px-6 pt-4 pb-2 flex items-start gap-6 shrink-0">
|
||||||
<div className="flex gap-6 shrink-0">
|
<div className="flex gap-6 shrink-0">
|
||||||
<StatCounter
|
<StatCounter
|
||||||
icon={Users}
|
icon={Users}
|
||||||
@ -664,7 +665,7 @@ export default function PeoplePage() {
|
|||||||
|
|
||||||
{/* Pending requests */}
|
{/* Pending requests */}
|
||||||
{hasRequests && (
|
{hasRequests && (
|
||||||
<div className="px-4 md:px-6 pb-3">
|
<div className="px-6 pb-3">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-wider font-medium">
|
<span className="text-xs text-muted-foreground uppercase tracking-wider font-medium">
|
||||||
Pending Requests
|
Pending Requests
|
||||||
@ -705,7 +706,7 @@ export default function PeoplePage() {
|
|||||||
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-4 md:px-6 pb-6">
|
<div className="px-6 pb-6">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<EntityTable<Person>
|
<EntityTable<Person>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -744,18 +745,6 @@ export default function PeoplePage() {
|
|||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
visibilityMode={visibilityMode}
|
visibilityMode={visibilityMode}
|
||||||
mobileCardRender={(person) => (
|
|
||||||
<div className={`rounded-lg border p-3 transition-colors ${selectedPersonId === person.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="font-medium text-sm truncate flex-1">{person.name}</span>
|
|
||||||
{person.category && <span className="text-[10px] text-muted-foreground">{person.category}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
{person.email && <span className="truncate">{person.email}</span>}
|
|
||||||
{person.phone && <span>{person.phone}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -345,13 +345,13 @@ export default function ProjectDetail() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-2 md:gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
|
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight flex-1">Loading...</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight flex-1">Loading...</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-4 md:px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
<ListSkeleton rows={4} />
|
<ListSkeleton rows={4} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -375,7 +375,7 @@ export default function ProjectDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
|
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -396,7 +396,8 @@ export default function ProjectDetail() {
|
|||||||
<Pin className={`h-4 w-4 ${project.is_tracked ? 'fill-current' : ''}`} />
|
<Pin className={`h-4 w-4 ${project.is_tracked ? 'fill-current' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={() => setShowProjectForm(true)}>
|
<Button variant="outline" size="sm" onClick={() => setShowProjectForm(true)}>
|
||||||
<Pencil className="h-3.5 w-3.5 md:mr-2" /><span className="hidden md:inline">Edit</span>
|
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -408,14 +409,15 @@ export default function ProjectDetail() {
|
|||||||
}}
|
}}
|
||||||
disabled={deleteProjectMutation.isPending}
|
disabled={deleteProjectMutation.isPending}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5 md:mr-2" /><span className="hidden md:inline">Delete</span>
|
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
||||||
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content area */}
|
{/* Content area */}
|
||||||
<div className="flex-1 overflow-hidden flex flex-col">
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
{/* Summary section - scrolls with left panel on small, fixed on large */}
|
{/* Summary section - scrolls with left panel on small, fixed on large */}
|
||||||
<div className="px-4 md:px-6 py-5 space-y-5 shrink-0 overflow-y-auto max-h-[50vh] lg:max-h-none lg:overflow-visible">
|
<div className="px-6 py-5 space-y-5 shrink-0 overflow-y-auto max-h-[50vh] lg:max-h-none lg:overflow-visible">
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{project.description && (
|
{project.description && (
|
||||||
<p className="text-sm text-muted-foreground">{project.description}</p>
|
<p className="text-sm text-muted-foreground">{project.description}</p>
|
||||||
@ -488,7 +490,7 @@ export default function ProjectDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task list header + view controls */}
|
{/* Task list header + view controls */}
|
||||||
<div className="px-4 md:px-6 pb-3 flex items-center justify-between flex-wrap gap-2 shrink-0">
|
<div className="px-6 pb-3 flex items-center justify-between shrink-0">
|
||||||
<h2 className="font-heading text-lg font-semibold">Tasks</h2>
|
<h2 className="font-heading text-lg font-semibold">Tasks</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* View toggle */}
|
{/* View toggle */}
|
||||||
@ -542,7 +544,7 @@ export default function ProjectDetail() {
|
|||||||
<div className="flex-1 overflow-hidden flex">
|
<div className="flex-1 overflow-hidden flex">
|
||||||
{/* Left panel: task list or kanban */}
|
{/* Left panel: task list or kanban */}
|
||||||
<div className={`overflow-y-auto transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${selectedTaskId ? 'w-full lg:w-[55%]' : 'w-full'}`}>
|
<div className={`overflow-y-auto transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${selectedTaskId ? 'w-full lg:w-[55%]' : 'w-full'}`}>
|
||||||
<div className="px-4 md:px-6 pb-6">
|
<div className="px-6 pb-6">
|
||||||
{topLevelTasks.length === 0 ? (
|
{topLevelTasks.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={ListChecks}
|
icon={ListChecks}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import type { Project } from '@/types';
|
import type { Project } from '@/types';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/select';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { GridSkeleton } from '@/components/ui/skeleton';
|
import { GridSkeleton } from '@/components/ui/skeleton';
|
||||||
@ -71,19 +70,10 @@ export default function ProjectsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1>
|
||||||
|
|
||||||
<Select
|
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4">
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
|
||||||
className="h-8 text-sm w-auto md:hidden"
|
|
||||||
>
|
|
||||||
{statusFilters.map((sf) => (
|
|
||||||
<option key={sf.value} value={sf.value}>{sf.label}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
|
|
||||||
{statusFilters.map((sf) => (
|
{statusFilters.map((sf) => (
|
||||||
<button
|
<button
|
||||||
key={sf.value}
|
key={sf.value}
|
||||||
@ -105,7 +95,7 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
<div className="relative hidden md:block">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
@ -116,11 +106,12 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={() => setShowForm(true)} size="sm">
|
<Button onClick={() => setShowForm(true)} size="sm">
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">New Project</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto px-4 md:px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
{/* Summary stats */}
|
{/* Summary stats */}
|
||||||
{!isLoading && projects.length > 0 && (
|
{!isLoading && projects.length > 0 && (
|
||||||
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { isPast, isToday, parseISO } from 'date-fns';
|
|||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import type { Reminder } from '@/types';
|
import type { Reminder } from '@/types';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/select';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { ListSkeleton } from '@/components/ui/skeleton';
|
import { ListSkeleton } from '@/components/ui/skeleton';
|
||||||
@ -100,19 +99,10 @@ export default function RemindersPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1>
|
||||||
|
|
||||||
<Select
|
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4">
|
||||||
value={filter}
|
|
||||||
onChange={(e) => setFilter(e.target.value as typeof filter)}
|
|
||||||
className="h-8 text-sm w-auto md:hidden"
|
|
||||||
>
|
|
||||||
{statusFilters.map((sf) => (
|
|
||||||
<option key={sf.value} value={sf.value}>{sf.label}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
|
|
||||||
{statusFilters.map((sf) => (
|
{statusFilters.map((sf) => (
|
||||||
<button
|
<button
|
||||||
key={sf.value}
|
key={sf.value}
|
||||||
@ -135,7 +125,7 @@ export default function RemindersPage() {
|
|||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
<div className="relative hidden md:block">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
@ -146,7 +136,8 @@ export default function RemindersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleCreateNew} size="sm">
|
<Button onClick={handleCreateNew} size="sm">
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Reminder</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Reminder
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -157,7 +148,7 @@ export default function RemindersPage() {
|
|||||||
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-4 md:px-6 py-5">
|
<div className="px-6 py-5">
|
||||||
{/* Summary stats */}
|
{/* Summary stats */}
|
||||||
{!isLoading && reminders.length > 0 && (
|
{!isLoading && reminders.length > 0 && (
|
||||||
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
||||||
|
|||||||
@ -344,7 +344,7 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Page header — matches Stage 4-5 pages */}
|
{/* Page header — matches Stage 4-5 pages */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-3 shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-3 shrink-0">
|
||||||
<Settings className="h-5 w-5 text-accent" aria-hidden="true" />
|
<Settings className="h-5 w-5 text-accent" aria-hidden="true" />
|
||||||
<h1 className="text-xl font-semibold font-heading">Settings</h1>
|
<h1 className="text-xl font-semibold font-heading">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
||||||
|
|
||||||
export interface ColumnDef<T> {
|
export interface ColumnDef<T> {
|
||||||
key: string;
|
key: string;
|
||||||
@ -29,7 +28,6 @@ interface EntityTableProps<T extends { id: number }> {
|
|||||||
onSort: (key: string) => void;
|
onSort: (key: string) => void;
|
||||||
visibilityMode: VisibilityMode;
|
visibilityMode: VisibilityMode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
mobileCardRender?: (item: T) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all'];
|
const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all'];
|
||||||
@ -129,51 +127,10 @@ export function EntityTable<T extends { id: number }>({
|
|||||||
onSort,
|
onSort,
|
||||||
visibilityMode,
|
visibilityMode,
|
||||||
loading = false,
|
loading = false,
|
||||||
mobileCardRender,
|
|
||||||
}: EntityTableProps<T>) {
|
}: EntityTableProps<T>) {
|
||||||
const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode));
|
const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode));
|
||||||
const colCount = visibleColumns.length;
|
const colCount = visibleColumns.length;
|
||||||
const showPinnedSection = showPinned && pinnedRows.length > 0;
|
const showPinnedSection = showPinned && pinnedRows.length > 0;
|
||||||
const isMobile = useMediaQuery('(max-width: 767px)');
|
|
||||||
|
|
||||||
if (isMobile && mobileCardRender) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{loading ? (
|
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
|
||||||
<div key={i} className="animate-pulse rounded-lg bg-card border border-border p-4 h-20" />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{showPinnedSection && (
|
|
||||||
<>
|
|
||||||
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{pinnedLabel}</p>
|
|
||||||
{pinnedRows.map((item) => (
|
|
||||||
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer">
|
|
||||||
{mobileCardRender(item)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{groups.map((group) => (
|
|
||||||
<React.Fragment key={group.label}>
|
|
||||||
{group.rows.length > 0 && (
|
|
||||||
<>
|
|
||||||
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{group.label}</p>
|
|
||||||
{group.rows.map((item) => (
|
|
||||||
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer">
|
|
||||||
{mobileCardRender(item)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import api from '@/lib/api';
|
|||||||
import type { Todo } from '@/types';
|
import type { Todo } from '@/types';
|
||||||
import { isTodoOverdue } from '@/lib/utils';
|
import { isTodoOverdue } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/select';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { ListSkeleton } from '@/components/ui/skeleton';
|
import { ListSkeleton } from '@/components/ui/skeleton';
|
||||||
import { CategoryFilterBar } from '@/components/shared';
|
import { CategoryFilterBar } from '@/components/shared';
|
||||||
@ -129,20 +128,11 @@ export default function TodosPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full animate-fade-in">
|
<div className="flex flex-col h-full animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight">Todos</h1>
|
<h1 className="font-heading text-2xl font-bold tracking-tight">Todos</h1>
|
||||||
|
|
||||||
{/* Priority filter */}
|
{/* Priority filter */}
|
||||||
<Select
|
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4">
|
||||||
value={priorityFilter}
|
|
||||||
onChange={(e) => setPriorityFilter(e.target.value as typeof priorityFilter)}
|
|
||||||
className="h-8 text-sm w-auto md:hidden"
|
|
||||||
>
|
|
||||||
{priorityFilters.map((pf) => (
|
|
||||||
<option key={pf.value} value={pf.value}>{pf.label}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
|
|
||||||
{priorityFilters.map((pf) => (
|
{priorityFilters.map((pf) => (
|
||||||
<button
|
<button
|
||||||
key={pf.value}
|
key={pf.value}
|
||||||
@ -164,7 +154,7 @@ export default function TodosPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category filter bar (All + Completed + Categories with drag) */}
|
{/* Category filter bar (All + Completed + Categories with drag) */}
|
||||||
<div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
|
<div className="flex-1 min-w-0">
|
||||||
<CategoryFilterBar
|
<CategoryFilterBar
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
pinnedLabel="Completed"
|
pinnedLabel="Completed"
|
||||||
@ -181,7 +171,8 @@ export default function TodosPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleCreateNew} size="sm">
|
<Button onClick={handleCreateNew} size="sm">
|
||||||
<Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Todo</span>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Todo
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -192,7 +183,7 @@ export default function TodosPage() {
|
|||||||
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
panelOpen ? 'w-full lg:w-[55%]' : 'w-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-4 md:px-6 py-5">
|
<div className="px-6 py-5">
|
||||||
{/* Summary stats */}
|
{/* Summary stats */}
|
||||||
{!isLoading && todos.length > 0 && (
|
{!isLoading && todos.length > 0 && (
|
||||||
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const buttonVariants = cva(
|
|||||||
default: 'h-10 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
sm: 'h-9 rounded-md px-3',
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: 'h-11 rounded-md px-8',
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: 'h-11 w-11 md:h-10 md:w-10',
|
icon: 'h-10 w-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full appearance-none rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 invalid:ring-red-500 invalid:border-red-500',
|
'flex h-10 w-full appearance-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 invalid:ring-red-500 invalid:border-red-500',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const Sheet: React.FC<SheetProps> = ({ open, onOpenChange, children }) => {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed right-0 top-0 h-full w-full sm:max-w-[540px] transition-transform duration-350',
|
'fixed right-0 top-0 h-full w-full max-w-[540px] transition-transform duration-350',
|
||||||
visible ? 'translate-x-0' : 'translate-x-full'
|
visible ? 'translate-x-0' : 'translate-x-full'
|
||||||
)}
|
)}
|
||||||
style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
|
style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export function useMediaQuery(query: string): boolean {
|
|
||||||
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mql = window.matchMedia(query);
|
|
||||||
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
||||||
mql.addEventListener('change', handler);
|
|
||||||
return () => mql.removeEventListener('change', handler);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
future: { hoverOnlyWhenSupported: true },
|
|
||||||
darkMode: ['class'],
|
darkMode: ['class'],
|
||||||
content: [
|
content: [
|
||||||
'./index.html',
|
'./index.html',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user