The top-level layouts/ directory does not exist; the wrappers live in library/layouts/. Six references across four docs named the phantom path, including a copy-pasteable cd command and a README-longform link that resolved to docs/layouts/README.md (a 404). how-to.md taught bunx for the iOS scaffold. bunx is not on PATH here, which is the exact failure package.json already documents in cap:sync; point both at the local bin. useMapProvider.js's own header listed three resolution rungs, omitting ?provider= and misnumbering window.MAP_PROVIDER. The function implements four. how-to.md was correct.
4.4 KiB
Architecture — app wrappers
The simple overview is in
library/layouts/README.md. This is the full contract for building and wiring an app wrapper.
Each folder in library/layouts/ is an app wrapper: a host shell that composes doxa-maps plugin-designs
(the independently-built custom elements) into one application.
Terminology (deliberate — don't drift)
| Thing | What it is |
|---|---|
| app-profile | An independently-built, self-contained custom element (a map, a dashboard, a list). These are the micro-frontends. |
| app wrapper (this folder) | The host/shell that mounts plugin-designs, provides nav/chrome/routing, and bounds resources. Not itself a micro-frontend — it's the container. |
A wrapper is a host page, not a regular app-profile. Keeping it a host (rather than a bundle nested inside a bundle) avoids the relative-path/base-URL resolution problem that nesting one position-independent bundle inside another would create.
Wrappers today
app-wrapper-material/— Material-styled wrapper. Structure/behaviour modelled on the Quasar YouTube layout gallery; conventional theming + nav model (--ui-*CSS custom properties, aSidebarNavItem-shaped tree). Two-mode sidebar, self-hosted icons.pane-wrapper/— a second wrapper beside it.
The sidebar contract (nav.js)
NAV is an ordered list. Order = render order, top to bottom. Three kinds:
item— a single entry. At top level these are loose items in no group.section— an always-open labelled group with an icon + name (the YouTube "guide" look).folder— a collapsible group (open: falseby default).
Every entry carries target: 'both' | 'desktop' | 'mobile'.
navForTarget(NAV, target) filters the tree, and is used by both:
- the UI — hides non-matching entries, and
- the packager — the mobile/Capacitor build copies fewer bundles into
webDir.
That's why a Desktop only folder both disappears on phones and is excluded from the app binary.
Sidebar modes (intentionally different per platform)
| Collapsed | Expanded | |
|---|---|---|
| Desktop (≥1024px) | icon rail stays visible (labels hidden) | full width: icons + labels + section headers |
| Mobile (<1024px) | completely hidden (saves space) | overlay drawer + scrim; choosing a profile closes it |
Icons (icons.js)
icons.js ships inline SVG — no CDN, no icon font, no Google/YouTube request. This is a hard
requirement, not a preference: F-Droid builds from source and rejects proprietary blobs / non-free
CDN dependencies, and local icons also mean the wrapper works offline and inside Capacitor from
local assets. Paths follow the Lucide idiom (24×24, 2px stroke, round caps).
Local staging
cd library/layouts/app-wrapper-material
python3 -m http.server 4173 --bind 0.0.0.0
Then open it and narrow the window below 1024px (or use the browser device toolbar) to check the mobile behaviour.
The PROFILES registry (the wrapper's map catalog)
Beside nav.js, the wrapper's index.html declares PROFILES — one entry per mountable map:
const PROFILES = {
'prayer-progress': {
tag: 'doxa-map', // the element to mount (a bundle may register several)
src: './doxa-maps/doxa-simple-map/doxa-simple-map.js', // the bundle IIFE, tree-relative
views: ['map'], // which sub-views (map/list/tree) this profile offers
// + the profile-config the wrapper passes (copied from the generated instance page's BASE_CONFIG)
},
}
nav.js ids route to PROFILES keys; the wrapper lazy-loads src on first visit and mounts
tag. Gotchas: an instance page's tag may differ from the bundle name (doxa-simple-map
registers <doxa-map> too), and colorStrategy values are registry keys derived from
FILENAMES (prayer-progress.js → 'prayerProgress').
How a wrapper becomes the served SPA
bun run shell (internal/install-shell.mjs) copies library/layouts/<wrapper>/ to the root of
public/, so public/index.html is the shell and public/doxa-maps/… are the bundles it
mounts — one served tree, two roles. Copy (not symlink) keeps Railway/Capacitor/file:// happy;
--no-token omits the local-only token.local.js (mandatory for release builds). Capacitor
also requires this root index.html (cap copy hard-fails without it).