From 9fa549a9e335fdf03af640965c4789f651ffe850 Mon Sep 17 00:00:00 2001 From: Periicles Date: Mon, 17 Aug 2026 00:11:24 +0200 Subject: [PATCH 1/3] build(scripts): add the landing page screenshot capture The crop region comes from the panel's real window via CGWindowList, not from a constant, so it cannot drift from the app's geometry. Two modes because screencapture -R needs Screen Recording permission that a terminal does not have by default: capture directly when it is granted, or crop a full-screen shot taken with Cmd-Shift-5, which needs nothing. The crop maths derives pixels-per-point from the screenshot's own width rather than assuming the backing scale. --- scripts/capture-hero.sh | 107 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100755 scripts/capture-hero.sh diff --git a/scripts/capture-hero.sh b/scripts/capture-hero.sh new file mode 100755 index 0000000..d4fe0fe --- /dev/null +++ b/scripts/capture-hero.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Captures the hero screenshot used on the landing page (docs/assets/hero.png). +# +# Usage: +# capture-hero.sh capture directly, after a countdown +# capture-hero.sh --from crop an existing full-screen screenshot +# capture-hero.sh --delay 8 seconds before the direct capture fires +# +# NotchBar must be running: the crop region is read from the panel's real window +# rather than hardcoded, so it cannot drift from the app's geometry. +# +# The direct mode needs Screen Recording permission for your terminal (System +# Settings > Privacy & Security > Screen Recording). Without it, screencapture +# fails with "could not create image from rect" — use --from instead: take a +# full-screen shot with Cmd-Shift-5 (Options > Timer, so the panel stays open), +# then pass the file. That path needs no permission at all. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="$ROOT/docs/assets/hero.png" +DELAY=6 +FROM="" + +while [ $# -gt 0 ]; do + case "$1" in + --from) FROM="${2:-}"; shift 2 ;; + --delay) DELAY="${2:-}"; shift 2 ;; + *) echo "error: unknown argument '$1'" >&2; exit 1 ;; + esac +done + +# Margin around the panel, in points: enough menu bar on either side for the +# notch to read as a notch, and a little desktop underneath. +SIDE_MARGIN=150 +BOTTOM_MARGIN=40 + +read -r X Y W H SCREEN_W SCALE < ($1["Width"] ?? 0) } + +guard let panel = panels.first, let pw = panel["Width"], let ph = panel["Height"] else { + FileHandle.standardError.write(Data("error: NotchBar is not running\n".utf8)) + exit(1) +} + +// Window bounds are already top-left origin, and the panel bleeds 2pt above the +// screen, so clamp the top edge to 0. +let x = max((panel["X"] ?? 0) - sideMargin, 0) +let y = max(panel["Y"] ?? 0, 0) +let width = min(pw + sideMargin * 2, screen.frame.width - x) +let height = ph + bottomMargin + +print("\(Int(x)) \(Int(y)) \(Int(width)) \(Int(height)) \(Int(screen.frame.width)) \(Int(screen.backingScaleFactor))") +SWIFT +) +EOF + +echo "==> Region: ${W}x${H} pt at (${X},${Y}) — screen ${SCREEN_W}pt @${SCALE}x" +mkdir -p "$(dirname "$OUT")" + +if [ -n "$FROM" ]; then + [ -f "$FROM" ] || { echo "error: no such file: $FROM" >&2; exit 1; } + + SHOT_W=$(sips -g pixelWidth "$FROM" | awk '/pixelWidth/{print $2}') + # A full-screen shot spans the screen, so its own width gives the true + # pixel-per-point ratio — more reliable than assuming the backing scale. + RATIO=$(awk -v a="$SHOT_W" -v b="$SCREEN_W" 'BEGIN{printf "%.6f", a/b}') + echo "==> Cropping $FROM (${SHOT_W}px wide, ${RATIO} px/pt)" + + px() { awk -v v="$1" -v r="$RATIO" 'BEGIN{printf "%d", v*r+0.5}'; } + sips -c "$(px "$H")" "$(px "$W")" --cropOffset "$(px "$Y")" "$(px "$X")" \ + "$FROM" --out "$OUT" >/dev/null +else + echo "==> Capturing in ${DELAY}s — hover the notch now and keep the cursor on it" + screencapture -T "$DELAY" -x -R "$X,$Y,$W,$H" "$OUT" || { + echo "error: screencapture failed. Grant Screen Recording to your terminal," >&2 + echo " or take a full-screen shot with Cmd-Shift-5 and pass --from." >&2 + exit 1 + } +fi + +OUT_W=$(sips -g pixelWidth "$OUT" | awk '/pixelWidth/{print $2}') +OUT_H=$(sips -g pixelHeight "$OUT" | awk '/pixelHeight/{print $2}') +echo "==> Done: $OUT (${OUT_W}x${OUT_H}px)" +[ "$OUT_W" -lt 1000 ] && echo "warning: under 1000px wide — it will look soft on a retina display" >&2 +echo " Commit it, and the landing page picks it up automatically." +exit 0 From a4ec7b4103862e7df1fd83e770becb04f13aa58d Mon Sep 17 00:00:00 2001 From: Periicles Date: Mon, 17 Aug 2026 00:11:24 +0200 Subject: [PATCH 2/3] docs(site): show a real screenshot when one exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero is a CSS mockup, which is honest but not evidence. The image is hidden in the markup and revealed by script only once it has actually loaded, so a missing docs/assets/hero.png costs nothing: no broken image, no layout hole, and the mockup stays as the fallback — including with JavaScript off. --- docs/index.html | 1 + docs/main.js | 10 ++++++++++ docs/style.css | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/docs/index.html b/docs/index.html index b181ba6..0cdfdad 100644 --- a/docs/index.html +++ b/docs/index.html @@ -33,6 +33,7 @@

