SkateSpots/.agents/skills/capacitor/SKILL.md
shakespeare.diy 06fae294b8 New project created with Shakespeare
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 19:49:07 +07:00

9.3 KiB

name description
capacitor Wrap the mkstack web app as a native iOS and Android application using Capacitor. Provides haptics, native file downloads / share sheet, OS-level secure storage (Keychain / KeyStore), deep-link routing, status-bar theme sync, and safe-area CSS utilities.

Capacitor Native Wrapper

This skill turns the web app into a native iOS and Android binary using Capacitor — no Swift or Kotlin required for the basics. The React UI runs unchanged inside a native WebView; the helpers below provide the cross-platform primitives that make it feel native.

Not included in the project by default. Run the setup below when the user wants to ship to an app store or produce an .apk / .ipa.

What this skill provides

Capability Web behavior Native behavior
Haptics (impactLight, notificationSuccess, …) navigator.vibrate() on Android browsers Taptic engine / Android haptics
downloadTextFile(filename, content) <a download> click Writes to app Documents directory
openUrl(url) window.open(url, '_blank') Native share sheet
secureStorage / useSecureLocalStorage localStorage iOS Keychain / Android KeyStore, auto-migrates plaintext values
<DeepLinkHandler /> no-op Forwards OS appUrlOpen into React Router
bootstrapNative() no-op Hides iOS keyboard accessory bar; syncs system-bar icon style with theme
Safe-area utilities (Tailwind) env(safe-area-inset-*) env(safe-area-inset-*) + SystemBars back-fill on Android

Every helper is SSR-safe and web-safe — import and call unconditionally from shared components. Each source file ships with a JSDoc header covering the exact API, gotchas, and usage examples; read them after copying.

Files to copy

Copy from .agents/skills/capacitor/files/ into the matching project location:

Skill file Copy to
files/lib/haptics.ts src/lib/haptics.ts
files/lib/downloadFile.ts src/lib/downloadFile.ts
files/lib/secureStorage.ts src/lib/secureStorage.ts
files/lib/nativeBootstrap.ts src/lib/nativeBootstrap.ts
files/hooks/useSecureLocalStorage.ts src/hooks/useSecureLocalStorage.ts
files/components/DeepLinkHandler.tsx src/components/DeepLinkHandler.tsx
files/capacitor.config.ts capacitor.config.ts (project root — edit appId, appName, scheme, background colors)
files/scripts/patch-cap-config.mjs scripts/patch-cap-config.mjs — only needed if you add local (non-SPM) native plugin classes; otherwise skip
files/safe-area-shim.css src/safe-area-shim.css — only if you need Android WebView <140 support (see shim file header and troubleshooting below)

Setup

1. Install

# Runtime + plugins
npm install @capacitor/core @capacitor/app @capacitor/filesystem \
  @capacitor/haptics @capacitor/keyboard @capacitor/share \
  capacitor-secure-storage-plugin

# Toolchain
npm install -D @capacitor/cli @capacitor/android @capacitor/ios

# Safe-area utilities (Tailwind v4 — this project)
npm install -D tailwindcss-safe-area

mkstack ships Tailwind v4 with CSS-first config (@import "tailwindcss" in src/index.css, no tailwind.config.ts). If you're adapting these instructions to a Tailwind v3 project, install tailwindcss-safe-area@0.8.0 instead and register it in tailwind.config.ts's plugins array — 1.x emits no utilities on v3.

2. Copy the skill files

Copy every file from the table above.

3. Register the safe-area plugin

Add a second @import to src/index.css, right after the existing @import "tailwindcss";:

@import "tailwindcss";
@import "tailwindcss-safe-area";
@import "tw-animate-css";

Use @import, not @plugin. The package ships a plain CSS file for Tailwind v4, not a JS plugin entry point.

4. Fill in capacitor.config.ts

Edit appId (reverse-DNS; cannot be changed after publishing), appName, ios.scheme, and the iOS/Android backgroundColor values. All other defaults in the skill's config are production-safe.

5. Update index.html

iOS won't expose env(safe-area-inset-*) without viewport-fit=cover:

<meta name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />

Call bootstrapNative() before React mounts, so system bars are themed at first paint:

// src/main.tsx
import { bootstrapNative } from '@/lib/nativeBootstrap';
bootstrapNative();
createRoot(document.getElementById('root')!).render(<App />);

Render <DeepLinkHandler /> inside <BrowserRouter> so useNavigate() works:

