mcpField
M
mobile-ux-optimizer

mobile-ux-optimizer

github.com/curiositech设计与 UI257503 次安装2 天前更新
代码仓库

关于 mobile-ux-optimizer

mobile-ux-optimizer 是面向移动端网页设计与开发的技能,提供触控友好、响应式和高性能体验的实现指导。内容覆盖视口与安全区域、导航、手势和设备测试。

主要功能

  1. 01移动优先响应式布局
  2. 02视口与安全区域处理
  3. 03触控目标和移动导航
  4. 04滑动与下拉刷新手势
  5. 05移动性能优化

适用场景

  1. 01修复地址栏导致的视口裁切
  2. 02为刘海和主屏指示条留出空间
  3. 03设计移动导航和触控交互
  4. 04改善移动端加载与滚动体验

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill mobile-ux-optimizer

安装后在 Claude Code 中输入/mobile-ux-optimizer确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.claude-plugin/
plugin.json
references/
gestures.md
navigation.md

详细介绍

移动优先的 UX 优化

构建经过触控优化、性能出色的移动端体验,正确处理视口并采用响应式模式。

适用场景

✅ 以下场景请使用此技能:

  • 视口问题(100vh 问题、安全区域、刘海屏)
  • 触控目标的尺寸和间距
  • 移动端导航模式(底部导航、抽屉、汉堡菜单)
  • 滑动手势和下拉刷新
  • 响应式断点策略
  • 移动端性能优化

❌ 以下场景请勿使用:

  • 原生应用开发 → 使用 react-native 或 swift-executor 技能
  • 仅限桌面端的功能 → 无需使用技能,采用标准模式即可
  • 通用 CSS/Tailwind 问题 → 使用 Tailwind 文档或 web-design-expert
  • PWA 安装/Service Worker → 使用 pwa-expert 技能

核心原则

移动优先意味着逐步增强,而非逐步简化
/* ❌ ANTI-PATTERN: Desktop-first (scale down) */
.card { width: 400px; }
@media (max-width: 768px) { .card { width: 100%; } }

/* ✅ CORRECT: Mobile-first (scale up) */
.card { width: 100%; }
@media (min-width: 768px) { .card { width: 400px; } }
44px 规则

Apple 的人机界面指南规定,最小触控目标为 44×44 点。Google Material 建议为 48×48dp。

// Touch-friendly button
<button className="min-h-[44px] min-w-[44px] px-4 py-3">
  Tap me
</button>

// Touch-friendly link with adequate padding
<a href="/page" className="inline-block py-3 px-4">
  Link text
</a>

视口处理

dvh 方案

移动浏览器具有动态工具栏。100vh 会包含 URL 栏,导致内容被截断。

/* ❌ ANTI-PATTERN: Content hidden behind browser UI */
.full-screen { height: 100vh; }

/* ✅ CORRECT: Responds to browser chrome */
.full-screen { height: 100dvh; }

/* Fallback for older browsers */
.full-screen {
  height: 100vh;
  height: 100dvh;
}
安全区域内边距(刘海屏和主屏幕指示器)
/* Handle iPhone notch and home indicator */
.bottom-nav {
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.header {
  padding-top: env(safe-area-inset-top, 0);
}

/* Full safe area padding */
.safe-container {
  padding: env(safe-area-inset-top)
           env(safe-area-inset-right)
           env(safe-area-inset-bottom)
           env(safe-area-inset-left);
}

必需的 meta 标签:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Tailwind 安全区域类
// Custom Tailwind utilities (add to globals.css)
@layer utilities {
  .pb-safe { padding-bottom: env(safe-area-inset-bottom); }
  .pt-safe { padding-top: env(safe-area-inset-top); }
  .h-screen-safe { height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
}

// Usage
<nav className="fixed bottom-0 pb-safe bg-leather-900">
  <BottomNav />
</nav>

移动端导航模式

底部导航(推荐用于移动端)
// components/BottomNav.tsx
'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';

const navItems = [
  { href: '/', icon: HomeIcon, label: 'Home' },
  { href: '/meetings', icon: CalendarIcon, label: 'Meetings' },
  { href: '/tools', icon: ToolsIcon, label: 'Tools' },
  { href: '/my', icon: UserIcon, label: 'My Recovery' },
];

export function BottomNav() {
  const pathname = usePathname();

  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-leather-900 border-t border-leather-700 pb-safe">
      <div className="flex justify-around">
        {navItems.map(({ href, icon: Icon, label }) => {
          const isActive = pathname === href || pathname.startsWith(`${href}/`);
          return (
            <Link
              key={href}
              href={href}
              className={`
                flex flex-col items-center py-2 px-3 min-h-[56px] min-w-[64px]
                ${isActive ? 'text-ember-400' : 'text-leather-400'}
              `}
            >
              <Icon className="w-6 h-6" />
              <span className="text-xs mt-1">{label}</span>
            </Link>
          );
        })}
      </div>
    </nav>
  );
}
滑出式抽屉(侧边菜单)
'use client';

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';

interface DrawerProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

export function Drawer({ isOpen, onClose, children }: DrawerProps) {
  // Prevent body scroll when open
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  // Close on escape
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="fixed inset-0 z-50">
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/60 backdrop-blur-sm"
        onClick={onClose}
        aria-hidden="true"
      />

      {/* Drawer */}
      <div
        className="absolute left-0 top-0 h-full w-[280px] max-w-[80vw]
                   bg-leather-900 shadow-xl transform transition-transform
                   animate-slide-in-left"
        role="dialog"
        aria-modal="true"
      >
        <div className="h-full overflow-y-auto pt-safe pb-safe">
          {children}
        </div>
      </div>
    </div>,
    document.body
  );
}

