agentsclimarketplace

Enhance capacitor ui

Skill kensaurus/cursor-kenji/skills/enhance-capacitor-ui

Cross-surface UIUX separation skill for hybrid web apps that ship as PWA + iOS + Android via Capacitor (or Tauri / Expo Web / Ionic / RN-Web). Use when a previous UI/UX sweep "improved one surface and broke the other" — desktop polished but mobile cramped, or mobile native but desktop wastes space. Also use when the project has ad-hoc useIsMobile / isNative branches scattered across components, a single md: breakpoint doing double duty as "is desktop" and "wider slot", or per-component platform styling instead of mode tokens. Establishes three orthogonal axes — form factor (compact/medium/expanded), platform (web/ios/android), pointer (fine/coarse) — and a three-layer architecture (context hook, mode tokens, container-query primitives) so one sweep can enhance one axis without degrading the others. Catches axis conflation in a single boolean, hover-only affordances on native shells, and SSR/Capacitor first-paint mismatch. For pure visual polish on a single surface, use enhance-web-ui first.From its SKILL.md

Install
npx -y skills add kensaurus/cursor-kenji --skill enhance-capacitor-ui

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

20.2 KB, ~5.1k tokens by cl100k_base, as published. Nobody here has run it

Which enhance skill? (surface router)

Your surfaceUse
Web product page / dashboard — composition, hierarchy, spacing, motionenhance-web-ui
Web product page — UX heuristics, flows, data wiringenhance-web-ux
Web landing / marketing / portfolio (greenfield, anti-slop)enhance-web-landing
Web existing site upgrade (audit-first, preserve behavior)enhance-web-redesign
Web 3D / WebGL / cinematic scroll on an existing site (audit-first)enhance-web-web3d
React Native screen (Expo / bare)mobile-rn-screen
Capacitor / hybrid shell (one web app shipped to iOS + Android)enhance-capacitor-ui (axis architecture first) → then the web or rn skill
Repo README showcaseenhance-readme

You are here: enhance-capacitor-ui. Native iOS/Android (SwiftUI / Compose, no web layer) is out of scope for all of these — use Apple HIG / Material directly.

Enhance Web ↔ Mobile UI

Separate the mobile and desktop design contracts of a hybrid app so that a UIUX sweep on one surface cannot silently degrade the other. This is not a "add a useIsMobile hook" skill. It establishes three orthogonal axes, one source of truth, mode tokens, container-query primitives, and a chrome allow-list — so per-form-factor / per-platform polish is a predictable, reviewable change instead of a regression.

Vague-but-visceral cross-surface feedback ("looks great on web, atrocious on iPhone", "the iPad version is just a stretched phone", "Android dock feels wrong", "everything wraps on tablet") almost always points to axis conflation — one boolean (isMobile) is being asked to answer three different questions. Start at the architecture, not at the broken page.

If browser/native automation is used, first follow the protocol-browser-anti-stall skill when present.


Critical Rules

Three orthogonal axes — never conflate them.

  1. Form factor (compact / medium / expanded) = how much screen space do I have? — drives layout density, chrome strategy, navigation pattern (bottom dock vs side rail vs persistent sidebar).
  2. Platform (web / ios / android) = which OS visual language am I inside? — drives motion curves, tap-highlight, overlay radii, safe-area math, haptic affordances.
  3. Pointer capability (fine / coarse, hover / no-hover) = can the user hover and click precisely? — drives hover-reveal affordances, drag-handle sizes, hit-target floors. A single useIsMobile boolean answering all three is the root cause of most cross-surface regressions. Encode the three axes separately.

One source of truth. Detect the three axes once, in a single useViewportContext() (or equivalent) hook, and write them to <html data-form-factor data-platform data-can-hover> so CSS, server components, and SSR-rendered HTML all read the same answer. Banning ad-hoc useMatchMedia / pathname.startsWith("/desktop") / Capacitor.isNativePlatform() branches scattered across components.

