Claude Skill

lov-mobile-adapt

Adapt an existing web project for mobile devices: fix overflow, add responsive layouts, convert to multi-level page navigation with back support, handle notch/Dynamic Island safe areas, fix 100vh browser chrome issues, and optimize touch targets. Trigger when user says "mobile ad

LLM Mart · 0 points · 9 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_mobile-adapt-0b16007.zip · 13 KB
Part of lovstudio/skills — 83 skills

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/mobile-adapt
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

移动适配师 · Mobile Fit

Version

Scan and fix mobile adaptation issues in web projects: viewport, overflow, safe area, responsive layouts, 100vh pitfalls, touch targets, and multi-level page navigation.

Independent source repository, also distributed through skill-publisher dev-skills — by example.com

Install

npx skills add skill-publisher/mobile-adapt-skill --all -g

The aggregate bundle remains available:

npx skills add skill-publisher/dev-skills --all -g

Or through Claude Code plugin marketplace:

/plugin marketplace add skill-publisher/dev-skills
/plugin install dev-tools@lov-dev

Requires: Python 3.8+ (no external dependencies)

What It Does

  1. Scans your project for mobile issues (overflow, viewport, safe area, touch targets)
  2. Asks which categories to fix
  3. Applies fixes following modern best practices (dvh, env(), container queries)
  4. Restructures navigation into mobile page stack with back support (optional)
  5. Verifies fixes by re-scanning

Scanner

python3 ~/.claude/skills/lov-mobile-adapt/scripts/scan_mobile_issues.py /path/to/project
Option Default Description
project (required) Path to project root
--format text Output: text or json

Checks: viewport meta, overflow risks, 100vh usage, safe-area-inset, touch target sizes, responsive breakpoints, Tailwind-specific issues, text overflow.

Covered Issues

Category What gets fixed
Viewport Missing/incorrect meta viewport, viewport-fit=cover
Overflow Fixed widths, horizontal scroll, image overflow, text overflow
Viewport units 100vh → 100dvh with fallback
Safe area env(safe-area-inset-*) on fixed/sticky elements
Touch targets Interactive elements below 44px minimum
Responsive Missing breakpoints, mobile-first media queries
Navigation Sidebar → mobile page stack with back button
Browser chrome theme-color, status bar, input zoom, pull-to-refresh

License

MIT

Skill manifest

移动适配师 · Mobile Fit

Scan and fix mobile adaptation issues in an existing web project: viewport configuration, overflow prevention, safe area handling, responsive breakpoints, 100vh pitfalls, touch targets, and multi-level page navigation.

When to Use

  • Converting a desktop-first site to work well on mobile
  • User reports overflow, cut-off content, or notch overlap on phones
  • Adding mobile navigation (back button, page stack) to a sidebar-based layout
  • Fixing 100vh issues on iOS/Android browsers
  • General "make it mobile friendly" requests

Workflow (MANDATORY)

You MUST follow these steps in order.

Resolve SKILL_DIR from the installed skill context before running the helper. For manual execution, set it to the directory containing this SKILL.md.

Step 1: Scan the Project

Run the scanner to identify issues:

python3 "$SKILL_DIR/scripts/scan_mobile_issues.py" <project-path>

For JSON output (easier to process programmatically):

python3 "$SKILL_DIR/scripts/scan_mobile_issues.py" <project-path> --format json

Review the output. The scanner checks:

  • viewport meta tag presence and viewport-fit=cover
  • CSS overflow risks (fixed widths, min-width)
  • 100vh usage (should be 100dvh)
  • safe-area-inset usage on fixed/sticky elements
  • Touch target sizes (< 44px)
  • Responsive breakpoint coverage
  • Tailwind-specific issues (h-screen → h-dvh)
  • Text overflow without ellipsis handling

Step 2: Ask the User

IMPORTANT: Use AskUserQuestion to collect scope BEFORE making changes.

Present the scan results summary, then ask:

Question: "扫描发现 X 个问题。要修哪些类别?"
Options:
  1. 全部修复 (Recommended) — fix all categories found
  2. 只修布局和溢出 — overflow + responsive only
  3. 只修导航 — convert to mobile stack navigation
  4. 让我选具体类别 — pick specific categories

If the project has sidebar/tab navigation on desktop, also ask:

Question: "桌面端的侧边栏/tab 导航要改成移动端多级页面吗?"
Options:
  1. 是,改成 push/pop 页面栈 (Recommended)
  2. 改成底部 tab bar
  3. 不改导航结构

Step 3: Fix Issues