触控手势

完整实现请见 references/gestures.md

Hook用途
useSwipe()检测带方向的滑动手势,可配置阈值
usePullToRefresh()带视觉反馈和阻尼效果的下拉刷新

快速用法:

// Swipe to dismiss
const { handleTouchStart, handleTouchEnd } = useSwipe({
  onSwipeLeft: () => dismiss(),
  threshold: 50,
});

// Pull to refresh
const { containerRef, pullDistance, isRefreshing, handlers } = 
  usePullToRefresh(async () => await refetchData());

移动端性能

图像优化
import Image from 'next/image';

// Responsive images with proper sizing
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
  priority // For above-the-fold images
  className="object-cover"
/>

// Lazy load below-fold images
<Image
  src="/feature.jpg"
  alt="Feature"
  width={400}
  height={300}
  loading="lazy"
/>
减小构建包体积
// Dynamic imports for heavy components
const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Skip server render for client-only
});

// Lazy load below-fold sections
const Comments = dynamic(() => import('@/components/Comments'));
骨架屏(而非加载旋转图标)
// Skeleton that matches final content layout
function MeetingCardSkeleton() {
  return (
    <div className="p-4 bg-leather-800 rounded-lg animate-pulse">
      <div className="h-4 bg-leather-700 rounded w-3/4 mb-2" />
      <div className="h-3 bg-leather-700 rounded w-1/2 mb-4" />
      <div className="flex gap-2">
        <div className="h-6 w-16 bg-leather-700 rounded" />
        <div className="h-6 w-16 bg-leather-700 rounded" />
      </div>
    </div>
  );
}

// Usage
{isLoading ? (
  <div className="space-y-4">
    {[...Array(5)].map((_, i) => <MeetingCardSkeleton key={i} />)}
  </div>
) : (
  meetings.map(m => <MeetingCard key={m.id} meeting={m} />)
)}

响应式模式

Tailwind 断点策略
sm: 640px   - Large phones (landscape)
md: 768px   - Tablets
lg: 1024px  - Small laptops
xl: 1280px  - Desktops
2xl: 1536px - Large screens
// Mobile: stack, Tablet+: side-by-side
<div className="flex flex-col md:flex-row gap-4">
  <aside className="w-full md:w-64">Sidebar</aside>
  <main className="flex-1">Content</main>
</div>

// Mobile: bottom nav, Desktop: sidebar
<nav className="md:hidden fixed bottom-0 left-0 right-0">
  <BottomNav />
</nav>
<aside className="hidden md:block w-64">
  <SidebarNav />
</aside>
容器查询(纯 CSS 响应式组件)
/* Component responds to its container, not viewport */
@container (min-width: 400px) {
  .card { flex-direction: row; }
}
<div className="@container">
  <div className="flex flex-col @md:flex-row">
    {/* Responds to parent container width */}
  </div>
</div>

在真实设备上测试

Chrome DevTools 移动端模拟
  1. 打开 DevTools(F12)
  2. 切换设备工具栏(Ctrl+Shift+M)
  3. 选择设备或设置自定义尺寸
  4. 限制网络/CPU 速度,以模拟真实性能
必测场景
  • 内容不会被刘海屏/主屏幕指示器截断
  • 触控目标至少为 44×44px
  • 滚动流畅(无卡顿)
  • 底部导航不会遮挡内容
  • 虚拟键盘显示时表单仍可正常使用
  • 横屏方向下正常显示
  • 下拉刷新不会与滚动操作冲突
BrowserStack/真实设备测试
# Expose local dev server to internet
npx localtunnel --port 3000
# or
ngrok http 3000

快速参考

问题解决方案
底部内容被截断使用 100dvh 替代 100vh
刘海屏遮挡内容添加 pt-safe / pb-safe
触控目标太小最小尺寸为 44×44px
滚动被锁定检查 body 上的 overflow: hidden
键盘遮挡输入框使用 visualViewport API
滚动卡顿使用 will-change: transform
双击缩放添加 touch-action: manipulation

参考资料

详细指南请见 /references/:

  • keyboard-handling.md - 虚拟键盘和表单 UX
  • animations.md - 适合触控操作的动画
  • accessibility.md - 移动端无障碍要求

评论

支持 Markdown