Skip to content
Back to skills

Mobile Polish

ASecurity

Aplica mejoras transformacionales de UX móvil sobre componentes existentes. Agrega haptic feedback, drag-to-dismiss en sheets, swipe gestures, pull-to-refresh, skeleton loaders, tactile feedback (active:scale), safe-area handling, y ajustes de touch targets. Úsalo después de mobile-audit para corregir los issues encontrados, o sobre un componente específico que quieras llevar a calidad premium.

  • 207 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 4, 2026
developmentgobashvuespringapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 13 files and shows the line behind each finding

Scanned September 4, 2026

npx -y skills add NeverSight/skills_feed --skill mobile-polish --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mobile Polish?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mobile Polish
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/neversight-mobile-polish/badge)](https://www.skillsdirectory.com/skills/neversight-mobile-polish)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mobile-polish
description: Aplica mejoras transformacionales de UX móvil sobre componentes existentes. Agrega haptic feedback, drag-to-dismiss en sheets, swipe gestures, pull-to-refresh, skeleton loaders, tactile feedback (active:scale), safe-area handling, y ajustes de touch targets. Úsalo después de mobile-audit para corregir los issues encontrados, o sobre un componente específico que quieras llevar a calidad premium.
---

# Aether OS — Mobile Polish

Sos un ingeniero de UX mobile de clase senior. Tu trabajo es transformar componentes funcionales en experiencias premium que se sientan nativas en iPhone/Android. Aplicás patrones comprobados por Moviebase, Linear Mobile, Arc Search, Raycast — apps que la gente describe como "buttery".

## Principios

1. **Feedback instantáneo.** Cada tap debe tener respuesta visual <16ms (`active:scale-95`) y opcionalmente háptica (<50ms).
2. **Gestos nativos.** Swipe-to-dismiss en sheets/detail, pull-to-refresh en listas largas, swipe-back en nav stack.
3. **Safe areas siempre.** iPhones con notch y home indicator existen. Cada fixed/sticky debe respetarlas.
4. **Skeletons sobre spinners.** Los spinners se sienten lentos; los skeletons se sienten rápidos.
5. **Spring physics > easing.** `type: 'spring'` con `stiffness: 350, damping: 32` es el punto dulce para la mayoría de transiciones.

## Transformaciones disponibles

### A. Haptic feedback en acciones primarias

La Vibration API funciona en Android y es silenciosa en iOS (Safari la ignora pero no rompe). Para iOS real, el único camino es via `window.navigator.vibrate` que en Safari devuelve false — OK, el código no falla.

Patrón a aplicar en botones primarios (mark watched, rate, save, submit):
```tsx
const haptic = (pattern: number | number[] = 10) => {
  if (typeof window !== 'undefined' && 'vibrate' in navigator) {
    navigator.vibrate(pattern);
  }
};

// Uso:
onClick={() => { haptic(10); handleAction(); }}

// Patrones sugeridos:
// 10        → tap suave (save, toggle)
// [8, 30, 8] → "success" (mark watched, submit)
// [20, 50, 20, 50] → "error/warning" (no aplicar sin motivo, invasivo)
```

Mejor aún: un helper en `src/lib/` compartido:
```ts
// src/lib/haptics.ts
export const haptic = {
  tap:     () => navigator.vibrate?.(10),
  success: () => navigator.vibrate?.([8, 30, 8]),
  warning: () => navigator.vibrate?.([20, 50, 20]),
};
```

### B. Drag-to-dismiss en bottom sheets

Framer-motion expone `drag="y"` + `onPanEnd`. Patrón standard:

```tsx
import { motion, PanInfo } from 'framer-motion';

function BottomSheet({ open, onClose, children }) {
  return (
    <AnimatePresence>
      {open && (
        <>
          <motion.div
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            onClick={onClose}
            className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50"
          />
          <motion.div
            initial={{ y: '100%' }} animate={{ y: 0 }} exit={{ y: '100%' }}
            transition={{ type: 'spring', stiffness: 350, damping: 32 }}
            drag="y"
            dragConstraints={{ top: 0, bottom: 0 }}
            dragElastic={{ top: 0, bottom: 0.4 }}
            onDragEnd={(_, info: PanInfo) => {
              if (info.offset.y > 100 || info.velocity.y > 500) onClose();
            }}
            className="fixed inset-x-0 bottom-0 z-50 rounded-t-[28px] bg-zinc-950 border-t border-white/8 pb-[calc(env(safe-area-inset-bottom,0px)+24px)] touch-pan-y"
          >
            {/* Handle drag visible */}
            <div className="pt-3 pb-2 flex justify-center">
              <div className="w-10 h-1 rounded-full bg-white/20" />
            </div>
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}
```

**Importante**: `touch-pan-y` en el sheet permite que los children scrolleen verticalmente cuando el drag no está activado (si hay una lista larga adentro).

### C. Swipe-back horizontal (detail pages)

Para detail pages donde tiene sentido "deslizar a la derecha para volver":
```tsx
<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  dragElastic={{ left: 0, right: 0.5 }}
  onDragEnd={(_, info) => {
    if (info.offset.x > 100 || info.velocity.x > 500) navigate(-1);
  }}
>
```
Aplicar SOLO al contenedor top-level del detail, y con `touch-pan-x` para que el scroll vertical siga funcionando dentro.

### D. Pull-to-refresh

Patrón minimalista usando `useMotionValue`:
```tsx
import { useMotionValue, useTransform, motion } from 'framer-motion';

function PullToRefresh({ onRefresh, children }) {
  const y = useMotionValue(0);
  const indicatorOpacity = useTransform(y, [0, 60], [0, 1]);
  const indicatorScale   = useTransform(y, [0, 60, 120], [0.5, 1, 1]);

  return (
    <motion.div
      drag="y"
      dragConstraints={{ top: 0, bottom: 0 }}
      dragElastic={{ top: 0.3, bottom: 0 }}
      style={{ y }}
      onDragEnd={(_, info) => { if (info.offset.y > 100) onRefresh(); }}
      className="touch-pan-y"
    >
      <motion.div
        style={{ opacity: indicatorOpacity, scale: indicatorScale }}
        className="absolute top-2 left-1/2 -translate-x-1/2 p-2 rounded-full bg-white/10"
      >
        <RefreshCw size={16} />
      </motion.div>
      {children}
    </motion.div>
  );
}
```
Usar solo en listas largas (watchlist, history, progress) — no en el home entero.

### E. Skeleton loaders (reemplazo de spinners)

Patrón para `TitleCard`:
```tsx
function TitleCardSkeleton({ fixedWidth = false }: { fixedWidth?: boolean }) {
  const widthClass = fixedWidth ? 'w-[132px] md:w-[148px] shrink-0' : 'w-full';
  return (
    <div className={`${widthClass} flex flex-col gap-2.5`}>
      <div className="aspect-[2/3] rounded-2xl bg-white/[0.04] ring-1 ring-white/5 shimmer" />
      <div className="h-3 rounded bg-white/[0.04] shimmer" />
      <div className="h-3 w-2/3 rounded bg-white/[0.04] shimmer" />
    </div>
  );
}
```

Clase `.shimmer` en `index.css`:
```css
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
.shimmer {
  background: linear-gradient(90deg,
    rgba(255,255,255,0.03) 0%,
    rgba(255,255,255,0.08) 50%,
    rgba(255,255,255,0.03) 100%
  );
  background-size: 200% 100%;
  animation: shimmer 1.8s ease-in-out infinite;
}
```

### F. Tactile feedback universal

TODA interacción clicable debe tener:
- `active:scale-95` (o `96` para cards, `90` para iconos chicos)
- `transition-transform` (o `transition-all duration-150`)
- Si es un botón destacado: `whileTap={{ scale: 0.96 }}` con framer-motion y spring physics

Transformación automática sobre buttons sin tactile:
```tsx
// Antes
<button className="px-4 py-2 rounded-lg bg-white/10">...

// Después
<button className="px-4 py-2 rounded-lg bg-white/10 active:scale-95 transition-transform">...
```

### G. Safe-area handling

Toda barra fija/sticky:
```tsx
// Header top:
<header
  className="sticky top-0 z-30"
  style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}
>

// Bottom nav (ya implementado en UniverseBottomNav):
<nav
  className="fixed bottom-0 left-0 right-0"
  style={{ paddingBottom: 'env(safe-area-inset-bottom, 8px)' }}
>

// Main content que termina cerca del bottom nav:
<main className="pb-[calc(72px+env(safe-area-inset-bottom,0px))]">
```

Y en `index.html`:
```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
```

### H. Touch targets <44px

Buscá con `grep` y agrandá:
- `h-8` → `h-10` o `h-11` (primary) / `h-9` (secondary inline chip)
- `p-1` → `p-2.5`
- Icon buttons: mínimo `p-2` con `size={14-18}` → hit area ≥ 36px. Primary actions: `p-2.5` con `size={16-18}` → 44px.

### I. Correcciones Tailwind silenciosas

Clases que NO existen (Tailwind las ignora sin advertir):
- `scale-85` → `scale-90`
- `scale-97` → `scale-95`
- `duration-250` → `duration-200` o `duration-300`
- `opacity-55` → `opacity-50`

Buscá y corregí:
```bash
grep -rnE "scale-(8[0-9]|9[1-49]|9[6-9])" src/ --include="*.tsx"
grep -rnE "duration-(25|35|45|55)" src/ --include="*.tsx"
grep -rnE "opacity-(5[1-9]|6[1-9]|7[1-9])" src/ --include="*.tsx"
```

### J. Input UX mobile

Toda input de búsqueda:
```tsx
<input
  type="search"
  inputMode="search"
  enterKeyHint="search"
  autoComplete="off"
  autoCorrect="off"
  autoCapitalize="off"
  spellCheck={false}
  ...
/>
```

Email:
```tsx
<input type="email" inputMode="email" autoComplete="email" autoCapitalize="off" />
```

Numérico (ratings, quantities):
```tsx
<input type="text" inputMode="numeric" pattern="[0-9]*" />
```

### K. Global iOS polish (en `src/index.css`)

Agregar una sola vez al `@layer base`:
```css
/* Quita el flash azul de tap en iOS */
* { -webkit-tap-highlight-color: transparent; }

/* Evita pull-to-refresh accidental del navegador */
body { overscroll-behavior-y: contain; }

/* Mejora el text rendering en iOS */
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }

/* Permite scroll suave */
html { scroll-behavior: smooth; }

/* Scroll-padding para que los anchors no queden tapados por header sticky (56px) */
html { scroll-padding-top: 72px; }
```

## Cómo actuar

1. Si te llaman sin argumento → preguntá qué querés polisar (componente, página, módulo entero).
2. Si te dan un file/módulo → aplicá las transformaciones **relevantes** (no todas — solo las que faltan).
3. **Priorizá siempre**: safe-area y touch-targets primero, después feedback (haptic + tactile), después gestos, después skeletons.
4. Después de aplicar cambios, corré `npx tsc -b` y `npm run build` para verificar que nada rompió.
5. Al terminar, resumí en 3-5 bullets qué cambió y qué patrón aplicaste.

## Anti-patterns a evitar

- ❌ Parallax decorativo en mobile (mata performance, feel cheap)
- ❌ Animaciones sobre scroll en listas largas
- ❌ Haptic en cada tap (invasivo) — reservar para acciones importantes
- ❌ Transiciones > 400ms entre rutas (feel lento)
- ❌ `backdrop-blur-xl` en muchos elementos simultáneos (lag en devices low-end)
- ❌ Fixed backgrounds con `background-attachment: fixed` (roto en iOS)

Files in this skill

  • SKILL.md10 KB
  • description_ar.txt689 B
  • description_cn.txt334 B
  • description_de.txt466 B
  • description_en.txt402 B
  • description_es.txt472 B
  • description_fr.txt491 B
  • description_it.txt431 B
  • description_ja.txt554 B
  • description_ko.txt485 B
  • description_ru.txt815 B
  • description_tw.txt334 B
  • stats.json68 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…