// src/AppRouter.tsx
<BrowserRouter>
  <DeepLinkHandler />
  <ScrollToTop />
  {/* <Routes>…</Routes> */}
</BrowserRouter>

7. Add platforms and build

npx cap add android
npx cap add ios          # macOS only

npm run build            # produces dist/
npx cap sync             # copies dist/ into android/ and ios/

npx cap open android     # Android Studio; hit Run
npx cap open ios         # Xcode (macOS only); hit Run

Commit the generated android/ and ios/ directories (signing keys stay excluded via the generated .gitignores). If you later add a local (non-SPM) Capacitor plugin class, append node scripts/patch-cap-config.mjs to your sync command so packageClassList survives each npx cap sync.

Using the APIs

Each file's JSDoc header has full usage details. Quick tour:

import { impactLight, notificationSuccess } from '@/lib/haptics';
import { downloadTextFile, openUrl } from '@/lib/downloadFile';
import { secureStorage } from '@/lib/secureStorage';
import { useSecureLocalStorage } from '@/hooks/useSecureLocalStorage';

impactLight();                                         // fire-and-forget
notificationSuccess();                                 // silent no-op on unsupported platforms

await downloadTextFile('export.json', JSON.stringify(data));
await openUrl('https://example.com');

await secureStorage.setItem('nwc:active', conn);
await secureStorage.getItem('nwc:active');

const [conn, setConn, ready] = useSecureLocalStorage<string | null>('nwc:active', null);
if (!ready) return <Spinner />;                        // native reads are async

Safe-area utilities

Once step 3 is done, *-safe utilities are available for padding, margin, position, height, border, and scroll properties. The most useful ones:

  • pt-safe, pb-safe, px-safe, py-safe, p-safe (also logical ps-/pe- and start-/end- positions)
  • top-safe, bottom-safe, inset-safe, inset-x-safe
  • h-dvh-safe, min-h-dvh-safe, h-svh-safe, h-lvh-safe, h-screen-safe
  • {prop}-safe-offset-{n} — safe area plus {n} (pb-safe-offset-4 = inset + 1rem)
  • {prop}-safe-or-{n} — max(safe-area, {n}) (pb-safe-or-8 = at least 2rem)
<header className="sticky top-0 pt-safe">…</header>
<nav className="fixed inset-x-0 bottom-0 pb-safe">…</nav>
<Dialog className="h-dvh-safe">…</Dialog>
<footer className="pb-safe-or-8">…</footer>

Full list: plugin README.

Common follow-ups

  • App icons / splash screen — standard icon generator → android/app/src/main/res/mipmap-*/ and ios/App/App/Assets.xcassets/AppIcon.appiconset/. For splash, add @capacitor/splash-screen and configure under plugins.SplashScreen.
  • Push notifications — add @capacitor/push-notifications (FCM / APNs); the web side keeps using Web Push.
  • Deep-link verification — host apple-app-site-association and assetlinks.json on your domain so the OS opens verified https:// links directly. Full checklist in the DeepLinkHandler.tsx header.
  • Nsec autofill via iCloud Keychain / Credential Manager — add @capgo/capacitor-autofill-save-password and wrap it analogously to secureStorage.

Troubleshooting

  • Safe-area utilities render as 0 on Android — Chromium WebView <140 reports env(safe-area-inset-*) as 0 (bug 40699457). SystemBars (insetsHandling: 'css', already set) injects --safe-area-inset-* CSS variables, but tailwindcss-safe-area uses env(…) directly. Preferred fix: target WebView 140+ (Aug 2025; auto-updated on Android ≥7). Fallback: copy files/safe-area-shim.css to src/ and @import it after @import "tailwindcss-safe-area"; so the variables take over when env() is 0.
  • pt-safe doesn't exist — either @import "tailwindcss-safe-area"; is missing / in the wrong position (must come after @import "tailwindcss";), or you installed 0.8.0 (v3-only) on a v4 project. npm ls tailwindcss-safe-area should show >=1.0.0.
  • Safe-area is 0 on iOS Safari / simulator — missing viewport-fit=cover in the <meta name="viewport"> tag.
  • Deep links don't navigate — <DeepLinkHandler /> must be inside <BrowserRouter>; useNavigate() throws silently otherwise.
  • iOS keyboard still shows the accessory bar — bootstrapNative() must run before any <input> is focused. Invoke at the top of main.tsx.

Remaining edge cases (secureStorage returning null after debug reinstall, status-bar theming with custom CSS-variable setups) are covered in the corresponding file headers.