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:
co-authored by
Claude Opus 4.8
parent
028dcb5e4e
commit
e79f643409
+11
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+1099
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -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
@@ -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(() => {});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user