A book is the use case Margin was built for, not the limit of it. Blog posts published straight to
the web are coming, and "New book" on the shelf is the wrong invitation for that. Project covers
both without promising anything the app does not do.
This is user-visible strings only. Internal identifiers keep saying book, because renaming bookId
and setBooks is churn nobody reads.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
Five 2560x1600 frames. The app window inside each is a CSS rebuild of the real interface rather
than a bitmap, laid out at the app's own 1440x900 and scaled once, so the type is real text at
render time instead of a resampled capture. The only Macs to hand are 1x, and an upscaled screen
capture looks like one.
The website's own AppWindow mockup was the obvious source and turned out to be wrong in several
places: the sidebar is 248px not 196, the proofing marks are straight underlines rather than wavy,
and the trim default differs. The rebuild follows shared/css/tokens.css and src/styles/app.css
instead, so these are more accurate than the marketing imagery.
Sample prose is original and written for this listing, since it goes in a public store page.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
The listing copy lives in text files rather than in a web form, so changing a description is a diff
someone can read and the store listing is reviewable next to the code it describes. Field lengths
are checked before sending, because Apple rejects an over-length field with an error that never
names the limit.
apple-provision.rb drives the Developer Portal through spaceship and is find-or-create throughout.
That matters most for the Developer ID certificate: an account may hold only a handful, they cannot
be un-revoked, and every copy of the app already signed by one stops verifying if it goes away.
Both scripts pin the App Store Connect team. This Apple ID can see more than one, and the other
belongs to somebody else entirely, so letting spaceship choose is how a listing ends up on the
wrong account.
The reviewer phone number and email are deliberately not in here. Apple requires a real number and
this repo is public.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
Tauri has no App Store target, so mas-package.sh covers the distance between the .app and something
App Store Connect will take. The order is load-bearing: the provisioning profile goes in before
codesign runs because the signature covers it, which is also why Tauri's own signing is switched
off for this build.
The sandbox costs three things, all Apple's rules rather than choices. The updater is gone, and
falls out for free because the plugin was already conditional on the config declaring it; only the
release overlay does. The Check for Updates menu item is gated on the same condition, since a menu
item that errors when clicked is its own rejection risk. The library moves into the container, and
the system spelling dictionary becomes unreadable.
Two things the first upload taught us. The profile is kept owner-only where it lives next to the
signing keys, and cp carried that mode into the bundle; Apple rejects a package containing anything
a non-root user cannot read. And altool exits 0 after printing UPLOAD FAILED, so its exit status
cannot be trusted and the transcript is the only reliable signal.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
An unsigned bundle on a current macOS opens to a malware warning with no obvious way past it, and
the way past it that does exist teaches people to click through exactly the warning worth reading.
The build now signs with a Developer ID certificate and notarizes with an App Store Connect API
key, which is also what does the App Store upload, so there is one credential to rotate.
The verification step is the point. codesign only says a signature is internally consistent;
spctl is what a person double-clicking the file actually meets, and it does not pass until the
notarization ticket is stapled.
A final job rewrites the version and sha256 in the tap's cask, using an SSH deploy key rather than
a token so a leak from a release job cannot reach the app repositories.
Also fixes Cargo.lock drifting a version behind on every release: the bump wrote Cargo.toml but
never staged the lock, so any fresh build dirtied the tree.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
The repo had no licence at all, which legally means all rights reserved. FSL grants free use for
anything except building a competing product, and each version becomes MIT two years after its
release. AGPL was the other candidate and is ruled out by the App Store, whose terms impose
restrictions the GPL forbids.
Cargo.toml still carried the Tauri scaffold defaults, and that description ends up in the deb and
rpm metadata.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
Move Check for Updates into the macOS app menu and add Settings (Cmd+,),
Save (Cmd+S), Find (Cmd+F), chapter navigation, and a Help > Report an
Issue link. Route new menu actions through menu-action events and trim
duplicate editor keybindings.
Replace native update dialogs with an in-app modal that shows a live
download progress bar and installing state, so updates no longer feel
abrupt. Drives the plugin's downloadAndInstall progress events through a
zustand store.
NSSpellChecker's implicit URL handling is unreliable for URLs alone on a
line (a bookmark list), flagging the whole URL as one misspelling. Request
the link checking type alongside spelling so the data detector claims URL
ranges, then keep only spelling results. Real misspellings are unaffected.
The bundled Hunspell dictionary (~49k SCOWL roots) lacked common words like
"cybersecurity", "scalable", and "assistantship", and checked words inside
URLs. On macOS, spelling now uses the system NSSpellChecker (same engine as
TextEdit/Safari): far better vocabulary, skips URLs, and respects words the
user has taught macOS. The custom/tech dictionaries and Harper grammar are
unchanged; non-macOS builds keep the Hunspell fallback.
include_str! embeds google-credentials.json at compile time, but the
file is gitignored, so CI checkouts had no file and the Rust crate
failed to compile on all three platforms. Write it from the
GOOGLE_CREDENTIALS secret, falling back to the committed example
placeholders so the build always succeeds.
Embed each bundled font the book uses (fetched in the webview) with its
own @font-face, point body text and headings at the chosen families, and
drop the hardcoded Literata injection in the packager. System fonts are
referenced by name with a serif fallback since they cannot be embedded.
Interpolate the book's body and heading families into the Typst
preamble, title page, cover, and contents. Embed the needed bundled
fonts and load any chosen system fonts (via fontdb) into the Typst
engine so PDF preview and export match the on-screen fonts.
Add a Typography section to book setup with one-click preset pairings,
an advanced body/heading picker, and a live sample. Enumerate installed
fonts via a new list_system_fonts Tauri command (fontdb) so they appear
alongside the bundled set, with a note that system fonts may not embed
in exports.
Split heading typography onto a new --font-heading token and set both
--font-book and --font-heading from the active book's fonts when it
opens or its setup changes. Declare @font-face for the new bundled
fonts.