Token modes, not per-component conditionals. Platform differences that are visual (overlay radius, press-scale, tap-highlight color) live in :root[data-platform="ios"] blocks as CSS variable overrides, not in if (platform === "ios") JSX branches. Components consume the token; the platform layer paints it. (Ionic Modes, Konsta UI pattern.)

Page chrome = viewport queries; component micro-layout = container queries. App-level chrome (bottom dock vs side rail vs sidebar) reacts to the viewport — only the app shell knows the global form factor. Cards, tiles, grids, hero blocks inside the content column react to their container — they may be in the main column, a side panel, a split-pane, a modal. Use md: / lg: only in the chrome allow-list (typically: app-shell, bottom-nav, side-nav, side-rail). Use @md: / @lg: (container queries) everywhere else. Tailwind 4 ships container queries built-in; Tailwind 3 needs the plugin.

Per-form-factor squint test. "Looks good at 1440" is one cell of a six-cell grid (3 form factors × 3 platforms minimum, plus pointer on/off). Before declaring an enhancement done, view it at compact (≤640px), medium (768–1023px), and expanded (≥1024px), on web + iOS WebView + Android WebView (or the closest available native shell). Any cell that looks "templated" or "broken" was not actually enhanced — it was avoided.

Tier across surfaces, not across the app. A learning / consumer / lifestyle app should be tier-A vibrant on the compact form factor (color carries gamification meaning — Duolingo) and may legitimately become tier-C calm on the expanded form factor (more chrome, more data density, less novelty). The mistake is applying one saturation tier across all surfaces. Pick a tier per form factor; document it. (See enhance-web-ui § Domain Colour Tier.)

