Refine admin PWA layout and tamagui usage
This commit is contained in:
@@ -16,9 +16,11 @@ export type NavKey = 'home' | 'tasks' | 'uploads' | 'profile';
|
||||
export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate: (key: NavKey) => void }) {
|
||||
const { t } = useTranslation('mobile');
|
||||
const location = useLocation();
|
||||
const { surface, border, primary, accentSoft, muted, subtle, shadow } = useAdminTheme();
|
||||
const surfaceColor = surface;
|
||||
const navSurface = withAlpha(surfaceColor, 0.92);
|
||||
const { surface, border, primary, accent, muted, subtle, shadow, glassSurfaceStrong, glassBorder, glassShadow } = useAdminTheme();
|
||||
const surfaceColor = glassSurfaceStrong ?? surface;
|
||||
const navSurface = glassSurfaceStrong ?? withAlpha(surfaceColor, 0.92);
|
||||
const navBorder = glassBorder ?? border;
|
||||
const navShadow = glassShadow ?? shadow;
|
||||
const [pressedKey, setPressedKey] = React.useState<NavKey | null>(null);
|
||||
|
||||
const isDeepHome = active === 'home' && location.pathname !== adminPath('/mobile/dashboard');
|
||||
@@ -38,14 +40,14 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
right={0}
|
||||
backgroundColor={navSurface}
|
||||
borderTopWidth={1}
|
||||
borderColor={border}
|
||||
borderColor={navBorder}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$4"
|
||||
zIndex={50}
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.08}
|
||||
shadowRadius={12}
|
||||
shadowOffset={{ width: 0, height: -4 }}
|
||||
shadowColor={navShadow}
|
||||
shadowOpacity={0.12}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: -6 }}
|
||||
// allow for safe-area inset on modern phones
|
||||
style={{
|
||||
paddingBottom: 'max(env(safe-area-inset-bottom, 0px), 8px)',
|
||||
@@ -58,6 +60,8 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
const activeState = item.key === active;
|
||||
const isPressed = pressedKey === item.key;
|
||||
const IconCmp = item.icon;
|
||||
const activeBg = primary;
|
||||
const activeShadow = withAlpha(primary, 0.4);
|
||||
return (
|
||||
<Pressable
|
||||
key={item.key}
|
||||
@@ -78,9 +82,10 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$2"
|
||||
borderRadius={12}
|
||||
backgroundColor={activeState ? accentSoft : 'transparent'}
|
||||
backgroundColor={activeState ? activeBg : 'transparent'}
|
||||
gap="$1"
|
||||
style={{
|
||||
boxShadow: activeState ? `0 10px 22px ${activeShadow}` : undefined,
|
||||
transform: isPressed ? 'scale(0.96)' : 'scale(1)',
|
||||
opacity: isPressed ? 0.9 : 1,
|
||||
transition: 'transform 140ms ease, background-color 140ms ease, opacity 140ms ease',
|
||||
@@ -93,17 +98,17 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
width={28}
|
||||
height={3}
|
||||
borderRadius={999}
|
||||
backgroundColor={primary}
|
||||
backgroundColor={accent}
|
||||
/>
|
||||
) : null}
|
||||
<YStack width={ICON_SIZE} height={ICON_SIZE} alignItems="center" justifyContent="center" shrink={0}>
|
||||
<IconCmp size={ICON_SIZE} color={activeState ? primary : subtle} />
|
||||
<IconCmp size={ICON_SIZE} color={activeState ? 'white' : subtle} />
|
||||
</YStack>
|
||||
<Text
|
||||
fontSize="$xs"
|
||||
fontWeight="700"
|
||||
fontFamily="$body"
|
||||
color={activeState ? primary : muted}
|
||||
color={activeState ? 'white' : muted}
|
||||
textAlign="center"
|
||||
flexShrink={1}
|
||||
>
|
||||
|
||||
@@ -37,13 +37,32 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const { t } = useTranslation('mobile');
|
||||
const { count: notificationCount } = useNotificationsBadge();
|
||||
const online = useOnlineStatus();
|
||||
const { background, surface, border, text, muted, warningBg, warningText, primary, danger, shadow } = useAdminTheme();
|
||||
const {
|
||||
background,
|
||||
surface,
|
||||
border,
|
||||
text,
|
||||
muted,
|
||||
warningBg,
|
||||
warningText,
|
||||
primary,
|
||||
danger,
|
||||
shadow,
|
||||
glassSurfaceStrong,
|
||||
glassBorder,
|
||||
glassShadow,
|
||||
appBackground,
|
||||
} = useAdminTheme();
|
||||
const backgroundColor = background;
|
||||
const surfaceColor = surface;
|
||||
const borderColor = border;
|
||||
const textColor = text;
|
||||
const mutedText = muted;
|
||||
const headerSurface = withAlpha(surfaceColor, 0.94);
|
||||
const headerSurface = glassSurfaceStrong ?? withAlpha(surfaceColor, 0.94);
|
||||
const headerBorder = glassBorder ?? borderColor;
|
||||
const actionSurface = glassSurfaceStrong ?? surfaceColor;
|
||||
const actionBorder = glassBorder ?? borderColor;
|
||||
const actionShadow = glassShadow ?? shadow;
|
||||
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
|
||||
const [loadingEvents, setLoadingEvents] = React.useState(false);
|
||||
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
|
||||
@@ -152,10 +171,17 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
width={34}
|
||||
height={34}
|
||||
borderRadius={12}
|
||||
backgroundColor={surfaceColor}
|
||||
backgroundColor={actionSurface}
|
||||
borderWidth={1}
|
||||
borderColor={actionBorder}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
style={{
|
||||
boxShadow: `0 10px 18px ${actionShadow}`,
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
}}
|
||||
>
|
||||
<Bell size={16} color={textColor} />
|
||||
{notificationCount > 0 ? (
|
||||
@@ -190,6 +216,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
backgroundColor={primary}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ boxShadow: `0 10px 18px ${withAlpha(primary, 0.32)}` }}
|
||||
>
|
||||
<QrCode size={16} color="white" />
|
||||
</XStack>
|
||||
@@ -200,18 +227,23 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
);
|
||||
|
||||
return (
|
||||
<YStack backgroundColor={backgroundColor} minHeight="100vh" alignItems="center">
|
||||
<YStack
|
||||
backgroundColor={backgroundColor}
|
||||
minHeight="100vh"
|
||||
alignItems="center"
|
||||
style={{ background: appBackground }}
|
||||
>
|
||||
<YStack
|
||||
backgroundColor={headerSurface}
|
||||
borderBottomWidth={1}
|
||||
borderColor={borderColor}
|
||||
borderColor={headerBorder}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.06}
|
||||
shadowRadius={10}
|
||||
shadowOffset={{ width: 0, height: 4 }}
|
||||
shadowColor={actionShadow}
|
||||
shadowOpacity={0.08}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
width="100%"
|
||||
maxWidth={800}
|
||||
position="sticky"
|
||||
|
||||
@@ -2,27 +2,32 @@ import React from 'react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { useAdminTheme } from '../theme';
|
||||
import { ADMIN_GRADIENTS, useAdminTheme } from '../theme';
|
||||
import { withAlpha } from './colors';
|
||||
|
||||
export function MobileCard({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof YStack>) {
|
||||
const { surface, border, shadow } = useAdminTheme();
|
||||
const { surface, border, shadow, glassSurface, glassBorder, glassShadow } = useAdminTheme();
|
||||
return (
|
||||
<YStack
|
||||
className={['admin-fade-up', className].filter(Boolean).join(' ')}
|
||||
backgroundColor={surface}
|
||||
borderRadius={18}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.08}
|
||||
shadowRadius={14}
|
||||
backgroundColor={glassSurface ?? surface}
|
||||
borderRadius={20}
|
||||
borderWidth={2}
|
||||
borderColor={glassBorder ?? border}
|
||||
shadowColor={glassShadow ?? shadow}
|
||||
shadowOpacity={0.16}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
padding="$3.5"
|
||||
space="$2"
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
@@ -99,13 +104,18 @@ export function CTAButton({
|
||||
iconLeft?: React.ReactNode;
|
||||
iconRight?: React.ReactNode;
|
||||
}) {
|
||||
const { primary, surface, border, text, danger } = useAdminTheme();
|
||||
const { primary, surface, border, text, danger, glassSurfaceStrong } = useAdminTheme();
|
||||
const isPrimary = tone === 'primary';
|
||||
const isDanger = tone === 'danger';
|
||||
const isDisabled = disabled || loading;
|
||||
const backgroundColor = isDanger ? danger : isPrimary ? primary : surface;
|
||||
const backgroundColor = isDanger ? danger : isPrimary ? primary : glassSurfaceStrong ?? surface;
|
||||
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
||||
const labelColor = isPrimary || isDanger ? 'white' : text;
|
||||
const primaryStyle = isPrimary
|
||||
? {
|
||||
boxShadow: `0 18px 28px ${withAlpha(primary, 0.4)}`,
|
||||
}
|
||||
: undefined;
|
||||
return (
|
||||
<Pressable
|
||||
onPress={isDisabled ? undefined : onPress}
|
||||
@@ -119,13 +129,14 @@ export function CTAButton({
|
||||
>
|
||||
<XStack
|
||||
height={52}
|
||||
borderRadius={16}
|
||||
borderRadius={18}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={backgroundColor}
|
||||
borderWidth={isPrimary || isDanger ? 0 : 1}
|
||||
borderWidth={isPrimary || isDanger ? 0 : 2}
|
||||
borderColor={borderColor}
|
||||
space="$2"
|
||||
style={primaryStyle}
|
||||
>
|
||||
{iconLeft}
|
||||
<Text fontSize="$sm" fontWeight="800" color={labelColor}>
|
||||
@@ -183,6 +194,7 @@ export function ActionTile({
|
||||
color,
|
||||
onPress,
|
||||
disabled = false,
|
||||
variant = 'grid',
|
||||
delayMs = 0,
|
||||
}: {
|
||||
icon: React.ComponentType<{ size?: number; color?: string }>;
|
||||
@@ -190,49 +202,56 @@ export function ActionTile({
|
||||
color: string;
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: 'grid' | 'cluster';
|
||||
delayMs?: number;
|
||||
}) {
|
||||
const { textStrong } = useAdminTheme();
|
||||
const backgroundColor = `${color}18`;
|
||||
const borderColor = `${color}40`;
|
||||
const shadowColor = `${color}2b`;
|
||||
const iconShadow = `${color}55`;
|
||||
const { textStrong, glassSurface } = useAdminTheme();
|
||||
const isCluster = variant === 'cluster';
|
||||
const backgroundColor = withAlpha(color, 0.12);
|
||||
const borderColor = withAlpha(color, 0.4);
|
||||
const shadowColor = withAlpha(color, 0.35);
|
||||
const iconShadow = withAlpha(color, 0.5);
|
||||
const tileStyle = {
|
||||
...(delayMs ? { animationDelay: `${delayMs}ms` } : {}),
|
||||
backgroundImage: `linear-gradient(135deg, ${backgroundColor}, ${color}0f)`,
|
||||
boxShadow: `0 10px 24px ${shadowColor}`,
|
||||
backgroundImage: `linear-gradient(135deg, ${backgroundColor}, ${withAlpha(color, 0.08)})`,
|
||||
boxShadow: isCluster ? `0 12px 18px ${shadowColor}` : `0 20px 30px ${shadowColor}`,
|
||||
};
|
||||
return (
|
||||
<Pressable
|
||||
onPress={disabled ? undefined : onPress}
|
||||
style={{ width: '48%', marginBottom: 12, opacity: disabled ? 0.5 : 1 }}
|
||||
style={{
|
||||
width: isCluster ? '100%' : '48%',
|
||||
flex: isCluster ? 1 : undefined,
|
||||
marginBottom: isCluster ? 0 : 12,
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<YStack
|
||||
className="admin-fade-up"
|
||||
style={tileStyle}
|
||||
borderRadius={16}
|
||||
borderRadius={isCluster ? 14 : 16}
|
||||
padding="$3"
|
||||
space="$2.5"
|
||||
backgroundColor={backgroundColor}
|
||||
borderWidth={1}
|
||||
backgroundColor={glassSurface ?? backgroundColor}
|
||||
borderWidth={2}
|
||||
borderColor={borderColor}
|
||||
minHeight={110}
|
||||
minHeight={120}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<XStack
|
||||
width={36}
|
||||
height={36}
|
||||
borderRadius={12}
|
||||
width={44}
|
||||
height={44}
|
||||
borderRadius={14}
|
||||
backgroundColor={color}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ boxShadow: `0 6px 14px ${iconShadow}` }}
|
||||
>
|
||||
<IconCmp size={16} color="white" />
|
||||
<IconCmp size={18} color="white" />
|
||||
</XStack>
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong} textAlign="center">
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong} textAlign="center">
|
||||
{label}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
@@ -17,11 +17,12 @@ type MobileScaffoldProps = {
|
||||
|
||||
export function MobileScaffold({ title, onBack, rightSlot, children, footer }: MobileScaffoldProps) {
|
||||
const { t } = useTranslation('mobile');
|
||||
const { background, surface, border, text, primary } = useAdminTheme();
|
||||
const headerSurface = withAlpha(surface, 0.94);
|
||||
const { background, surface, border, text, primary, glassSurfaceStrong, glassBorder, appBackground } = useAdminTheme();
|
||||
const headerSurface = glassSurfaceStrong ?? withAlpha(surface, 0.94);
|
||||
const headerBorder = glassBorder ?? border;
|
||||
|
||||
return (
|
||||
<YStack backgroundColor={background} minHeight="100vh">
|
||||
<YStack backgroundColor={background} minHeight="100vh" style={{ background: appBackground }}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
@@ -30,7 +31,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
|
||||
paddingBottom="$3"
|
||||
backgroundColor={headerSurface}
|
||||
borderBottomWidth={1}
|
||||
borderColor={border}
|
||||
borderColor={headerBorder}
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={60}
|
||||
|
||||
@@ -92,6 +92,11 @@ vi.mock('../../theme', () => ({
|
||||
primary: '#FF5A5F',
|
||||
danger: '#b91c1c',
|
||||
shadow: 'rgba(0,0,0,0.12)',
|
||||
glassSurface: 'rgba(255,255,255,0.8)',
|
||||
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
|
||||
glassBorder: 'rgba(229,231,235,0.7)',
|
||||
glassShadow: 'rgba(15,23,42,0.14)',
|
||||
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user