are we done yet?

macOS 14+ · Apple Silicon · free & open source

+
diff --git a/docs/main.js b/docs/main.js index 443da0d..c8c51b1 100644 --- a/docs/main.js +++ b/docs/main.js @@ -7,6 +7,8 @@ fr: "Ton notch répond à la question. La progression de ton cours, là où tu regardes déjà." }, "cta.download":{ en: "Download for Mac", fr: "Télécharger pour Mac" }, "meta.reqs": { en: "macOS 14+ · Apple Silicon · free & open source", fr: "macOS 14+ · Apple Silicon · gratuit & open source" }, + "hero.shot": { en: "NotchBar expanded in the notch, showing the current event's progress", + fr: "NotchBar déployé dans le notch, montrant la progression de l'événement en cours" }, "mock.title": { en: "Organic Chemistry", fr: "Chimie organique" }, "mock.left": { en: "23 min left", fr: "23 min restantes" }, "feat.heading":{ en: "What it does", fr: "Ce que ça fait" }, @@ -90,5 +92,13 @@ b.addEventListener("click", function () { apply(b.getAttribute("data-lang")); }); }); + // Swap the CSS mock for the real screenshot, but only once it has loaded: + // a missing asset then costs nothing instead of showing a broken image. + var shot = document.querySelector(".hero-shot"); + if (shot) { + shot.addEventListener("load", function () { shot.hidden = false; }); + if (shot.complete && shot.naturalWidth > 0) shot.hidden = false; + } + apply(resolveLang()); })(); diff --git a/docs/style.css b/docs/style.css index c30486a..6ec4e16 100644 --- a/docs/style.css +++ b/docs/style.css @@ -30,6 +30,13 @@ header.site,footer.site{max-width:var(--max);margin:0 auto;padding:22px 24px; .hero h1{font-size:clamp(44px,8vw,92px);font-weight:850;letter-spacing:-.045em;line-height:.9;margin:0 0 20px} .hero .sub{color:var(--muted);font-size:17px;max-width:42ch;margin:0 0 24px} .hero-stage{display:flex;align-items:center} +/* The real screenshot, revealed by main.js only once it actually loads. Until + docs/assets/hero.png exists — or with JS off — it stays hidden and the CSS + mock below is what the page shows. No broken image, ever. */ +.hero-shot{width:100%;height:auto;border-radius:14px; + box-shadow:0 30px 60px -34px rgba(0,0,0,.6)} +.hero-shot:not([hidden]){display:block} +.hero-shot:not([hidden]) + .desktop{display:none} .desktop{position:relative;width:100%;border-radius:14px 14px 10px 10px;overflow:hidden; background:radial-gradient(120% 90% at 20% -10%,#1a2340 0,rgba(26,35,64,0) 55%), From af333d8bfc03ec61916e8b0ddc75a47aa5708c3b Mon Sep 17 00:00:00 2001 From: Periicles Date: Mon, 17 Aug 2026 00:11:24 +0200 Subject: [PATCH 3/3] docs(readme): document refreshing the landing page screenshot --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 7db229a..0a306b0 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,26 @@ swiftlint The CI pipeline runs `swiftlint` on every PR. Fix all errors before pushing. +**Refreshing the landing page screenshot** + +`docs/assets/hero.png` is the shot at the top of the [website](https://periicles.github.io/Notchapp/). Until it exists the page falls back to a CSS mockup, so the site is never broken by a missing file. + +Run NotchBar, open a calendar event so the panel shows the **in progress** state — title, times and progress bar, which is the whole point of the shot — then: + +```sh +scripts/capture-hero.sh # counts down, then captures; hover the notch and hold +``` + +The crop region is read from the panel's real window, so it cannot drift from the app's geometry. + +Direct capture needs Screen Recording permission for your terminal. Without it `screencapture` fails with *could not create image from rect* — take a full-screen shot instead (**⌘⇧5 → Options → Timer**, so the panel stays open while it fires) and crop that, which needs no permission: + +```sh +scripts/capture-hero.sh --from ~/Desktop/Screenshot*.png +``` + +Check what you are about to publish: the shot includes the menu bar on either side of the notch, and the panel shows real event titles. + ## Contributing **Branch naming**