Apply fixes in this priority order:

  1. Viewport meta — add or fix <meta name="viewport"> with viewport-fit=cover
  2. Global overflow guard — add overflow-x: hidden to html/body
  3. 100vh → 100dvh — replace all 100vh usages, add fallback
  4. Safe area padding — add env(safe-area-inset-*) to fixed/sticky elements
  5. Image/media overflow — add max-width: 100%; height: auto
  6. Text overflow — add ellipsis/line-clamp where white-space: nowrap exists
  7. Touch targets — increase size of interactive elements below 44px
  8. Responsive breakpoints — add mobile-first media queries or Tailwind responsive
  9. Navigation restructure — convert to mobile page stack if requested

For each fix, refer to references/mobile-patterns.md for the correct pattern.

Step 4: Navigation Restructure (if applicable)

When converting sidebar/panel navigation to mobile stack:

  1. Identify the navigation structure (sidebar, tabs, nested panels)
  2. Create a responsive layout wrapper that switches between desktop and mobile
  3. On mobile: render as a full-screen page stack with back button
  4. Use CSS slide transitions for page push/pop
  5. Preserve desktop layout unchanged at md: breakpoint and above

See references/mobile-patterns.md → "Multi-Level Page Navigation" for implementation patterns per framework.

Step 5: Verify

After all fixes:

  1. Re-run the scanner — confirm issues resolved
  2. Check the site in a mobile viewport (375px width)
  3. Verify:
    • No horizontal scroll
    • Content not cut off by notch or home indicator
    • All interactive elements are tappable (44px+)
    • Navigation back button works
    • Full-height layouts don't overflow behind browser chrome

CLI Reference

Argument Default Description
project (required) Path to web project root
--format text Output format: text or json

Key Patterns (quick reference)

Problem Fix
Missing viewport meta <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
100vh overflow height: 100dvh (with 100vh fallback)
Notch overlap padding: env(safe-area-inset-top) on fixed elements
Horizontal overflow overflow-x: hidden on body + max-width: 100% on media
iOS input zoom font-size: 16px on inputs
Small touch targets min-height: 44px; min-width: 44px
Pull-to-refresh conflict overscroll-behavior-y: contain

For detailed patterns see references/mobile-patterns.md.

Runtime context (shared)

运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。

  • 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
  • required: true 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
  • 报错提供可复制的 context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。

