app now responsive for mobile and small devices.
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
frontend/src/components/ui/shared/MobileDetailBackButton.tsx
Normal file
19
frontend/src/components/ui/shared/MobileDetailBackButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
61
frontend/src/components/ui/shared/ResponsiveDialog.tsx
Normal file
61
frontend/src/components/ui/shared/ResponsiveDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user