feat: native iPad wrapper — Capacitor iOS scaffold

Wrap the PWA in a thin native iOS shell (Capacitor 8):
- capacitor.config.json (webDir dist, appId uk.timevans.thedirector,
  iOS safe-area contentInset + theme background).
- @capacitor/{core,cli,ios} as devDependencies (web app keeps zero
  runtime deps); ios:add / ios:sync / ios:open / ios npm scripts.
- main.js skips service-worker registration under Capacitor
  (window.Capacitor.isNativePlatform) — assets are already local.
- iOS <meta> hints in index.html; .gitignore for native build output.
- docs/native-ipad-wrapper.md: full Xcode + CocoaPods build guide,
  incl. the keep-awake plugin note for the wake toggle in WKWebView.

The iOS project generation/build (npx cap add ios → Xcode) needs a Mac
with full Xcode + CocoaPods, which this env lacks; it is documented for
the user to run. tests/tools/capacitor-config.test.js guards the config.
211 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-07-20 23:55:20 +01:00
co-authored by Claude Opus 4.8
parent 028dcb5e4e
commit e79f643409
10 changed files with 1261 additions and 5 deletions
+11
View File
@@ -1,3 +1,14 @@
.superpowers/
node_modules/
dist/
# Capacitor native build output (the ios/ project itself is committed once generated,
# but these generated/derived paths are not)
/ios/App/App/public/
/ios/App/Pods/
/ios/App/output/
/ios/App/build/
/ios/DerivedData/
/ios/capacitor-cordova-ios-plugins/
*.xcuserstate
.DS_Store
+3 -1
View File
@@ -130,7 +130,9 @@ not parsed; you write the structured markdown. The `tools/` directory is dev-onl
3. **Slice 3** — dice rule-packs (CoC d100, VANITY d6-pool, Panic & Glory, Dee Sanction) + tray pack selector. ✅
4. **Slice 4** — the clue **safety-net** (essential-clue gap tracker + fallbacks). ✅
5. **Slice 5** — the convention **hub** (all slots, live "live now / up next / done", deep-links into each scenario). ✅ All **six** Continuum 2026 slots ported and scheduled with real times.
6. **Later** — native iPad wrapper. (Everything else in the design is built: all §5 tray tools — dice, NPC, art with online Generate, clue-net, cast, break timer, parking-lot, wake-lock — plus the con hub, six ported scenarios, and the markdown → scenario-data generator.)
6. **Native iPad wrapper** — a Capacitor iOS shell around the PWA is scaffolded (config, scripts, integration). Generate + build it on a Mac with Xcode + CocoaPods: see [`docs/native-ipad-wrapper.md`](docs/native-ipad-wrapper.md). ✅ (config) / ⬜ (iOS build needs the toolchain)
**The full design is now built.** All §5 tray tools (dice with 6 packs, NPC, art with online Generate, clue-net, cast, break timer, parking-lot, wake-lock), the con hub, six ported Continuum scenarios, the markdown → scenario-data generator, and the native-wrapper scaffold.
### Online art "Generate"
+14
View File
@@ -0,0 +1,14 @@
{
"appId": "uk.timevans.thedirector",
"appName": "The Director",
"webDir": "dist",
"backgroundColor": "#0b0e13",
"ios": {
"contentInset": "always",
"backgroundColor": "#0b0e13",
"preferredContentMode": "mobile"
},
"server": {
"iosScheme": "capacitor"
}
}
+73
View File
@@ -0,0 +1,73 @@
# The Director — native iPad wrapper (Capacitor)
The app ships as an offline-first PWA; this wraps that same build in a thin native
iOS shell (a WKWebView) so it can be installed from Xcode / TestFlight and get a
real home-screen app, splash screen, and App Store path. **No app logic changes** —
Capacitor serves the `dist/` build locally on the device.
The Capacitor **config, scripts, and integration are committed**. Generating and
building the actual iOS project must be done on a Mac with the toolchain below —
it cannot be produced without Xcode + CocoaPods.
## Prerequisites (on the Mac)
- **Full Xcode** (not just Command Line Tools). Then point the toolchain at it:
`sudo xcode-select -s /Applications/Xcode.app/Contents/Developer` and accept the
licence (`sudo xcodebuild -license accept`).
- **CocoaPods**: `brew install cocoapods` (or `sudo gem install cocoapods`).
- Node ≥ 20 (Capacitor 8) — already required to build the web app.
Check: `xcodebuild -version` and `pod --version` should both print a version.
## One-time: generate the iOS project
```bash
npm install # pulls @capacitor/core, cli, ios (devDependencies)
npm run ios:add # = vite build → npx cap add ios (runs pod install)
```
This creates `ios/` (an Xcode project under `ios/App`). Commit it — it is the native
project source. Generated/derived paths inside it are git-ignored (see `.gitignore`).
App icons + splash (once): drop a 1024×1024 source at `resources/icon.png` and a
splash at `resources/splash.png`, then `npx @capacitor/assets generate --ios`.
## Every build after a web change
```bash
npm run ios:sync # = vite build → npx cap sync ios (copies fresh web assets + plugins)
npm run ios:open # opens ios/App/App.xcworkspace in Xcode
# or in one step:
npm run ios
```
In Xcode: pick your Team (Signing & Capabilities), choose an iPad simulator or a
connected iPad, and Run (⌘R). For a device/TestFlight build you need an Apple
Developer account and a signing profile.
## Config
[`capacitor.config.json`](../capacitor.config.json):
- `appId` — **`uk.timevans.thedirector`**. Change this *before* the first `ios:add`
(changing the bundle id after generation means regenerating or editing Xcode).
- `webDir: dist` — the vite output the shell serves.
- `ios.contentInset: always` + `viewport-fit=cover` in `index.html` handle the safe
areas (notch / home indicator) so the Director Rail sits correctly.
- `backgroundColor` matches the app theme so there is no white flash on launch.
## iOS-specific notes
- **Offline**: fully native — the web assets are bundled in the app, so it works with
no network regardless of the service worker. The browser PWA still uses `sw.js`; the
wrapper **skips SW registration** (`window.Capacitor.isNativePlatform()` in `main.js`)
to avoid a redundant SW under the `capacitor://` scheme.
- **Keep-awake (💡)**: `navigator.wakeLock` is unreliable inside WKWebView. For a solid
screen-on toggle in the native build, add
[`@capacitor-community/keep-awake`](https://github.com/capacitor-community/keep-awake)
and call `KeepAwake.keepAwake()` / `.allowSleep()` from the shell's wake toggle when
`window.Capacitor` is present. The current toggle degrades gracefully until then.
- **Online art "Generate"**: works the same — it calls the GM-configured endpoint over
the network when connected.
- **Orientation**: the manifest requests landscape; set the same in Xcode
(Deployment Info → iPad → Landscape only) so it matches at the table.
@@ -97,6 +97,12 @@ jsdom. No new dependencies. Fully offline.
(`gmd.art.generated`, capped 24). No endpoint/key hardcoded — the GM pastes their gen URL. Tests:
`generate.test.js`, `library.test.js`, `art-tray-generate.test.js`. Network never load-bearing.
## Still deferred (backlog)
- **Native iPad wrapper scaffolded (Capacitor).** `capacitor.config.json` (webDir `dist`, appId
`uk.timevans.thedirector`, iOS safe-area/theme), `@capacitor/{core,cli,ios}` as devDependencies
(web app keeps zero runtime deps), `ios:add`/`ios:sync`/`ios:open` scripts, `main.js` skips the SW
under Capacitor, iOS `<meta>` hints, `.gitignore` for native build output, and
`docs/native-ipad-wrapper.md` (the Xcode + CocoaPods build guide). `tests/tools/capacitor-config.test.js`
guards the config. The actual iOS project generation/build needs a Mac with full Xcode + CocoaPods
(not available in this env) and is documented for the user to run.
- Native iPad app wrapper (the only remaining design item; a side project).
## Done — the design is fully built.
+4
View File
@@ -7,6 +7,10 @@
<link rel="stylesheet" href="/src/styles.css" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0b0e13" />
<!-- iOS home-screen / native-wrapper hints -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="The Director" />
</head>
<body>
<gm-shell></gm-shell>
+1099
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -9,9 +9,16 @@
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"gen:scenario": "node tools/md-to-scenario.mjs"
"gen:scenario": "node tools/md-to-scenario.mjs",
"ios:add": "npm run build && cap add ios",
"ios:sync": "npm run build && cap sync ios",
"ios:open": "cap open ios",
"ios": "npm run build && cap sync ios && cap open ios"
},
"devDependencies": {
"@capacitor/cli": "^8.4.2",
"@capacitor/core": "^8.4.2",
"@capacitor/ios": "^8.4.2",
"jsdom": "^24.0.0",
"vite": "^5.2.0",
"vitest": "^1.6.0"
+5 -1
View File
@@ -11,7 +11,11 @@ if (typeof document !== 'undefined' && document.querySelector('gm-shell')) {
});
}
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
// Register the service worker for the browser PWA. Skipped inside the native
// iOS wrapper (Capacitor), where assets are already bundled locally and a SW
// under the capacitor:// scheme is unnecessary and can conflict.
const inCapacitor = typeof window !== 'undefined' && !!window.Capacitor?.isNativePlatform?.();
if (!inCapacitor && typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
});
+36
View File
@@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
const cfg = JSON.parse(readFileSync('capacitor.config.json', 'utf8'));
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
describe('Capacitor iOS wrapper', () => {
it('has a reverse-DNS appId and an app name', () => {
expect(cfg.appId).toMatch(/^[a-z]+(\.[a-z0-9]+)+$/);
expect(cfg.appName).toBeTruthy();
});
it('points webDir at the vite build output (dist)', () => {
expect(cfg.webDir).toBe('dist');
});
it('exposes the iOS build scripts', () => {
for (const s of ['ios:add', 'ios:sync', 'ios:open']) {
expect(pkg.scripts[s], s).toBeTruthy();
}
expect(pkg.scripts['ios:sync']).toContain('build'); // always sync fresh web assets
});
it('declares the Capacitor toolchain as dev-only (web app keeps zero runtime deps)', () => {
expect(pkg.dependencies ?? {}).toEqual({});
for (const d of ['@capacitor/core', '@capacitor/cli', '@capacitor/ios']) {
expect(pkg.devDependencies[d], d).toBeTruthy();
}
});
it('skips the service worker inside the native wrapper', () => {
const main = readFileSync('src/main.js', 'utf8');
expect(main).toContain('isNativePlatform');
expect(main).toContain("register('/sw.js')"); // still registers for the browser PWA
});
});