refactor: improve layout structure and consistency across components
- Refactored layout structure in multiple components to enhance consistency and maintainability by introducing outer and inner wrapper classes. - Updated the `MonthNavHeader` component for shared month navigation functionality, improving code reuse. - Adjusted padding and margin properties in various components to ensure a cohesive design and better responsiveness. - Removed unnecessary padding from certain elements to streamline the layout and improve visual clarity.
This commit is contained in:
@@ -10,14 +10,15 @@ import { useEffect } from "react";
|
|||||||
import { useAdminPage, AdminDutyList, ReassignSheet } from "@/components/admin";
|
import { useAdminPage, AdminDutyList, ReassignSheet } from "@/components/admin";
|
||||||
import { useTranslation } from "@/i18n/use-translation";
|
import { useTranslation } from "@/i18n/use-translation";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
|
import { MonthNavHeader } from "@/components/calendar/MonthNavHeader";
|
||||||
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
||||||
import { LoadingState } from "@/components/states/LoadingState";
|
import { LoadingState } from "@/components/states/LoadingState";
|
||||||
import { ErrorState } from "@/components/states/ErrorState";
|
import { ErrorState } from "@/components/states/ErrorState";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon } from "lucide-react";
|
|
||||||
|
|
||||||
const PAGE_WRAPPER_CLASS =
|
const OUTER_CLASS =
|
||||||
"content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6";
|
"content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background";
|
||||||
|
const INNER_CLASS =
|
||||||
|
"mx-auto flex w-full max-w-[var(--max-width-app)] flex-col";
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { t, monthName } = useTranslation();
|
const { t, monthName } = useTranslation();
|
||||||
@@ -31,31 +32,37 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
if (!admin.isAllowed) {
|
if (!admin.isAllowed) {
|
||||||
return (
|
return (
|
||||||
<div className={PAGE_WRAPPER_CLASS}>
|
<div className={OUTER_CLASS}>
|
||||||
<AccessDeniedScreen primaryAction="reload" />
|
<div className={INNER_CLASS}>
|
||||||
|
<AccessDeniedScreen primaryAction="reload" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin.adminCheckComplete === null) {
|
if (admin.adminCheckComplete === null) {
|
||||||
return (
|
return (
|
||||||
<div className={PAGE_WRAPPER_CLASS}>
|
<div className={OUTER_CLASS}>
|
||||||
|
<div className={INNER_CLASS}>
|
||||||
<div className="py-4 flex flex-col items-center gap-2">
|
<div className="py-4 flex flex-col items-center gap-2">
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
<p className="text-sm text-muted-foreground">{t("admin.loading_users")}</p>
|
<p className="text-sm text-muted-foreground">{t("admin.loading_users")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin.adminAccessDenied) {
|
if (admin.adminAccessDenied) {
|
||||||
return (
|
return (
|
||||||
<div className={PAGE_WRAPPER_CLASS}>
|
<div className={OUTER_CLASS}>
|
||||||
|
<div className={INNER_CLASS}>
|
||||||
<div className="flex flex-col gap-4 py-6">
|
<div className="flex flex-col gap-4 py-6">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}
|
{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -64,46 +71,17 @@ export default function AdminPage() {
|
|||||||
const year = admin.adminMonth.getFullYear();
|
const year = admin.adminMonth.getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={PAGE_WRAPPER_CLASS}>
|
<div className={OUTER_CLASS}>
|
||||||
|
<div className={INNER_CLASS}>
|
||||||
<header className="sticky top-[var(--app-safe-top)] z-10 flex flex-col items-center border-b border-border bg-background py-3">
|
<header className="sticky top-[var(--app-safe-top)] z-10 flex flex-col items-center border-b border-border bg-background py-3">
|
||||||
<div className="flex w-full items-center justify-between px-1">
|
<MonthNavHeader
|
||||||
<Button
|
month={admin.adminMonth}
|
||||||
type="button"
|
disabled={admin.loading}
|
||||||
variant="secondary"
|
onPrevMonth={admin.onPrevMonth}
|
||||||
size="icon"
|
onNextMonth={admin.onNextMonth}
|
||||||
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
|
titleAriaLabel={`${t("admin.title")}, ${monthName(month)} ${year}`}
|
||||||
aria-label={t("nav.prev_month")}
|
className="w-full px-1"
|
||||||
disabled={admin.loading}
|
/>
|
||||||
onClick={admin.onPrevMonth}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="size-5" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
<h1
|
|
||||||
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
|
|
||||||
aria-label={`${t("admin.title")}, ${monthName(month)} ${year}`}
|
|
||||||
>
|
|
||||||
<span className="text-xs font-normal leading-none text-muted">
|
|
||||||
{year}
|
|
||||||
</span>
|
|
||||||
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
|
|
||||||
{monthName(month)}
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
|
|
||||||
aria-label={t("nav.next_month")}
|
|
||||||
disabled={admin.loading}
|
|
||||||
onClick={admin.onNextMonth}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="size-5" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground m-0 mt-1">
|
|
||||||
{admin.loading ? "…" : t("admin.duties_count", { count: String(admin.dutyOnly.length) })}
|
|
||||||
</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{admin.successMessage && (
|
{admin.successMessage && (
|
||||||
@@ -157,6 +135,7 @@ export default function AdminPage() {
|
|||||||
onCloseAnimationEnd={admin.closeReassign}
|
onCloseAnimationEnd={admin.closeReassign}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,15 +56,19 @@ export default function Home() {
|
|||||||
}, [setCurrentView, setSelectedDay]);
|
}, [setCurrentView, setSelectedDay]);
|
||||||
|
|
||||||
const content = accessDenied ? (
|
const content = accessDenied ? (
|
||||||
<div className="content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
<div className="content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background">
|
||||||
<AccessDeniedScreen primaryAction="reload" />
|
<div className="mx-auto flex w-full max-w-[var(--max-width-app)] flex-col">
|
||||||
|
<AccessDeniedScreen primaryAction="reload" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : currentView === "currentDuty" ? (
|
) : currentView === "currentDuty" ? (
|
||||||
<div className="content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
<div className="content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background">
|
||||||
<CurrentDutyView
|
<div className="mx-auto flex w-full max-w-[var(--max-width-app)] flex-col">
|
||||||
onBack={handleBackFromCurrentDuty}
|
<CurrentDutyView
|
||||||
openedFromPin={startParam === "duty"}
|
onBack={handleBackFromCurrentDuty}
|
||||||
/>
|
openedFromPin={startParam === "duty"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CalendarPage isAllowed={isAllowed} initDataRaw={initDataRaw} />
|
<CalendarPage isAllowed={isAllowed} initDataRaw={initDataRaw} />
|
||||||
|
|||||||
@@ -168,41 +168,43 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
}, [isAdmin, router]);
|
}, [isAdmin, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
<div className="content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background">
|
||||||
<div
|
<div className="mx-auto flex w-full max-w-[var(--max-width-app)] flex-col">
|
||||||
ref={calendarStickyRef}
|
<div
|
||||||
className="sticky top-[var(--app-safe-top)] z-10 min-h-[var(--calendar-block-min-height)] bg-background pb-2 touch-pan-y"
|
ref={calendarStickyRef}
|
||||||
>
|
className="sticky top-[var(--app-safe-top)] z-10 min-h-[var(--calendar-block-min-height)] bg-background pb-2 touch-pan-y"
|
||||||
<CalendarHeader
|
>
|
||||||
month={currentMonth}
|
<CalendarHeader
|
||||||
disabled={navDisabled}
|
month={currentMonth}
|
||||||
onPrevMonth={handlePrevMonth}
|
disabled={navDisabled}
|
||||||
onNextMonth={handleNextMonth}
|
onPrevMonth={handlePrevMonth}
|
||||||
/>
|
onNextMonth={handleNextMonth}
|
||||||
<CalendarGrid
|
/>
|
||||||
currentMonth={currentMonth}
|
<CalendarGrid
|
||||||
|
currentMonth={currentMonth}
|
||||||
|
duties={duties}
|
||||||
|
calendarEvents={calendarEvents}
|
||||||
|
onDayClick={handleDayClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<ErrorState message={error} onRetry={retry} className="my-3" />
|
||||||
|
)}
|
||||||
|
{!error && (
|
||||||
|
<DutyList
|
||||||
|
scrollMarginTop={stickyBlockHeight}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DayDetail
|
||||||
|
ref={dayDetailRef}
|
||||||
duties={duties}
|
duties={duties}
|
||||||
calendarEvents={calendarEvents}
|
calendarEvents={calendarEvents}
|
||||||
onDayClick={handleDayClick}
|
onClose={handleCloseDayDetail}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<ErrorState message={error} onRetry={retry} className="my-3" />
|
|
||||||
)}
|
|
||||||
{!error && (
|
|
||||||
<DutyList
|
|
||||||
scrollMarginTop={stickyBlockHeight}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DayDetail
|
|
||||||
ref={dayDetailRef}
|
|
||||||
duties={duties}
|
|
||||||
calendarEvents={calendarEvents}
|
|
||||||
onClose={handleCloseDayDetail}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,9 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useTranslation } from "@/i18n/use-translation";
|
import { useTranslation } from "@/i18n/use-translation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { MonthNavHeader } from "@/components/calendar/MonthNavHeader";
|
||||||
ChevronLeft as ChevronLeftIcon,
|
|
||||||
ChevronRight as ChevronRightIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
export interface CalendarHeaderProps {
|
export interface CalendarHeaderProps {
|
||||||
/** Currently displayed month (used for title). */
|
/** Currently displayed month (used for title). */
|
||||||
@@ -30,51 +26,19 @@ export function CalendarHeader({
|
|||||||
onNextMonth,
|
onNextMonth,
|
||||||
className,
|
className,
|
||||||
}: CalendarHeaderProps) {
|
}: CalendarHeaderProps) {
|
||||||
const { t, monthName, weekdayLabels } = useTranslation();
|
const { weekdayLabels } = useTranslation();
|
||||||
const year = month.getFullYear();
|
|
||||||
const monthIndex = month.getMonth();
|
|
||||||
const labels = weekdayLabels();
|
const labels = weekdayLabels();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn("flex flex-col", className)}>
|
<header className={cn("flex flex-col", className)}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<MonthNavHeader
|
||||||
<Button
|
month={month}
|
||||||
type="button"
|
disabled={disabled}
|
||||||
variant="secondary"
|
onPrevMonth={onPrevMonth}
|
||||||
size="icon"
|
onNextMonth={onNextMonth}
|
||||||
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
|
ariaLive
|
||||||
aria-label={t("nav.prev_month")}
|
className="mb-3"
|
||||||
disabled={disabled}
|
/>
|
||||||
onClick={onPrevMonth}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="size-5" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
<div className="flex min-h-[2rem] flex-col items-center justify-center gap-0">
|
|
||||||
<h1
|
|
||||||
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-atomic="true"
|
|
||||||
>
|
|
||||||
<span className="text-xs font-normal leading-none text-muted">
|
|
||||||
{year}
|
|
||||||
</span>
|
|
||||||
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
|
|
||||||
{monthName(monthIndex)}
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
|
|
||||||
aria-label={t("nav.next_month")}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onNextMonth}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="size-5" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-7 gap-0.5 mb-1.5 text-center text-[0.75rem] text-muted">
|
<div className="grid grid-cols-7 gap-0.5 mb-1.5 text-center text-[0.75rem] text-muted">
|
||||||
{labels.map((label, i) => (
|
{labels.map((label, i) => (
|
||||||
<span key={i} aria-hidden>
|
<span key={i} aria-hidden>
|
||||||
|
|||||||
86
webapp-next/src/components/calendar/MonthNavHeader.tsx
Normal file
86
webapp-next/src/components/calendar/MonthNavHeader.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Shared month navigation row: prev button, year + month title, next button.
|
||||||
|
* Used by CalendarHeader and admin page for consistent layout and spacing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTranslation } from "@/i18n/use-translation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
ChevronLeft as ChevronLeftIcon,
|
||||||
|
ChevronRight as ChevronRightIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export interface MonthNavHeaderProps {
|
||||||
|
/** Currently displayed month (used for title). */
|
||||||
|
month: Date;
|
||||||
|
/** Whether month navigation is disabled (e.g. during loading). */
|
||||||
|
disabled?: boolean;
|
||||||
|
onPrevMonth: () => void;
|
||||||
|
onNextMonth: () => void;
|
||||||
|
/** Optional aria-label for the month title (e.g. admin page). */
|
||||||
|
titleAriaLabel?: string;
|
||||||
|
/** When true, title is announced on change (e.g. calendar). */
|
||||||
|
ariaLive?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_BUTTON_CLASS =
|
||||||
|
"size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50";
|
||||||
|
|
||||||
|
export function MonthNavHeader({
|
||||||
|
month,
|
||||||
|
disabled = false,
|
||||||
|
onPrevMonth,
|
||||||
|
onNextMonth,
|
||||||
|
titleAriaLabel,
|
||||||
|
ariaLive = false,
|
||||||
|
className,
|
||||||
|
}: MonthNavHeaderProps) {
|
||||||
|
const { t, monthName } = useTranslation();
|
||||||
|
const year = month.getFullYear();
|
||||||
|
const monthIndex = month.getMonth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center justify-between", className)}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className={NAV_BUTTON_CLASS}
|
||||||
|
aria-label={t("nav.prev_month")}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onPrevMonth}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="size-5" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
<div className="flex min-h-[2rem] flex-col items-center justify-center gap-0">
|
||||||
|
<h1
|
||||||
|
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
|
||||||
|
{...(titleAriaLabel ? { "aria-label": titleAriaLabel } : {})}
|
||||||
|
{...(ariaLive ? { "aria-live": "polite", "aria-atomic": true } : {})}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-normal leading-none text-muted">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
|
||||||
|
{monthName(monthIndex)}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className={NAV_BUTTON_CLASS}
|
||||||
|
aria-label={t("nav.next_month")}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onNextMonth}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="size-5" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -168,7 +168,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
|||||||
if (state === "loading") {
|
if (state === "loading") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-4"
|
className="flex min-h-[50vh] flex-col items-center justify-center gap-4"
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label={t("loading")}
|
aria-label={t("loading")}
|
||||||
@@ -221,7 +221,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
|||||||
loadTodayDuties();
|
loadTodayDuties();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-4">
|
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4">
|
||||||
<Card className="w-full max-w-[var(--max-width-app)]">
|
<Card className="w-full max-w-[var(--max-width-app)]">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<p className="text-error">{errorMessage}</p>
|
<p className="text-error">{errorMessage}</p>
|
||||||
@@ -249,7 +249,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
|||||||
|
|
||||||
if (!duty) {
|
if (!duty) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-4">
|
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4">
|
||||||
<Card className="current-duty-card--no-duty w-full max-w-[var(--max-width-app)] border-t-4 border-t-muted">
|
<Card className="current-duty-card--no-duty w-full max-w-[var(--max-width-app)] border-t-4 border-t-muted">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("current_duty.title")}</CardTitle>
|
<CardTitle>{t("current_duty.title")}</CardTitle>
|
||||||
@@ -307,7 +307,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
|||||||
Boolean(duty.username && String(duty.username).trim());
|
Boolean(duty.username && String(duty.username).trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-4">
|
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4">
|
||||||
<Card
|
<Card
|
||||||
className="current-duty-card w-full max-w-[var(--max-width-app)] border-t-4 border-t-duty animate-in fade-in-0 slide-in-from-bottom-4 duration-300 motion-reduce:animate-none motion-reduce:duration-0"
|
className="current-duty-card w-full max-w-[var(--max-width-app)] border-t-4 border-t-duty animate-in fade-in-0 slide-in-from-bottom-4 duration-300 motion-reduce:animate-none motion-reduce:duration-0"
|
||||||
role="article"
|
role="article"
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export const DayDetail = React.forwardRef<DayDetailHandle, DayDetailProps>(
|
|||||||
if (!open || !selectedDay) return null;
|
if (!open || !selectedDay) return null;
|
||||||
|
|
||||||
const panelClassName =
|
const panelClassName =
|
||||||
"max-w-[min(360px,calc(100vw-24px))] max-h-[70vh] overflow-auto bg-surface text-[var(--text)] rounded-xl shadow-lg p-4 pt-9";
|
"max-w-[min(360px,calc(100vw - var(--app-safe-left, 0) - var(--app-safe-right, 0) - 24px))] max-h-[70vh] overflow-auto bg-surface text-[var(--text)] rounded-xl shadow-lg p-4 pt-9";
|
||||||
const closeButton = (
|
const closeButton = (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export interface FullScreenStateShellProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WRAPPER_CLASS =
|
const OUTER_CLASS =
|
||||||
"content-safe flex min-h-[var(--tg-viewport-stable-height,100vh)] flex-col items-center justify-center gap-4 bg-background px-4 text-foreground";
|
"content-safe flex min-h-[var(--tg-viewport-stable-height,100vh)] flex-col items-center justify-center gap-4 bg-background text-foreground";
|
||||||
|
const INNER_CLASS = "mx-auto w-full max-w-[var(--max-width-app)] px-3 flex flex-col items-center gap-4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full-screen centered shell with title, optional description, and primary action.
|
* Full-screen centered shell with title, optional description, and primary action.
|
||||||
@@ -37,15 +38,17 @@ export function FullScreenStateShell({
|
|||||||
}: FullScreenStateShellProps) {
|
}: FullScreenStateShellProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className ? `${WRAPPER_CLASS} ${className}` : WRAPPER_CLASS}
|
className={className ? `${OUTER_CLASS} ${className}` : OUTER_CLASS}
|
||||||
role={role}
|
role={role}
|
||||||
>
|
>
|
||||||
<h1 className="text-xl font-semibold">{title}</h1>
|
<div className={INNER_CLASS}>
|
||||||
{description != null && (
|
<h1 className="text-xl font-semibold">{title}</h1>
|
||||||
<p className="text-center text-muted-foreground">{description}</p>
|
{description != null && (
|
||||||
)}
|
<p className="text-center text-muted-foreground">{description}</p>
|
||||||
{children}
|
)}
|
||||||
{primaryAction}
|
{children}
|
||||||
|
{primaryAction}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-4 sm:px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-content"
|
data-slot="card-content"
|
||||||
className={cn("px-6", className)}
|
className={cn("px-4 sm:px-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-4 sm:px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export const MESSAGES: Record<Lang, Record<string, string>> = {
|
|||||||
"admin.no_duties": "No duties this month.",
|
"admin.no_duties": "No duties this month.",
|
||||||
"admin.no_users_for_assign": "No users available for assignment.",
|
"admin.no_users_for_assign": "No users available for assignment.",
|
||||||
"admin.save": "Save",
|
"admin.save": "Save",
|
||||||
"admin.duties_count": "{count} duties this month",
|
|
||||||
"admin.list_aria": "List of duties to reassign",
|
"admin.list_aria": "List of duties to reassign",
|
||||||
"admin.reassign_aria": "Reassign duty: {date}, {time}, {name}",
|
"admin.reassign_aria": "Reassign duty: {date}, {time}, {name}",
|
||||||
"admin.section_aria": "Duties for {date}",
|
"admin.section_aria": "Duties for {date}",
|
||||||
@@ -195,7 +194,6 @@ export const MESSAGES: Record<Lang, Record<string, string>> = {
|
|||||||
"admin.no_duties": "В этом месяце дежурств нет.",
|
"admin.no_duties": "В этом месяце дежурств нет.",
|
||||||
"admin.no_users_for_assign": "Нет пользователей для назначения.",
|
"admin.no_users_for_assign": "Нет пользователей для назначения.",
|
||||||
"admin.save": "Сохранить",
|
"admin.save": "Сохранить",
|
||||||
"admin.duties_count": "{count} дежурств в этом месяце",
|
|
||||||
"admin.list_aria": "Список дежурств для перераспределения",
|
"admin.list_aria": "Список дежурств для перераспределения",
|
||||||
"admin.reassign_aria": "Переназначить дежурство: {date}, {time}, {name}",
|
"admin.reassign_aria": "Переназначить дежурство: {date}, {time}, {name}",
|
||||||
"admin.section_aria": "Дежурства за {date}",
|
"admin.section_aria": "Дежурства за {date}",
|
||||||
|
|||||||
Reference in New Issue
Block a user