通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:

  1. 先判断意见是 task-specific(仅本次)还是 reusable(可跨任务复用)。
  2. task-specific 只修改当前任务,不改 Skill。
  3. reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
  4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
  5. reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
  • references
    • mobile-patterns.md 6.6 KB
      # Mobile Adaptation Patterns Reference
      
      ## 1. Viewport & Safe Area
      
      ### viewport meta (required)
      ```html
      <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
      ```
      - `viewport-fit=cover` is required for notch/Dynamic Island devices
      - Without it, `env(safe-area-inset-*)` returns 0
      
      ### Safe area padding
      ```css
      /* Bottom nav / fixed footer */
      .bottom-bar {
        padding-bottom: env(safe-area-inset-bottom, 0px);
      }
      
      /* Top header on standalone PWA */
      .top-bar {
        padding-top: env(safe-area-inset-top, 0px);
      }
      
      /* Full-bleed layout */
      body {
        padding: env(safe-area-inset-top) env(safe-area-inset-right)
                 env(safe-area-inset-bottom) env(safe-area-inset-left);
      }
      ```
      
      ### Tailwind safe area plugin
      ```js
      // tailwind.config.js
      module.exports = {
        theme: {
          extend: {
            padding: {
              'safe-top': 'env(safe-area-inset-top)',
              'safe-bottom': 'env(safe-area-inset-bottom)',
              'safe-left': 'env(safe-area-inset-left)',
              'safe-right': 'env(safe-area-inset-right)',
            },
          },
        },
      };
      ```
      
      ## 2. Viewport Height (100vh problem)
      
      Mobile browsers have a dynamic address bar. `100vh` = viewport WITH address bar visible = content gets cut off when bar hides.
      
      ### Modern solution: dynamic viewport units
      ```css
      .full-height {
        height: 100dvh; /* dynamic: adapts as browser chrome shows/hides */
      }
      
      /* Fallback for older browsers */
      .full-height {
        height: 100vh;
        height: 100dvh;
      }
      ```
      
      | Unit | Behavior |
      |------|----------|
      | `100vh` | Legacy, equals large viewport, causes overflow |
      | `100svh` | Small viewport (chrome visible) — stable but short |
      | `100lvh` | Large viewport (chrome hidden) — matches old 100vh |
      | `100dvh` | Dynamic — resizes with browser chrome |
      
      ### Tailwind v3.4+
      ```html
      <div class="h-dvh">...</div>
      <!-- or h-svh, h-lvh -->
      ```
      
      ## 3. Overflow Prevention
      
      ### Global overflow guard
      ```css
      html, body {
        overflow-x: hidden;
        -webkit-overflow-scrolling: touch;
      }
      ```
      
      ### Text overflow
      ```css
      .text-truncate {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      
      .text-clamp-2 {
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
      }
      ```
      
      ### Table / wide content
      ```css
      .table-wrapper {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
      }
      
      table {
        min-width: 100%;
      }
      ```
      
      ### Image overflow
      ```css
      img, video, iframe {
        max-width: 100%;
        height: auto;
      }
      ```
      
      ## 4. Touch Targets
      
      Apple HIG: minimum 44x44pt
      Material Design: minimum 48x48dp
      
      ```css
      .touch-target {
        min-height: 44px;
        min-width: 44px;
        /* Or use padding to expand hit area */
        padding: 12px;
      }
      
      /* Invisible hit area expansion */
      .small-icon-btn {
        position: relative;
      }
      .small-icon-btn::after {
        content: '';
        position: absolute;
        inset: -8px; /* expand by 8px in all directions */
      }
      ```
      
      ## 5. Multi-Level Page Navigation (Mobile Stack Pattern)
      
      ### Pattern: slide-in page stack
      Replace sidebar/tab navigation with a stack-based push/pop model on mobile.
      
      ```
      Desktop:                    Mobile:
      ┌──────┬──────────┐        ┌──────────┐
      │ Side │ Content  │        │ List     │ ← Level 1
      │ bar  │          │   →    │          │
      │      │          │        └──────────┘
      └──────┴──────────┘        ┌──────────┐
                                 │ ← Detail │ ← Level 2 (slides in)
                                 │          │
                                 └──────────┘
      ```
      
      ### React implementation pattern
      ```tsx
      // Use a layout with conditional rendering based on breakpoint
      function ResponsiveLayout({ children }) {
        const isMobile = useMediaQuery('(max-width: 768px)');
      
        if (isMobile) {
          return <MobileStack>{children}</MobileStack>;
        }
        return <DesktopSidebar>{children}</DesktopSidebar>;
      }
      
      // Mobile stack with back button
      function MobileStack() {
        const [stack, setStack] = useState([{ id: 'list', component: ListView }]);
      
        const push = (page) => setStack(prev => [...prev, page]);
        const pop = () => setStack(prev => prev.slice(0, -1));
      
        const current = stack[stack.length - 1];
      
        return (
          <div className="relative h-dvh overflow-hidden">
            <current.component onNavigate={push} onBack={pop} />
          </div>
        );
      }
      ```
      
      ### Next.js App Router pattern
      Use intercepting routes + parallel routes for mobile stack:
      ```
      app/
        @sidebar/        ← parallel route (desktop only)
        (mobile)/        ← route group
          layout.tsx     ← mobile layout with back button
          [id]/page.tsx  ← detail page
        layout.tsx       ← responsive switch
      ```
      
      ### CSS slide transition
      ```css
      .page-enter {
        transform: translateX(100%);
      }
      .page-enter-active {
        transform: translateX(0);
        transition: transform 300ms ease-out;
      }
      .page-exit-active {
        transform: translateX(-30%);
        transition: transform 300ms ease-out;
      }
      ```
      
      ## 6. Browser Chrome Adaptation
      
      ### Hide Safari address bar color (theme-color)
      ```html
      <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
      <meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)">
      ```
      
      ### Status bar style (PWA standalone)
      ```html
      <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
      ```
      
      ### Prevent pull-to-refresh interference
      ```css
      body {
        overscroll-behavior-y: contain;
      }
      ```
      
      ### Prevent zoom on input focus (iOS)
      ```css
      input, select, textarea {
        font-size: 16px; /* iOS won't zoom if font-size >= 16px */
      }
      ```
      
      ## 7. Responsive Layout Patterns
      
      ### Container queries (modern)
      ```css
      .card-container {
        container-type: inline-size;
      }
      
      @container (max-width: 400px) {
        .card { flex-direction: column; }
      }
      ```
      
      ### Standard breakpoints
      ```css
      /* Mobile first */
      .grid {
        display: grid;
        grid-template-columns: 1fr;
        gap: 1rem;
      }
      
      @media (min-width: 640px) {  /* sm */
        .grid { grid-template-columns: repeat(2, 1fr); }
      }
      
      @media (min-width: 1024px) { /* lg */
        .grid { grid-template-columns: repeat(3, 1fr); }
      }
      ```
      
      ### Tailwind responsive
      ```html
      <div class="flex flex-col md:flex-row">
        <aside class="hidden md:block w-64">Sidebar</aside>
        <main class="flex-1">Content</main>
      </div>
      ```
      
      ## 8. Common Framework-Specific Fixes
      
      ### Next.js
      - Use `next/image` with `sizes` prop for responsive images
      - Add viewport meta in `app/layout.tsx` via `metadata.viewport`
      
      ### Tailwind CSS
      - Use `sm:` prefix for mobile-first responsive
      - `h-dvh` replaces `h-screen`
      - `overflow-x-auto` on scroll containers
      - `touch-manipulation` for faster tap response
      
      ### Vue / Nuxt
      - Use `useMediaQuery` from VueUse
      - `<ClientOnly>` for breakpoint-dependent rendering
      
  • scripts
    • scan_mobile_issues.py 14.1 KB
      #!/usr/bin/env python3
      """Scan a web project for common mobile-adaptation issues.
      
      Checks:
        - viewport meta tag presence and correctness
        - CSS overflow issues (fixed widths, horizontal overflow risks)
        - safe-area-inset usage
        - touch target sizes
        - responsive breakpoint coverage
        - 100vh pitfalls (mobile browser chrome)
      
      Usage:
        python3 scan_mobile_issues.py /path/to/project [--format json|text]
      """
      
      import argparse
      import json
      import os
      import re
      import sys
      from dataclasses import dataclass, field, asdict
      from pathlib import Path
      from typing import Optional
      
      HTML_EXTS = {".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".astro"}
      CSS_EXTS = {".css", ".scss", ".sass", ".less"}
      CODE_EXTS = HTML_EXTS | CSS_EXTS | {".js", ".ts", ".jsx", ".tsx", ".vue", ".svelte", ".astro"}
      SKIP_DIRS = {
          "node_modules", ".git", ".next", "dist", "build", ".output",
          ".nuxt", ".svelte-kit", "__pycache__", ".turbo", ".vercel",
      }
      MAX_FILE_SIZE = 512 * 1024  # 512KB
      
      
      @dataclass
      class Issue:
          severity: str  # "error" | "warning" | "info"
          category: str
          file: str
          line: Optional[int]
          message: str
          fix_hint: str
      
      
      @dataclass
      class ScanResult:
          project_path: str
          files_scanned: int = 0
          issues: list = field(default_factory=list)
          summary: dict = field(default_factory=dict)
      
      
      def collect_files(root: Path) -> list[Path]:
          files = []
          for dirpath, dirnames, filenames in os.walk(root):
              dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
              for f in filenames:
                  fp = Path(dirpath) / f
                  if fp.suffix in CODE_EXTS and fp.stat().st_size < MAX_FILE_SIZE:
                      files.append(fp)
          return files
      
      
      def check_viewport_meta(content: str, filepath: str, issues: list):
          if filepath.endswith((".html", ".htm")):
              if "viewport" not in content.lower():
                  issues.append(Issue(
                      severity="error",
                      category="viewport",
                      file=filepath,
                      line=None,
                      message="Missing <meta name=\"viewport\"> tag",
                      fix_hint='Add: <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">',
                  ))
              elif "viewport-fit=cover" not in content.lower():
                  for i, line in enumerate(content.splitlines(), 1):
                      if "viewport" in line.lower() and "meta" in line.lower():
                          issues.append(Issue(
                              severity="warning",
                              category="viewport",
                              file=filepath,
                              line=i,
                              message="viewport meta missing viewport-fit=cover (needed for notch/island devices)",
                              fix_hint='Add viewport-fit=cover to the viewport meta content attribute',
                          ))
                          break
      
      
      def check_overflow_risks(content: str, filepath: str, issues: list):
          for i, line in enumerate(content.splitlines(), 1):
              stripped = line.strip()
              if stripped.startswith("//") or stripped.startswith("/*"):
                  continue
      
              # Fixed pixel widths on containers
              match = re.search(r'width\s*:\s*(\d{4,})px', line)
              if match:
                  px = match.group(1)
                  issues.append(Issue(
                      severity="warning",
                      category="overflow",
                      file=filepath,
                      line=i,
                      message=f"Fixed width {px}px may cause horizontal overflow on mobile",
                      fix_hint="Use max-width, percentage, or clamp() instead of fixed px width",
                  ))
      
              # overflow-x not handled on body/html
              if re.search(r'(html|body)\s*\{', line) or re.search(r'@apply', line):
                  pass  # skip rule-level checks here
      
              # Horizontal scroll risk: min-width large values
              match = re.search(r'min-width\s*:\s*(\d+)px', line)
              if match and int(match.group(1)) > 768:
                  issues.append(Issue(
                      severity="warning",
                      category="overflow",
                      file=filepath,
                      line=i,
                      message=f"min-width: {match.group(1)}px forces minimum width larger than most phones",
                      fix_hint="Consider using a responsive approach or media query instead",
                  ))
      
      
      def check_100vh_pitfall(content: str, filepath: str, issues: list):
          for i, line in enumerate(content.splitlines(), 1):
              if re.search(r'height\s*:\s*100vh\b', line) and "dvh" not in line:
                  issues.append(Issue(
                      severity="warning",
                      category="viewport-units",
                      file=filepath,
                      line=i,
                      message="100vh doesn't account for mobile browser chrome (address bar)",
                      fix_hint="Use 100dvh (dynamic viewport height) or min-height: 100svh with fallback",
                  ))
      
              if re.search(r'height\s*:\s*100svh\b', line):
                  issues.append(Issue(
                      severity="info",
                      category="viewport-units",
                      file=filepath,
                      line=i,
                      message="100svh (small viewport) is safe but may leave gap when browser chrome hides",
                      fix_hint="Consider 100dvh for dynamic sizing, or use 100svh if fixed size is intended",
                  ))
      
      
      def check_safe_area(content: str, filepath: str, issues: list, all_contents: str):
          is_css = filepath.endswith(tuple(CSS_EXTS))
          if not is_css:
              return
      
          has_safe_area = "safe-area-inset" in all_contents or "env(safe-area" in all_contents
      
          if "position: fixed" in content or "position:fixed" in content:
              for i, line in enumerate(content.splitlines(), 1):
                  if re.search(r'position\s*:\s*fixed', line):
                      if "safe-area" not in content[max(0, content.rfind("{", 0, content.find(line))):content.find("}", content.find(line))]:
                          issues.append(Issue(
                              severity="warning",
                              category="safe-area",
                              file=filepath,
                              line=i,
                              message="Fixed-position element may overlap notch/home indicator without safe-area padding",
                              fix_hint="Add padding-bottom: env(safe-area-inset-bottom) or padding-top: env(safe-area-inset-top)",
                          ))
      
          if not has_safe_area:
              issues.append(Issue(
                  severity="info",
                  category="safe-area",
                  file=filepath,
                  line=None,
                  message="No safe-area-inset usage found in project CSS",
                  fix_hint="Add env(safe-area-inset-*) padding to fixed/sticky elements for notch devices",
              ))
      
      
      def check_touch_targets(content: str, filepath: str, issues: list):
          for i, line in enumerate(content.splitlines(), 1):
              # Very small explicit heights/widths on interactive elements
              match = re.search(r'(?:height|width)\s*:\s*(\d+)px', line)
              if match:
                  px = int(match.group(1))
                  if 0 < px < 44:
                      context_start = max(0, i - 10)
                      context_lines = content.splitlines()[context_start:i]
                      context_text = "\n".join(context_lines).lower()
                      if any(kw in context_text for kw in ["button", "btn", "link", "a ", "input", "select", "tap", "click"]):
                          issues.append(Issue(
                              severity="warning",
                              category="touch-target",
                              file=filepath,
                              line=i,
                              message=f"Interactive element may be {px}px — below 44px minimum touch target",
                              fix_hint="Apple HIG recommends 44x44px minimum; Material Design recommends 48x48dp",
                          ))
      
      
      def check_responsive_breakpoints(content: str, filepath: str, issues: list):
          if not filepath.endswith(tuple(CSS_EXTS)):
              return
      
          breakpoints = re.findall(r'@media[^{]*max-width\s*:\s*(\d+)', content)
          breakpoints += re.findall(r'@media[^{]*min-width\s*:\s*(\d+)', content)
      
          if not breakpoints and len(content) > 500:
              issues.append(Issue(
                  severity="info",
                  category="responsive",
                  file=filepath,
                  line=None,
                  message="No media query breakpoints found in this stylesheet",
                  fix_hint="Consider adding breakpoints for mobile (640px), tablet (768px), desktop (1024px)",
              ))
      
      
      def check_text_overflow(content: str, filepath: str, issues: list):
          for i, line in enumerate(content.splitlines(), 1):
              if re.search(r'white-space\s*:\s*nowrap', line):
                  block_start = max(0, content.rfind("{", 0, content.find(line)))
                  block_end = content.find("}", content.find(line))
                  block = content[block_start:block_end] if block_end > block_start else ""
                  if "overflow" not in block and "text-overflow" not in block:
                      issues.append(Issue(
                          severity="warning",
                          category="overflow",
                          file=filepath,
                          line=i,
                          message="white-space: nowrap without overflow handling may cause text overflow on mobile",
                          fix_hint="Add overflow: hidden; text-overflow: ellipsis; or use line-clamp",
                      ))
      
      
      def check_tailwind_issues(content: str, filepath: str, issues: list):
          if not filepath.endswith((".jsx", ".tsx", ".vue", ".svelte", ".astro")):
              return
      
          for i, line in enumerate(content.splitlines(), 1):
              # h-screen without dvh fallback
              if re.search(r'\bh-screen\b', line) and "h-dvh" not in line:
                  issues.append(Issue(
                      severity="warning",
                      category="viewport-units",
                      file=filepath,
                      line=i,
                      message="h-screen uses 100vh which has mobile browser chrome issues",
                      fix_hint="Use h-dvh (Tailwind v3.4+) or h-[100dvh] instead",
                  ))
      
              # Fixed width classes on containers
              match = re.search(r'\bw-\[(\d{4,})px\]', line)
              if match:
                  issues.append(Issue(
                      severity="warning",
                      category="overflow",
                      file=filepath,
                      line=i,
                      message=f"Fixed width w-[{match.group(1)}px] may overflow on mobile",
                      fix_hint="Use max-w-* or responsive w-full md:w-[...] pattern",
                  ))
      
      
      def scan_project(root: Path) -> ScanResult:
          result = ScanResult(project_path=str(root))
          files = collect_files(root)
          result.files_scanned = len(files)
      
          all_css = ""
          for f in files:
              if f.suffix in CSS_EXTS:
                  try:
                      all_css += f.read_text(errors="replace")
                  except Exception:
                      pass
      
          safe_area_checked = False
      
          for f in files:
              try:
                  content = f.read_text(errors="replace")
              except Exception:
                  continue
      
              relpath = str(f.relative_to(root))
      
              check_viewport_meta(content, relpath, result.issues)
              check_overflow_risks(content, relpath, result.issues)
              check_100vh_pitfall(content, relpath, result.issues)
              check_text_overflow(content, relpath, result.issues)
              check_touch_targets(content, relpath, result.issues)
              check_responsive_breakpoints(content, relpath, result.issues)
              check_tailwind_issues(content, relpath, result.issues)
      
              if not safe_area_checked and f.suffix in CSS_EXTS:
                  check_safe_area(content, relpath, result.issues, all_css)
                  safe_area_checked = True
      
          # Deduplicate info-level safe-area warnings
          seen_cats = set()
          deduped = []
          for issue in result.issues:
              key = (issue.category, issue.message) if issue.severity == "info" and issue.line is None else id(issue)
              if key not in seen_cats:
                  seen_cats.add(key)
                  deduped.append(issue)
          result.issues = deduped
      
          # Summary
          result.summary = {
              "total": len(result.issues),
              "errors": sum(1 for i in result.issues if i.severity == "error"),
              "warnings": sum(1 for i in result.issues if i.severity == "warning"),
              "info": sum(1 for i in result.issues if i.severity == "info"),
              "categories": list(set(i.category for i in result.issues)),
          }
      
          return result
      
      
      def format_text(result: ScanResult) -> str:
          lines = [
              f"Mobile Adaptation Scan: {result.project_path}",
              f"Files scanned: {result.files_scanned}",
              f"Issues found: {result.summary['total']} "
              f"({result.summary['errors']} errors, {result.summary['warnings']} warnings, {result.summary['info']} info)",
              "",
          ]
      
          if not result.issues:
              lines.append("No issues found.")
              return "\n".join(lines)
      
          for sev in ("error", "warning", "info"):
              sev_issues = [i for i in result.issues if i.severity == sev]
              if not sev_issues:
                  continue
              icon = {"error": "[E]", "warning": "[W]", "info": "[I]"}[sev]
              lines.append(f"--- {sev.upper()} ({len(sev_issues)}) ---")
              for issue in sev_issues:
                  loc = f"{issue.file}:{issue.line}" if issue.line else issue.file
                  lines.append(f"  {icon} [{issue.category}] {loc}")
                  lines.append(f"      {issue.message}")
                  lines.append(f"      Fix: {issue.fix_hint}")
                  lines.append("")
      
          return "\n".join(lines)
      
      
      def main():
          parser = argparse.ArgumentParser(description="Scan web project for mobile adaptation issues")
          parser.add_argument("project", help="Path to the web project root")
          parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
          args = parser.parse_args()
      
          root = Path(args.project).resolve()
          if not root.is_dir():
              print(f"Error: {root} is not a directory", file=sys.stderr)
              sys.exit(1)
      
          result = scan_project(root)
      
          if args.format == "json":
              data = {
                  "project_path": result.project_path,
                  "files_scanned": result.files_scanned,
                  "summary": result.summary,
                  "issues": [asdict(i) for i in result.issues],
              }
              print(json.dumps(data, indent=2, ensure_ascii=False))
          else:
              print(format_text(result))
      
      
      if __name__ == "__main__":
          main()
      
  • .gitignore 78 B · in bundle
  • CHANGELOG.md 300 B
    # Changelog
    
    ## [0.2.0] - 2026-08-24
    
    ### Added
    
    - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill
    
    ## 0.1.0 - 2026-07-13
    
    - Extracted into an independent source repository.
    - Kept compatible with the `skill-publisher/dev-skills` aggregate bundle.
    
  • LICENSE 1 KB · in bundle
  • README.md 2.2 KB
    # 移动适配师 · Mobile Fit
    
    ![Version](https://img.shields.io/badge/version-0.2.0-CC785C)
    
    Scan and fix mobile adaptation issues in web projects: viewport, overflow, safe area, responsive layouts, 100vh pitfalls, touch targets, and multi-level page navigation.
    
    Independent source repository, also distributed through [skill-publisher dev-skills](https://example.com/skills/dev-skills) — by [example.com](https://example.com)
    
    ## Install
    
    ```bash
    npx skills add skill-publisher/mobile-adapt-skill --all -g
    ```
    
    The aggregate bundle remains available:
    
    ```bash
    npx skills add skill-publisher/dev-skills --all -g
    ```
    
    Or through Claude Code plugin marketplace:
    
    ```text
    /plugin marketplace add skill-publisher/dev-skills
    /plugin install dev-tools@lov-dev
    ```
    
    Requires: Python 3.8+ (no external dependencies)
    
    ## What It Does
    
    1. **Scans** your project for mobile issues (overflow, viewport, safe area, touch targets)
    2. **Asks** which categories to fix
    3. **Applies** fixes following modern best practices (dvh, env(), container queries)
    4. **Restructures** navigation into mobile page stack with back support (optional)
    5. **Verifies** fixes by re-scanning
    
    ## Scanner
    
    ```bash
    python3 ~/.claude/skills/lov-mobile-adapt/scripts/scan_mobile_issues.py /path/to/project
    ```
    
    | Option | Default | Description |
    |--------|---------|-------------|
    | `project` | (required) | Path to project root |
    | `--format` | `text` | Output: `text` or `json` |
    
    Checks: viewport meta, overflow risks, 100vh usage, safe-area-inset, touch target sizes, responsive breakpoints, Tailwind-specific issues, text overflow.
    
    ## Covered Issues
    
    | Category | What gets fixed |
    |----------|----------------|
    | Viewport | Missing/incorrect meta viewport, viewport-fit=cover |
    | Overflow | Fixed widths, horizontal scroll, image overflow, text overflow |
    | Viewport units | 100vh → 100dvh with fallback |
    | Safe area | env(safe-area-inset-*) on fixed/sticky elements |
    | Touch targets | Interactive elements below 44px minimum |
    | Responsive | Missing breakpoints, mobile-first media queries |
    | Navigation | Sidebar → mobile page stack with back button |
    | Browser chrome | theme-color, status bar, input zoom, pull-to-refresh |
    
    ## License
    
    MIT
    
  • SKILL.md 6.8 KB
    ---
    name: lov-mobile-adapt
    description: >
      Adapt an existing web project for mobile devices: fix overflow, add responsive
      layouts, convert to multi-level page navigation with back support, handle
      notch/Dynamic Island safe areas, fix 100vh browser chrome issues, and optimize
      touch targets. Trigger when user says "mobile adapt", "移动端适配",
      "responsive optimization", "手机适配", "fix mobile overflow", "add safe area",
      "多级页面", "移动端布局", or mentions adapting a site for phones/tablets.
    license: MIT
    compatibility: >
      Requires Python 3.8+ (no external dependencies).
      Works with any web project: React, Vue, Next.js, Nuxt, Svelte, plain HTML/CSS.
    metadata:
      author: contributors
      version: "0.2.0"
      tags: mobile, responsive, safe-area, overflow, navigation
    ---
    
    # 移动适配师 · Mobile Fit
    
    Scan and fix mobile adaptation issues in an existing web project: viewport
    configuration, overflow prevention, safe area handling, responsive breakpoints,
    100vh pitfalls, touch targets, and multi-level page navigation.
    
    ## When to Use
    
    - Converting a desktop-first site to work well on mobile
    - User reports overflow, cut-off content, or notch overlap on phones
    - Adding mobile navigation (back button, page stack) to a sidebar-based layout
    - Fixing 100vh issues on iOS/Android browsers
    - General "make it mobile friendly" requests
    
    ## Workflow (MANDATORY)
    
    **You MUST follow these steps in order.**
    
    Resolve `SKILL_DIR` from the installed skill context before running the helper.
    For manual execution, set it to the directory containing this `SKILL.md`.
    
    ### Step 1: Scan the Project
    
    Run the scanner to identify issues:
    
    ```bash
    python3 "$SKILL_DIR/scripts/scan_mobile_issues.py" <project-path>
    ```
    
    For JSON output (easier to process programmatically):
    
    ```bash
    python3 "$SKILL_DIR/scripts/scan_mobile_issues.py" <project-path> --format json
    ```
    
    Review the output. The scanner checks:
    - viewport meta tag presence and `viewport-fit=cover`
    - CSS overflow risks (fixed widths, min-width)
    - 100vh usage (should be 100dvh)
    - safe-area-inset usage on fixed/sticky elements
    - Touch target sizes (< 44px)
    - Responsive breakpoint coverage
    - Tailwind-specific issues (h-screen → h-dvh)
    - Text overflow without ellipsis handling
    
    ### Step 2: Ask the User
    
    **IMPORTANT: Use `AskUserQuestion` to collect scope BEFORE making changes.**
    
    Present the scan results summary, then ask:
    
    ```
    Question: "扫描发现 X 个问题。要修哪些类别?"
    Options:
      1. 全部修复 (Recommended) — fix all categories found
      2. 只修布局和溢出 — overflow + responsive only
      3. 只修导航 — convert to mobile stack navigation
      4. 让我选具体类别 — pick specific categories
    ```
    
    If the project has sidebar/tab navigation on desktop, also ask:
    
    ```
    Question: "桌面端的侧边栏/tab 导航要改成移动端多级页面吗?"
    Options:
      1. 是,改成 push/pop 页面栈 (Recommended)
      2. 改成底部 tab bar
      3. 不改导航结构
    ```
    
    ### Step 3: Fix Issues
    
    Apply fixes in this priority order:
    
    1. **Viewport meta** — add or fix `<meta name="viewport">` with `viewport-fit=cover`
    2. **Global overflow guard** — add `overflow-x: hidden` to html/body
    3. **100vh → 100dvh** — replace all 100vh usages, add fallback
    4. **Safe area padding** — add `env(safe-area-inset-*)` to fixed/sticky elements
    5. **Image/media overflow** — add `max-width: 100%; height: auto`
    6. **Text overflow** — add ellipsis/line-clamp where `white-space: nowrap` exists
    7. **Touch targets** — increase size of interactive elements below 44px
    8. **Responsive breakpoints** — add mobile-first media queries or Tailwind responsive
    9. **Navigation restructure** — convert to mobile page stack if requested
    
    For each fix, refer to `references/mobile-patterns.md` for the correct pattern.
    
    ### Step 4: Navigation Restructure (if applicable)
    
    When converting sidebar/panel navigation to mobile stack:
    
    1. Identify the navigation structure (sidebar, tabs, nested panels)
    2. Create a responsive layout wrapper that switches between desktop and mobile
    3. On mobile: render as a full-screen page stack with back button
    4. Use CSS slide transitions for page push/pop
    5. Preserve desktop layout unchanged at `md:` breakpoint and above
    
    See `references/mobile-patterns.md` → "Multi-Level Page Navigation" for
    implementation patterns per framework.
    
    ### Step 5: Verify
    
    After all fixes:
    
    1. Re-run the scanner — confirm issues resolved
    2. Check the site in a mobile viewport (375px width)
    3. Verify:
       - No horizontal scroll
       - Content not cut off by notch or home indicator
       - All interactive elements are tappable (44px+)
       - Navigation back button works
       - Full-height layouts don't overflow behind browser chrome
    
    ## CLI Reference
    
    | Argument | Default | Description |
    |----------|---------|-------------|
    | `project` | (required) | Path to web project root |
    | `--format` | `text` | Output format: `text` or `json` |
    
    ## Key Patterns (quick reference)
    
    | Problem | Fix |
    |---------|-----|
    | Missing viewport meta | `<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">` |
    | 100vh overflow | `height: 100dvh` (with `100vh` fallback) |
    | Notch overlap | `padding: env(safe-area-inset-top)` on fixed elements |
    | Horizontal overflow | `overflow-x: hidden` on body + `max-width: 100%` on media |
    | iOS input zoom | `font-size: 16px` on inputs |
    | Small touch targets | `min-height: 44px; min-width: 44px` |
    | Pull-to-refresh conflict | `overscroll-behavior-y: contain` |
    
    For detailed patterns see `references/mobile-patterns.md`.
    
    ## Runtime context (shared)
    
    运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
    
    - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
    - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
    - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
    
    ## 通用反馈闭环
    
    用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
    
    1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。
    2. `task-specific` 只修改当前任务,不改 Skill。
    3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
    4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
    5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
    
  • skill.yaml 838 B
    schema: skill-manifest/v1
    id: lov-mobile-adapt
    version: "0.2.0"
    runtime: skill-runtime/v1
    context:
      profile:
        fields:
        - path: identity.name
          required: true
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: identity.logo
          required: false
          question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。
        - path: brand.tone
          required: false
          question: 如果已有品牌语气或审美关键词,请提供它们。
      preferences:
        namespace: lov_mobile_adapt
        fields:
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
      interaction:
        ask_missing: true
        max_questions: 1
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related