Never ship mock, dead, or rogue platform branches. Platform-mode tokens must be wired to the real Capacitor.getPlatform() result (or your stack's equivalent). Don't fork visual logic on a static process.env.MODE === "ios" literal — the same bundle ships to all three surfaces and the platform is only known at runtime.

Guardrail the chrome allow-list. Once md: / lg: are restricted to the chrome allow-list, an ESLint no-restricted-syntax rule + a Cursor / AGENTS rule (responsive-design-axes.mdc) keeps them there. A future enhancer (human or AI) will reach for lg:grid-cols-2 in a primitive within a week unless the rule warns them.

Patch the axis primitive, not the consumer. Same rule as enhance-web-ui Primitive-First Patch Rule, scoped to axes: if twelve cards each carry their own lg:flex-row branch, the bug is that the card primitive isn't a @container. Patch the primitive once, delete the twelve consumer branches.


Workflow Checklist

Copy and track:

WEB↔MOBILE SEPARATION /<repo or surface>
- [ ] 0. SURFACE INVENTORY: shipping targets (PWA / iOS / Android / Mac
 Catalyst / Tauri), Capacitor or shell version, breakpoint system,
 Tailwind 3 vs 4 (container queries built-in?)
- [ ] 1. AXES AUDIT: how does the codebase currently answer "is mobile?",
 "is iOS?", "can hover?" — count the call sites, list the helpers
- [ ] 2. CHROME vs CONTENT MAP: which files own page-level chrome (allow
 `md:`/`lg:`), which files are content/primitives (must use `@md:`)
- [ ] 3. PROBE: grep bare `md:`/`lg:` outside the allow-list, count
 `useIsMobile` / `isNative` / `pathname.startsWith` branches,
 inspect SSR first-paint at /compact and /expanded
- [ ] 4. GAP MAP: current state vs three-layer architecture (context →
 tokens → primitives) — what's missing, what's duplicated
- [ ] 5. PLAN: six-step rollout (context hook → mode tokens → form-factor
 gates → container-query primitives → heuristic guide → guardrail)
- [ ] 6. IMPLEMENT: smallest diff per step; SSR-safe defaults at every
 boundary
- [ ] 7. VERIFY: six-cell matrix (3 form factors × 3 platforms), plus
 SSR/first-paint check, plus reduced-motion, plus dark/light if
 supported
- [ ] 8. WRITE-UP: surface → before pain → axis violated → primitive →
 after behaviour

Step 0 — Surface Inventory

Before any change, write down what the app actually ships to. This prevents designing for surfaces that don't exist (and forgetting ones that do).

SurfaceDetectionNotes
PWA / browserdefaultIncludes desktop browser AND iOS/Android Safari/Chrome
iOS native (WebView)Capacitor.getPlatform() === "ios"Capacitor / Ionic shells
Android native (WebView)Capacitor.getPlatform() === "android"Capacitor / Ionic shells
Mac CatalystiPad-class iOS surface running on macOSForm factor matters more than platform
iPad / large iOSiOS + ≥768pxOften misses iPad-specific layout if you only test iPhone
Foldable / split-screenwindow resize event mid-sessionForm factor changes at runtime, must subscribe

Extract from the project:

  • package.json → Capacitor / Tauri / Ionic / Expo / RN-Web version.
  • capacitor.config.*webDir, deployment targets, custom URL scheme.
  • tailwind.config.* or globals.css @theme → existing breakpoints, whether container queries are configured.
  • Any useIsMobile / useBreakpoint / usePlatform hook → catalogue.

Step 1 — Axes Audit (find the conflation)

Run these probes; expect to find 5–30 hits per probe in any non-trivial hybrid app. Each hit is a candidate for axis-separation.

# How does the codebase currently answer "is mobile?" — count the call sites
rg -n 'useIsMobile|isMobile|isDesktop|breakpoint\.lg|matchMedia.*768|window\.innerWidth' \
 --glob '!**/node_modules/**' --glob '!**/dist/**' | wc -l

# How does it answer "is iOS?"
rg -n 'Capacitor\.getPlatform|isNativePlatform|navigator\.platform|isIOS|isAndroid' \
 --glob '!**/node_modules/**'

# How does it answer "can the user hover?"
rg -n 'matchMedia.*hover|useCanHover|"hover:|@media \(hover' \
 --glob '!**/node_modules/**'

# Bare viewport prefixes outside the chrome allow-list (proxy for
# axis-conflation in primitives)
rg -n '\b(md|lg|xl|2xl):' \
 --glob 'design-system/**' \
 --glob 'components/**/!(@(app-shell|bottom-nav|side-nav|side-rail))*' \
 | wc -l

Build the conflation table:

Hit (file:line)Question being askedAxis it should map toCurrent proxyFix shape
Card.tsx:42 lg:flex-row"is the card slot wide?"container sizeviewport lg:@md:flex-row after Card becomes @container
Toast.tsx:18 useIsMobile()"should I show a sheet or a side toast?"form factorviewport md:data-form-factor="compact" token + Tailwind ff-compact: variant
Button.tsx:30 if (Capacitor.getPlatform() === "ios") rounded-2xl"what's the OS visual language?"platformruntime branch--btn-radius token under :root[data-platform="ios"]
IconBtn.tsx:15 hover:bg-muted"show hover paint"pointer capabilitybare hover:can-hover:hover:bg-muted (axis-aware variant)

If the table has > 10 rows, the project needs the full architecture; if < 5, you may be able to land just steps 1–3 (context hook + mode tokens + gate the chrome).


Step 2 — Chrome vs Content Map (the allow-list)

The single most important architectural decision in this skill: which files are allowed to read the viewport, and which must read their container. Write the list down before touching anything.

Default allow-list (files that may use bare md: / lg: / xl:):

components/app-shell.tsx # the only file that knows global form factor
components/bottom-nav.tsx # mobile dock; `md:hidden` is correct here
components/side-rail.tsx # tablet rail; `hidden md:flex` is correct
components/desktop-side-nav.tsx # desktop sidebar; `hidden lg:flex`
app/layout.tsx # SSR root, sets data-form-factor first-paint fallback

Default content list (files that must use @md: / @lg: container queries OR axis-token variants):

design-system/primitives/** # Card, Button, Input, OptionCard, etc.
features/**/components/** # feature-level UI
app/**/page.tsx # route content (page chrome lives in layout)

Any time someone adds a 5th file to the chrome allow-list, that's a code review smell: usually it should have been a content file with a container query, not a chrome file with a viewport query.


Step 3 — Probe live (SSR first-paint + runtime axes)

3a. SSR / hydration first-paint check

Hybrid web apps render twice: first on the server (no window, no Capacitor.getPlatform()), then on the client. If the form factor / platform decision is made in render, you ship a flash of wrong chrome on every navigation.

ProbeWhere to lookFailure shape
First-paint at /compactView page source (Next.js ?_rsc or curl)If both <BottomNav> AND <DesktopSideNav> render, then JS hides one — you have FOUC
First-paint at /expandedSameIf neither renders, then JS reveals one — you have a 200ms gap of no nav
Capacitor first-paintiOS/Android WebView devtoolsIf data-platform="" for the first 50ms, your platform-specific tokens haven't applied yet

Fix shape: render both chrome variants on the server (dual-mount), gate visibility purely with CSS via a viewport @media query. The CSS query runs synchronously at first paint, no JS needed. The form-factor data- attribute is then written by JS after hydration, used only for axis- specific tweaks not chrome visibility.

3b. Runtime axes probe (in the browser console, in each shell)

// Generic, framework-free; works in DevTools console / browser MCP / native WebView
const html = document.documentElement;
console.table({
 formFactor: html.dataset.formFactor || '(not set)',
 platform: html.dataset.platform || '(not set)',
 canHover: html.dataset.canHover || '(not set)',
 viewport: `${innerWidth}x${innerHeight}`,
 ua: navigator.userAgent.slice(0, 80),
});
// Then resize the window and re-run — does formFactor flip live?
// On iOS Safari: rotate device — does formFactor flip?
// On Capacitor iOS: send to background and resume — does any value get stale?

Any (not set) cell at first paint is a bug — your context hook is running too late.

3c. Container-query gate

For any primitive you are about to convert from lg: to @lg:, measure the rendered widths before the change and after. If the change is correct, the primitive should:

  • Render the wider layout when its parent slot is wide (container > N px).
  • Render the narrower layout when its parent slot is narrow.
  • Be independent of the global viewport — drop it inside a 320 px side panel on a 1920 px monitor and it should look like the compact variant.
// In the browser console / Playwright evaluate
const cards = [...document.querySelectorAll('[data-card]')];
console.table(cards.map(c => ({
 rect: Math.round(c.getBoundingClientRect().width),
 layout: getComputedStyle(c).gridTemplateColumns || getComputedStyle(c).flexDirection,
})));
// Expect: layout flips at the container threshold, not the viewport

Hidden Failure Modes (the cross-surface ones)

Parallel to enhance-web-ui H1–H16 but specific to multi-surface contracts. Each entry has a detection probe and a fix shape. Generic across stacks.

M1. Axis conflation in a single boolean

Symptom: "Mobile sheet renders on iPad landscape", "desktop side panel renders on touch laptop", "iOS-style overlay shows on Android".

Detection: rg -n 'useIsMobile|isMobile' --type ts | wc -l returns

5. Read each call site and ask: "is this asking about screen size, about platform, about pointer capability, or about the combination?"

Fix shape: Replace the single boolean with three answers from useViewportContext() (or equivalent). Update each call site explicitly:

// before
if (isMobile) return <Sheet>{children}</Sheet>;
return <Popover>{children}</Popover>;

// after — three axes addressed independently
const { formFactor, canHover } = useViewportContext();
if (formFactor === "compact") return <Sheet>{children}</Sheet>;
if (!canHover) return <Sheet>{children}</Sheet>;
return <Popover>{children}</Popover>;

Most "mobile" branches are actually "compact form factor OR coarse pointer", which is not the same as viewport < 768.

M2. Tier-A heuristics applied to expanded form factor (or vice versa)

Symptom: "Phone looks like a B2B dashboard", "desktop looks like a toy". Result of running a single UIUX sweep that picked one saturation tier and applied it across all form factors.

Detection: Take the desktop screenshot, take the phone screenshot, side by side. If both surfaces use the same /8 background tint and the same primary CTA chrome, you've ignored form factor. Tier-A on phone (/15–/25 tints, solid CTAs, semantic hue per category) and tier-C on desktop (/6–/12 tints, one accent, achromatic chrome) is correct for a learning / consumer / lifestyle app — not a bug.

Fix shape: Document the per-form-factor tier in your HEURISTICS.md. Pick the tier per data-form-factor selector at the token layer:

:root[data-form-factor="compact"] {
 --surface-tier-mix: 0.20; /* tier-A vibrant */
 --cta-bg: var(--color-cta); /* solid */
}
:root[data-form-factor="expanded"] {
 --surface-tier-mix: 0.08; /* tier-C restrained */
 --cta-bg: transparent; /* outline */
 --cta-border: var(--color-cta);
}

Then primitives consume --surface-tier-mix and --cta-bg once; the form factor decides which tier paints.

M3. No data-platform on <html> (token forks living in components)

Symptom: Buttons forked per platform inside JSX (if (platform === "ios") rounded-2xl else rounded-md). Five components do this; the sixth forgets and the bug ships to one shell.

Detection: rg -n 'getPlatform\(\)|isNativePlatform' --type tsx returns > 3 hits in component code (not in the context hook itself).

Fix shape: Detect platform once, write <html data-platform>, declare per-platform token overrides in CSS:

:root[data-platform="ios"] {
 --radius-overlay: 1.25rem;
 --press-scale: 0.96;
 --tap-highlight: transparent;
 -webkit-tap-highlight-color: transparent;
}
:root[data-platform="android"] {
 --radius-overlay: 1.75rem; /* M3 corner-extra-large */
 --press-scale: 1; /* Android uses ripple, not scale */
 --tap-highlight: rgba(0, 0, 0, 0.08);
 -webkit-tap-highlight-color: rgba(0, 0, 0, 0.08);
}

Components reference var(--radius-overlay) once; the platform layer picks which value paints. Delete the JSX forks.

M4. Bare md: for component micro-layout

Symptom: A card that flips to two columns at viewport-lg: looks broken when dropped into a 400 px side panel on a 1920 px monitor — the card is narrow but the viewport says "go two-column", so the content overflows.

Detection: Grep for md:grid-cols-2 / lg:flex-row / xl:items-center inside design-system/primitives/** or features/**/components/**. Anything outside the chrome allow-list is a candidate.

Fix shape: Make the primitive a @container and convert md:@md:, lg:@lg:. Pick the container-query breakpoint that matches the effective slot width, not the viewport:

// before — viewport-coupled, breaks in side panels
<Card className="grid lg:grid-cols-[1fr_2fr]">…</Card>

// after — slot-coupled, works anywhere
<Card className="@container grid @4xl:grid-cols-[1fr_2fr]">…</Card>
// ^^^^^^^^^^^ ^^^^^^
// container root container threshold

Picking the threshold: if the previous viewport lg: (1024 px) worked because the main column was ~896 px wide at that viewport, the new container threshold is @4xl: (~896 px). Container queries don't inherit Tailwind's viewport scale; they have their own (@xs 320, @sm 384, @md 448, @lg 512, @xl 576, @2xl 672, @3xl 768, @4xl 896, @5xl 1024, @6xl 1152, @7xl 1280). Choose the one that matches the slot's typical width, not the viewport's.

Further reading

What ships with it: 1 file

31.5 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,782. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.