app now responsive for mobile and small devices.

This commit is contained in:
2026-07-11 17:10:02 +03:30
parent 893cd5b128
commit 2f7df312c1
49 changed files with 1710 additions and 775 deletions

View File

@@ -32,7 +32,7 @@ export const Dropdown = forwardRef<HTMLSelectElement, DropdownProps>(
form-select w-full appearance-none rounded-[var(--radius-md)] border
${error ? 'border-red-500' : 'border-border'}
bg-background-card text-text-primary
pl-4 pr-14 py-2 text-sm
pl-4 pr-14 py-2.5 sm:py-2 text-base sm:text-sm
focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]

View File

@@ -91,7 +91,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
${error ? 'border-red-500' : 'border-border'}
bg-background-secondary/90 text-text-primary
${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2
${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2.5 sm:py-2 text-base sm:text-sm
placeholder:text-text-muted

View File

@@ -70,7 +70,7 @@ export function LanguageToggle() {
<ul
role="listbox"
aria-label={t('selectLanguage')}
className="absolute right-0 z-[200] mt-2 min-w-[10rem] rounded-[var(--radius-md)] border border-border bg-background-secondary/95 py-1 shadow-lg backdrop-blur-sm"
className="absolute right-0 z-[200] mt-2 w-[min(10rem,calc(100vw-2rem))] rounded-[var(--radius-md)] border border-border bg-background-secondary/95 py-1 shadow-lg backdrop-blur-sm"
>
{LOCALE_OPTIONS.map((option) => {
const selected = option === locale;

View File

@@ -0,0 +1,19 @@
'use client';
import { ChevronLeft } from 'lucide-react';
import { useTranslations } from 'next-intl';
export function MobileDetailBackButton({ onClick }: { onClick: () => void }) {
const t = useTranslations('common');
return (
<button
type="button"
onClick={onClick}
className="lg:hidden inline-flex items-center gap-1 text-sm text-primary hover:opacity-90 mb-3"
>
<ChevronLeft className="h-4 w-4 icon-flat" />
{t('back')}
</button>
);
}

View File

@@ -0,0 +1,61 @@
'use client';
import type { ReactNode } from 'react';
type ResponsiveDialogOverlayProps = {
children: ReactNode;
onBackdropClick?: () => void;
className?: string;
zIndexClass?: string;
};
type ResponsiveDialogPanelProps = {
children: ReactNode;
className?: string;
maxWidthClass?: string;
role?: string;
'aria-modal'?: boolean | 'true' | 'false';
'aria-labelledby'?: string;
};
export function ResponsiveDialogOverlay({
children,
onBackdropClick,
className = '',
zIndexClass = 'z-50',
}: ResponsiveDialogOverlayProps) {
return (
<div
className={`fixed inset-0 ${zIndexClass} flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50 ${className}`.trim()}
role="presentation"
onMouseDown={(e) => {
if (onBackdropClick && e.target === e.currentTarget) {
onBackdropClick();
}
}}
>
{children}
</div>
);
}
export function ResponsiveDialogPanel({
children,
className = '',
maxWidthClass = 'sm:max-w-lg',
role,
'aria-modal': ariaModal,
'aria-labelledby': ariaLabelledby,
}: ResponsiveDialogPanelProps) {
return (
<div
className={`w-full ${maxWidthClass} max-h-[90dvh] overflow-y-auto rounded-t-[var(--radius-lg)] sm:rounded-[var(--radius-md)] border border-border bg-background-secondary p-4 sm:p-6 shadow-xl ${className}`.trim()}
role={role}
aria-modal={ariaModal}
aria-labelledby={ariaLabelledby}
onMouseDown={(e) => e.stopPropagation()}
>
{children}
</div>
);
}

View File

@@ -21,8 +21,8 @@ export function SearchBar({
embedded = false,
}: SearchBarProps) {
const field = (
<div className={`flex gap-4 items-center ${embedded ? '' : 'flex-1'}`}>
<div className="flex-1 min-w-0">
<div className={`flex flex-col sm:flex-row gap-3 sm:gap-4 sm:items-center ${embedded ? '' : 'flex-1'}`}>
<div className="flex-1 min-w-0 w-full">
<Input
placeholder={placeholder}
value={value}
@@ -33,7 +33,7 @@ export function SearchBar({
icon={<Search className="h-4 w-4 icon-flat" />}
/>
</div>
{actions && <div className="flex gap-2 shrink-0">{actions}</div>}
{actions && <div className="flex flex-wrap gap-2 shrink-0 w-full sm:w-auto">{actions}</div>}
</div>
);
@@ -41,5 +41,5 @@ export function SearchBar({
return field;
}
return <div className="surface-card p-4">{field}</div>;
return <div className="surface-card p-3 sm:p-4">{field}</div>;
}

View File

@@ -13,6 +13,7 @@ import {
CreditCard,
Package,
ListTodo,
X,
} from 'lucide-react';
import type { OrgTypeName } from '@/components/shared/permissions';
import { useAuth } from '@/lib/hooks/useAuth';
@@ -36,7 +37,12 @@ type MenuItem = {
orgTypes: OrgTypeName[];
};
function Sidebar() {
type SidebarProps = {
mobileOpen?: boolean;
onClose?: () => void;
};
function Sidebar({ mobileOpen = false, onClose }: SidebarProps) {
const t = useTranslations('nav');
const tCommon = useTranslations('common');
const pathname = usePathname();
@@ -87,9 +93,23 @@ function Sidebar() {
);
return (
<aside className="w-56 min-w-56 shrink-0 bg-background-secondary/90 border-r border-border text-text-primary flex flex-col">
<div className="h-[71px] px-4 flex items-center">
<h1 className="text-lg font-medium tracking-tight">{tCommon('appName')}</h1>
<aside
className={`fixed inset-y-0 left-0 z-50 w-56 min-w-56 shrink-0 bg-background-secondary/95 border-r border-border text-text-primary flex flex-col backdrop-blur-sm transition-transform duration-200 ease-out lg:relative lg:translate-x-0 lg:z-auto ${
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
}`}
>
<div className="h-[71px] px-4 flex items-center justify-between gap-2">
<h1 className="text-lg font-medium tracking-tight truncate">{tCommon('appName')}</h1>
{onClose ? (
<button
type="button"
className="lg:hidden inline-flex items-center justify-center h-9 w-9 rounded-[var(--radius-md)] border border-border/70 text-text-primary hover:bg-background-card/80 shrink-0"
onClick={onClose}
aria-label={tCommon('closeMenu')}
>
<X className="h-5 w-5 icon-flat" />
</button>
) : null}
</div>
<div className="mx-4 border-b border-border/70" />
@@ -105,6 +125,7 @@ function Sidebar() {
key={item.path}
href={item.path}
prefetch
onClick={onClose}
className={`flex items-center gap-3 px-3 py-2.5 rounded-[var(--radius-sm)] border transition-colors ${
isActive
? 'bg-primary-soft border-primary/60 text-text-primary'

View File

@@ -9,16 +9,18 @@ interface TableProps {
export function Table({ headers, body, footer }: TableProps) {
return (
<div className="surface-card overflow-hidden">
<table className="w-full">
<thead className="bg-background-secondary/70 border-b border-border">
{headers}
</thead>
<tbody className="divide-y divide-border/60">
{body}
</tbody>
</table>
<div className="overflow-x-auto">
<table className="w-full min-w-[36rem] [&_th]:px-3 sm:[&_th]:px-6 [&_td]:px-3 sm:[&_td]:px-6">
<thead className="bg-background-secondary/70 border-b border-border">
{headers}
</thead>
<tbody className="divide-y divide-border/60">
{body}
</tbody>
</table>
</div>
{footer && (
<div className="px-6 py-3 border-t border-border/60 flex justify-between items-center bg-background-secondary/70">
<div className="px-3 sm:px-6 py-3 border-t border-border/60 flex flex-col gap-2 sm:flex-row sm:justify-between sm:items-center bg-background-secondary/70">
{footer}
</div>
)}