| 1 | # π€ Marketing screenshots β native Mac + iPhone captures for the hero collage |
| 2 | |
| 3 | How we capture the native **Mac** and **iPhone** screenshots that sit in the |
| 4 | marketing site's three-device hero collage (alongside a web frame). This is the |
| 5 | committed, hand-operable form of a capture recipe that previously lived only in |
| 6 | a throwaway branch and agent memory β the "outlive-AI" rule in action: anyone, |
| 7 | human or agent, can stand the demo up and reproduce the shots from this doc plus |
| 8 | `tools/marketing-shots/`, with no special knowledge encoded elsewhere. |
| 9 | |
| 10 | The whole flow is fictional and local. Nothing here ever points an app at a |
| 11 | real or production instance, and the Mac app is built in an **isolated identity** |
| 12 | so it can only ever show the seeded demo space β never anyone's real signed-in |
| 13 | spaces (see *Isolation* below β it is the load-bearing safety property). |
| 14 | |
| 15 | The tool stands up the demo and points you here; the interactive captures |
| 16 | themselves stay **manual** on purpose (a person frames and takes each shot). |
| 17 | |
| 18 | ## Quick start (`./tools/dev shots`) |
| 19 | |
| 20 | ```sh |
| 21 | ./tools/dev shots up # Terminal 1 β build the isolated demo Mac app (if |
| 22 | # needed) + run a throwaway local server seeded with |
| 23 | # the fictional "The Commons" space. Foreground: |
| 24 | # prints the local URL + a localhost sign-in link; |
| 25 | # Ctrl-C stops it. |
| 26 | ./tools/dev shots tunnel # Terminal 2 β give the local server a real public |
| 27 | # https try-*.11relay.com address (via `lchat try |
| 28 | # --local`) + single-use Mac & iPhone sign-in links. |
| 29 | ./tools/dev shots mac # Launch the isolated "Eleven Demo" Mac app. |
| 30 | ./tools/dev shots web-check shot.png # Headless 2x screenshot of the seeded |
| 31 | # space, to eyeball the look before capturing native. |
| 32 | ``` |
| 33 | |
| 34 | Implementation lives in `tools/marketing-shots/` (`serve.sh`, `tunnel.sh`, |
| 35 | `seed-team.mjs`, `demo-lib.mjs`, `device-link.mjs`, `web-check.mjs`) β the |
| 36 | `shots` verbs are thin wrappers, mirroring how `./tools/dev up` wraps |
| 37 | `tools/lchat-dev-up.mjs`. Runtime artifacts (the demo DB, state, PNGs) are |
| 38 | gitignored. |
| 39 | |
| 40 | ## The demo space: "The Commons" |
| 41 | |
| 42 | A fictional civic-tech team space that matches the web hero frame |
| 43 | (`website/src/assets/hero/team-web.png`). It's seeded **through the real E2EE |
| 44 | protocol** (`demo-lib.mjs` provisions each member via an admin key-invite β claim |
| 45 | β pubkey, then encrypts every thread/message client-side exactly as the browser |
| 46 | does) β the server only ever holds ciphertext, so there is no plaintext |
| 47 | shortcut. Six threads: **The commons π±** (the flagship β the exact web-frame |
| 48 | conversation: Aoife / Marcus / Devi, a reply, and reactions), Standup, Shipping |
| 49 | π, Field notes π, Roadmap, Watercooler β. You sign in as **Cass** (your own |
| 50 | messages are the accent bubbles on the right, matching the web frame's reply). |
| 51 | |
| 52 | Eyeball it any time without a native build: |
| 53 | |
| 54 | ```sh |
| 55 | ./tools/dev shots web-check /tmp/commons.png && open /tmp/commons.png |
| 56 | ``` |
| 57 | |
| 58 | ## Isolation β why this is safe (load-bearing) |
| 59 | |
| 60 | A normal Mac build reads the **real** account. Both `LChatMac.entitlements` and |
| 61 | `LChatMac-Dev.entitlements` carry |
| 62 | `keychain-access-groups = $(AppIdentifierPrefix)com.elevenmessenger.messenger.shared`, |
| 63 | and a dev alt (`./tools/dev alt`) shares that same group **on purpose** β an alt |
| 64 | is the real account, not a sandbox, and has grabbed Paul's private space before. |
| 65 | That is exactly what we must NOT do for a marketing screenshot. |
| 66 | |
| 67 | So the demo Mac build is signed with **`ios/LChatMac/LChatMac-Demo.entitlements`** |
| 68 | β minimal by design (only `app-sandbox`, `network.client`, and |
| 69 | `files.user-selected.read-write`), with the shared keychain group (and the app |
| 70 | group, push, and Sparkle exceptions) **removed**. No shared keychain group β |
| 71 | the build has its **own empty keychain** and can only show the demo space you |
| 72 | add. It also gets a distinct bundle id, `com.elevenmessenger.messenger.mac.capdemo` |
| 73 | (its own sandbox container + Launch Services identity, so it runs beside the real |
| 74 | app), and the display name "Eleven Demo". The real purple `AppIcon` is kept on |
| 75 | purpose so the screenshot shows the real brand mark. |
| 76 | |
| 77 | **Crucially, none of this touches `project.yml`.** `./tools/dev shots` applies |
| 78 | the demo identity purely as xcodebuild **command-line build-setting overrides** |
| 79 | (`CODE_SIGN_ENTITLEMENTS=<abs demo path>`, |
| 80 | `PRODUCT_BUNDLE_IDENTIFIER=β¦mac.capdemo`, `LCHAT_MAC_DISPLAY_NAME=Eleven Demo`), |
| 81 | which outrank the target/config values in the generated project. A plain |
| 82 | `./tools/dev build mac` (or `xcodegen generate`) therefore still resolves the |
| 83 | normal entitlements (Release β `LChatMac.entitlements`, Debug β |
| 84 | `LChatMac-Dev.entitlements`) and the real `com.elevenmessenger.messenger.mac` |
| 85 | bundle id β the released app is never at risk. |
| 86 | |
| 87 | Verify the isolation on the built app any time β **no output means isolated**: |
| 88 | |
| 89 | ```sh |
| 90 | codesign -d --entitlements :- \ |
| 91 | "ios/build-demo/Build/Products/Debug/Eleven Demo.app" | grep -i keychain |
| 92 | # β (no output) = no keychain-access-groups = isolated, own empty keychain |
| 93 | ``` |
| 94 | |
| 95 | ## Capturing the Mac shot |
| 96 | |
| 97 | 1. `./tools/dev shots up` (Terminal 1), then `./tools/dev shots tunnel` |
| 98 | (Terminal 2) for a real https origin the native apps can reach. |
| 99 | 2. `./tools/dev shots mac` to launch "Eleven Demo". In the app: **Spaces βΈ Add |
| 100 | from linkβ¦** and paste the **Mac** link `shots tunnel` printed (signs you in |
| 101 | as Cass). If a "Keep your access safe / Sign in" card appears, dismiss it with |
| 102 | "Remind me later". |
| 103 | 3. **Leave the Mac on its default Classic light theme** (light background, blue |
| 104 | accent bubbles) β that is what `hero-mac.png` uses and what the collage wants |
| 105 | for the natives; do **not** switch it to Lit Room (see *Theme pairing*). Open |
| 106 | **The commons π±** to mirror the web frame, or **Standup** for variety (it has |
| 107 | an own-bubble + a reaction). |
| 108 | 4. **Capture full-screen, then crop with PIL** β the robust path. Interactive |
| 109 | `screencapture -w` (click a window) and System Events window-bounds both rely |
| 110 | on Accessibility / Quartz (pyobjc), which isn't granted on the build Mac and |
| 111 | is unreliable when driven headlessly. Full-screen + crop needs none of that: |
| 112 | |
| 113 | ```sh |
| 114 | screencapture -o ~/Desktop/team-mac-raw.png # -o = no window shadow; full screen, 2x retina |
| 115 | ``` |
| 116 | |
| 117 | Crop the app-window region to the collage aspect (`hero-mac.png` is |
| 118 | **1300Γ919**, aspect β **1.415**) and scale to 1300Γ919: |
| 119 | |
| 120 | ```python |
| 121 | from PIL import Image |
| 122 | im = Image.open("team-mac-raw.png") # full-screen 2x capture |
| 123 | # Pick a crop box (left, top, right, bottom) around the window, keeping |
| 124 | # (right-left)/(bottom-top) β 1.415 so nothing is squashed on the resize. |
| 125 | box = (LEFT, TOP, RIGHT, BOTTOM) |
| 126 | im.crop(box).resize((1300, 919), Image.LANCZOS).save("team-mac.png") |
| 127 | ``` |
| 128 | |
| 129 | ## Capturing the iPhone shot β REAL DEVICE ONLY |
| 130 | |
| 131 | > β οΈ **Do NOT use the iOS Simulator.** The iOS 26.3.1 simulator renders emoji as |
| 132 | > tofu (β‘) β confirmed 2026-07-28, both in-app and in Safari. Use a real iPhone. |
| 133 | |
| 134 | 1. `./tools/dev shots tunnel` prints an **iPhone** link. Open it **on the |
| 135 | device** (paste into Safari β Open in the app / join) β it signs the phone in |
| 136 | as Cass. The phone is the real app, so it'll show this demo space alongside |
| 137 | any real spaces; scroll to / open "The Commons" for the shot. |
| 138 | 2. Land on the space's **thread list** (the frame in `hero-ios.png`: title + the |
| 139 | six thread rows with previews). |
| 140 | 3. Screenshot on the device (**Volume-Up + Side button**) and AirDrop it to the |
| 141 | Mac. An iPhone 16 Pro shot is **1206Γ2622** (aspect β 0.460). |
| 142 | |
| 143 | ### Reusing the existing bezel frame |
| 144 | |
| 145 | The website's `hero-ios.png` (**760Γ1554**, RGBA, transparent surround) already |
| 146 | has the Apple bezel baked in. Rather than re-render a device frame, drop the new |
| 147 | screenshot into that same bezel. Its screen rect is **(41, 39)** with size |
| 148 | **678Γ1476** (aspect β 0.459 β the raw 1206Γ2622 shot's 0.460 is a near-perfect |
| 149 | match, so scaling barely distorts), corners rounded at **r β 92px**. PIL only |
| 150 | (numpy is **not** installed on the build Mac): |
| 151 | |
| 152 | ```python |
| 153 | from PIL import Image, ImageDraw |
| 154 | |
| 155 | BEZEL = "hero-ios.png" # 760Γ1554, Apple bezel baked in, transparent surround |
| 156 | RAW = "team-ios-raw.png" # on-device screenshot (e.g. 1206Γ2622, iPhone 16 Pro) |
| 157 | OUT = "team-ios.png" |
| 158 | |
| 159 | X, Y, W, H, R = 41, 39, 678, 1476, 92 # screen rect + corner radius in the bezel |
| 160 | |
| 161 | frame = Image.open(BEZEL).convert("RGBA") |
| 162 | shot = Image.open(RAW).convert("RGBA").resize((W, H), Image.LANCZOS) |
| 163 | |
| 164 | # Rounded-rect mask so the shot's corners tuck under the bezel's screen cutout. |
| 165 | mask = Image.new("L", (W, H), 0) |
| 166 | ImageDraw.Draw(mask).rounded_rectangle([0, 0, W, H], radius=R, fill=255) |
| 167 | |
| 168 | frame.paste(shot, (X, Y), mask) |
| 169 | frame.save(OUT) |
| 170 | ``` |
| 171 | |
| 172 | You may **read** `website/src/assets/hero/hero-ios.png` as the reference bezel, |
| 173 | but the composite output belongs in the website repo β do not modify the website |
| 174 | repo from a messenger-repo change. |
| 175 | |
| 176 | ## Theme pairing |
| 177 | |
| 178 | The collage deliberately pairs a **warm** web frame (Lit Room cream) with |
| 179 | **lighter** natives (Classic light / blue). So only the **web** frame is Lit |
| 180 | Room; **both natives stay on the default Classic light** (blue accent) β the |
| 181 | Mac app's out-of-the-box theme, matching `hero-mac.png` and the iPhone. Don't |
| 182 | switch the Mac to Lit Room, or all three frames read as the same cream wash. |
| 183 | |
| 184 | ## Where the outputs go |
| 185 | |
| 186 | The finished frames live in the **website** repo (not this one): |
| 187 | |
| 188 | - `website/src/assets/hero/team-web.png`, `team-mac.png`, `team-ios.png` β wired |
| 189 | into the homepage `.collage` in `website/src/index.njk`. |
| 190 | - The original `hero-web.png` / `hero-mac.png` / `hero-ios.png` family (a |
| 191 | household space) is used on `/families/`. |
| 192 | |
| 193 | Committing and deploying the website is a separate, website-repo concern (its |
| 194 | own deploy path) β out of scope for this kit, which only produces the raw + framed |
| 195 | images. |
| 196 | |
| 197 | ## Known cosmetic gaps (not seed bugs) |
| 198 | |
| 199 | - **"6 members Β· N online":** the header's *online* count reflects who is really |
| 200 | connected (you), so it reads "6 members" or "6 members Β· 1 online", not the |
| 201 | web frame's mocked "3 online". Faking more would need extra live connections β |
| 202 | not worth it for a still. |
| 203 | - **Thread-list times** all read the same recent minute (the server stamps |
| 204 | `created_at` at seed time). In-thread times match the web ("Today 9:12 AM") |
| 205 | because those come from the encrypted payload. |
| 206 | |
| 207 | ## Teardown |
| 208 | |
| 209 | - Ctrl-C the tunnel (Terminal 2) to retire the public address, then Ctrl-C the |
| 210 | server (Terminal 1). The demo DB is wiped on the next `shots up` anyway. |
| 211 | - Quit "Eleven Demo". |
| 212 | - Optional: `rm -rf ios/build-demo` and the gitignored |
| 213 | `tools/marketing-shots/.demo*` runtime files when the collage is done. |
| 214 | |