From 4640e394edc4e10be243d2494b26c51be0f35fcb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 19:10:32 +0000 Subject: [PATCH 1/7] feat(ci): publish glucose-guard-design and dispatch TestFlight Create or update iDustbin/glucose-guard-design from the bundled iOS branding, enable 4. Build Loop, and start a TestFlight build on this branch. Skip LoopKit sync on feature branches so a test build keeps the Glucose Guard icons and name. Co-authored-by: iDustBin --- .github/workflows/bootstrap_glucose_guard.yml | 44 +++++++++++++++++ .github/workflows/build_loop.yml | 5 +- README.md | 12 +++-- Scripts/publish_glucose_guard_design.sh | 47 +++++++++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/bootstrap_glucose_guard.yml create mode 100755 Scripts/publish_glucose_guard_design.sh diff --git a/.github/workflows/bootstrap_glucose_guard.yml b/.github/workflows/bootstrap_glucose_guard.yml new file mode 100644 index 0000000000..6c5e7b82df --- /dev/null +++ b/.github/workflows/bootstrap_glucose_guard.yml @@ -0,0 +1,44 @@ +name: Bootstrap Glucose Guard +run-name: Bootstrap Glucose Guard (${{ github.ref_name }}) +on: + workflow_dispatch: + push: + branches: + - main + - cursor/glucose-guard-branding-05a7 + paths: + - "glucose-guard-design/**" + - "Scripts/publish_glucose_guard_design.sh" + - ".github/workflows/bootstrap_glucose_guard.yml" + +permissions: + contents: read + actions: write + +jobs: + publish-design-and-build: + runs-on: ubuntu-latest + env: + GH_PAT: ${{ secrets.GH_PAT }} + GH_TOKEN: ${{ secrets.GH_PAT }} + steps: + - name: Checkout Loop fork + uses: actions/checkout@v5 + + - name: Require GH_PAT + run: | + if [ -z "${GH_PAT}" ]; then + echo "Secret GH_PAT is missing. Add the same PAT used for Loop browser builds." >&2 + exit 1 + fi + + - name: Publish iDustbin/glucose-guard-design + run: ./Scripts/publish_glucose_guard_design.sh + + - name: Enable and start 4. Build Loop + run: | + set -euo pipefail + gh workflow enable "4. Build Loop" || true + gh workflow run "4. Build Loop" --ref "${GITHUB_REF_NAME}" + echo "Dispatched 4. Build Loop on ${GITHUB_REF_NAME}" + echo "That job applies icons + GlucoseGuard name from iDustbin/glucose-guard-design, then Fastlane/TestFlight." diff --git a/.github/workflows/build_loop.yml b/.github/workflows/build_loop.yml index 6e6a68da58..fb9dd59e8d 100644 --- a/.github/workflows/build_loop.yml +++ b/.github/workflows/build_loop.yml @@ -96,9 +96,10 @@ jobs: # This syncs any target branch to upstream branch of the same name - name: Sync upstream changes - if: | # do not run the upstream sync action on the upstream repository + if: | # skip on feature branches so a TestFlight test build is not reset to LoopKit steps.workflow-permission.outputs.has_permission == 'true' && - vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit' + vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit' && + (github.ref_name == 'main' || github.ref_name == 'dev') id: sync uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2 with: diff --git a/README.md b/README.md index 3e81f6b882..533e0d72fa 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ This fork builds **Glucose Guard** on top of [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). -This step is **iOS only**: GitHub Actions sync from upstream Loop, apply Glucose -Guard icons and the display name, then publish to the existing TestFlight -account. Branding lives in `glucose-guard-design/` (and later in -[iDustbin/glucose-guard-design](https://github.com/iDustbin/glucose-guard-design) -when that repo exists). There is no web or Kubernetes app in this step. +Two GitHub pieces, then one iOS build: + +1. This repo is already the fork of [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). +2. [iDustbin/glucose-guard-design](https://github.com/iDustbin/glucose-guard-design) holds logo, icons, and the **GlucoseGuard** display name. The **Bootstrap Glucose Guard** Action creates that repo (via `GH_PAT`) and then starts **4. Build Loop**. +3. Build Loop clones the design repo, applies branding, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. + +Run **Bootstrap Glucose Guard** (or push to this branch) after `GH_PAT` is set. Then install the new TestFlight build to see icon and name. For a local Xcode icon/name refresh after an upstream pull: diff --git a/Scripts/publish_glucose_guard_design.sh b/Scripts/publish_glucose_guard_design.sh new file mode 100755 index 0000000000..b26faacf25 --- /dev/null +++ b/Scripts/publish_glucose_guard_design.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Create or update iDustbin/glucose-guard-design from the bundled branding tree. +# Uses GH_PAT (never printed). Intended for GitHub Actions. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OWNER="${GLUCOSE_GUARD_DESIGN_OWNER:-iDustbin}" +REPO="${GLUCOSE_GUARD_DESIGN_REPO:-glucose-guard-design}" +SRC="${ROOT}/glucose-guard-design" + +if [[ -z "${GH_PAT:-}" ]]; then + echo "GH_PAT is unset. Add the Loop browser-build PAT as a repo secret." >&2 + exit 1 +fi + +if [[ ! -d "${SRC}/ios" ]]; then + echo "Missing ${SRC}/ios" >&2 + exit 1 +fi + +export GH_TOKEN="${GH_PAT}" +export GH_PROMPT_DISABLED=1 + +if ! gh repo view "${OWNER}/${REPO}" >/dev/null 2>&1; then + echo "Creating ${OWNER}/${REPO}" + gh repo create "${OWNER}/${REPO}" \ + --public \ + --description "Glucose Guard iOS branding: logo, icons, display name" \ + --disable-wiki +else + echo "${OWNER}/${REPO} already exists" +fi + +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT +cp -R "${SRC}/." "${work}/" +cd "${work}" +git init -b main +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" +git add -A +git commit -m "sync iOS branding from LoopWorkspace" + +git -c "http.extraHeader=Authorization: Bearer ${GH_PAT}" \ + push --force "https://github.com/${OWNER}/${REPO}.git" main + +echo "Published branding to ${OWNER}/${REPO}" From 85ae6d493b479bc8facad513bfa3f57e303f01b9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 19:11:28 +0000 Subject: [PATCH 2/7] fix(ci): publish branding to iDustbin/glucoseguard Use https://github.com/iDustbin/glucoseguard as the design repo and fix the authenticated git push so Bootstrap can write icons and the display name, then dispatch 4. Build Loop. Co-authored-by: iDustBin --- .github/workflows/bootstrap_glucose_guard.yml | 6 ++++-- .github/workflows/build_loop.yml | 2 +- README.md | 2 +- Scripts/apply_glucose_guard_branding.sh | 6 +++--- Scripts/publish_glucose_guard_design.sh | 12 +++++++----- glucose-guard-design/README.md | 5 +++-- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/bootstrap_glucose_guard.yml b/.github/workflows/bootstrap_glucose_guard.yml index 6c5e7b82df..33d272d83a 100644 --- a/.github/workflows/bootstrap_glucose_guard.yml +++ b/.github/workflows/bootstrap_glucose_guard.yml @@ -32,7 +32,9 @@ jobs: exit 1 fi - - name: Publish iDustbin/glucose-guard-design + - name: Publish iDustbin/glucoseguard + env: + GLUCOSE_GUARD_DESIGN_REPO: glucoseguard run: ./Scripts/publish_glucose_guard_design.sh - name: Enable and start 4. Build Loop @@ -41,4 +43,4 @@ jobs: gh workflow enable "4. Build Loop" || true gh workflow run "4. Build Loop" --ref "${GITHUB_REF_NAME}" echo "Dispatched 4. Build Loop on ${GITHUB_REF_NAME}" - echo "That job applies icons + GlucoseGuard name from iDustbin/glucose-guard-design, then Fastlane/TestFlight." + echo "That job applies icons + GlucoseGuard name from iDustbin/glucoseguard, then Fastlane/TestFlight." diff --git a/.github/workflows/build_loop.yml b/.github/workflows/build_loop.yml index fb9dd59e8d..5e80aab454 100644 --- a/.github/workflows/build_loop.yml +++ b/.github/workflows/build_loop.yml @@ -216,7 +216,7 @@ jobs: env: GH_PAT: ${{ secrets.GH_PAT }} GLUCOSE_GUARD_DESIGN_OWNER: iDustbin - GLUCOSE_GUARD_DESIGN_REPO: glucose-guard-design + GLUCOSE_GUARD_DESIGN_REPO: glucoseguard GLUCOSE_GUARD_DESIGN_REF: main run: ./Scripts/apply_glucose_guard_branding.sh diff --git a/README.md b/README.md index 533e0d72fa..942ae5ca85 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This fork builds **Glucose Guard** on top of [LoopKit/LoopWorkspace](https://git Two GitHub pieces, then one iOS build: 1. This repo is already the fork of [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). -2. [iDustbin/glucose-guard-design](https://github.com/iDustbin/glucose-guard-design) holds logo, icons, and the **GlucoseGuard** display name. The **Bootstrap Glucose Guard** Action creates that repo (via `GH_PAT`) and then starts **4. Build Loop**. +2. [iDustbin/glucoseguard](https://github.com/iDustbin/glucoseguard) holds logo, icons, and the **GlucoseGuard** display name. The **Bootstrap Glucose Guard** Action publishes that repo (via `GH_PAT`) and then starts **4. Build Loop**. 3. Build Loop clones the design repo, applies branding, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. Run **Bootstrap Glucose Guard** (or push to this branch) after `GH_PAT` is set. Then install the new TestFlight build to see icon and name. diff --git a/Scripts/apply_glucose_guard_branding.sh b/Scripts/apply_glucose_guard_branding.sh index fbf40a3bcf..0c5aca0e73 100755 --- a/Scripts/apply_glucose_guard_branding.sh +++ b/Scripts/apply_glucose_guard_branding.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Pull Glucose Guard branding from iDustbin/glucose-guard-design and apply +# Pull Glucose Guard branding from iDustbin/glucoseguard and apply # it to this LoopWorkspace checkout. Used after upstream LoopKit sync so # icons and the display name survive a fork reset. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DESIGN_OWNER="${GLUCOSE_GUARD_DESIGN_OWNER:-iDustbin}" -DESIGN_REPO="${GLUCOSE_GUARD_DESIGN_REPO:-glucose-guard-design}" +DESIGN_REPO="${GLUCOSE_GUARD_DESIGN_REPO:-glucoseguard}" DESIGN_REF="${GLUCOSE_GUARD_DESIGN_REF:-main}" LOCAL_DESIGN="${GLUCOSE_GUARD_DESIGN_PATH:-}" CLONE_DIR="" @@ -56,7 +56,7 @@ resolve_source() { return fi - echo "No Glucose Guard design source found. Create iDustbin/glucose-guard-design or set GLUCOSE_GUARD_DESIGN_PATH." >&2 + echo "No Glucose Guard design source found. Create iDustbin/glucoseguard or set GLUCOSE_GUARD_DESIGN_PATH." >&2 exit 1 } diff --git a/Scripts/publish_glucose_guard_design.sh b/Scripts/publish_glucose_guard_design.sh index b26faacf25..c2a7e72327 100755 --- a/Scripts/publish_glucose_guard_design.sh +++ b/Scripts/publish_glucose_guard_design.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# Create or update iDustbin/glucose-guard-design from the bundled branding tree. +# Create or update iDustbin/glucoseguard from the bundled branding tree. # Uses GH_PAT (never printed). Intended for GitHub Actions. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OWNER="${GLUCOSE_GUARD_DESIGN_OWNER:-iDustbin}" -REPO="${GLUCOSE_GUARD_DESIGN_REPO:-glucose-guard-design}" +REPO="${GLUCOSE_GUARD_DESIGN_REPO:-glucoseguard}" SRC="${ROOT}/glucose-guard-design" if [[ -z "${GH_PAT:-}" ]]; then @@ -20,6 +20,7 @@ fi export GH_TOKEN="${GH_PAT}" export GH_PROMPT_DISABLED=1 +export GIT_TERMINAL_PROMPT=0 if ! gh repo view "${OWNER}/${REPO}" >/dev/null 2>&1; then echo "Creating ${OWNER}/${REPO}" @@ -41,7 +42,8 @@ git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A git commit -m "sync iOS branding from LoopWorkspace" -git -c "http.extraHeader=Authorization: Bearer ${GH_PAT}" \ - push --force "https://github.com/${OWNER}/${REPO}.git" main +# Token only in the remote URL for this process; do not echo the URL. +git remote add origin "https://x-access-token:${GH_PAT}@github.com/${OWNER}/${REPO}.git" +git push --force origin main -echo "Published branding to ${OWNER}/${REPO}" +echo "Published branding to https://github.com/${OWNER}/${REPO}" diff --git a/glucose-guard-design/README.md b/glucose-guard-design/README.md index c97f3bf54d..27c826acbe 100644 --- a/glucose-guard-design/README.md +++ b/glucose-guard-design/README.md @@ -29,5 +29,6 @@ GLUCOSE_GUARD_DESIGN_PATH=/path/to/glucose-guard-design \ ./Scripts/apply_glucose_guard_branding.sh ``` -Until `iDustbin/glucose-guard-design` exists as its own GitHub repo, this tree -stays bundled inside the Loop fork so TestFlight still gets the Glucose Guard icon. +The GitHub source of truth is [iDustbin/glucoseguard](https://github.com/iDustbin/glucoseguard). +This folder is the copy that Bootstrap publishes there, and the local fallback +if the remote is not cloned yet. From bd6a634b48bc1bde9ef0fb9605af6beec0388b00 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 19:18:57 +0000 Subject: [PATCH 3/7] fix(ci): clone iDustbin/glucoseguard for TestFlight branding Use token-authenticated git clone of the design repo in CI and fail the build if that clone does not succeed, instead of silently falling back to the bundled copy. Co-authored-by: iDustBin --- .github/workflows/bootstrap_glucose_guard.yml | 1 + Scripts/apply_glucose_guard_branding.sh | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bootstrap_glucose_guard.yml b/.github/workflows/bootstrap_glucose_guard.yml index 33d272d83a..ad09bc53af 100644 --- a/.github/workflows/bootstrap_glucose_guard.yml +++ b/.github/workflows/bootstrap_glucose_guard.yml @@ -9,6 +9,7 @@ on: paths: - "glucose-guard-design/**" - "Scripts/publish_glucose_guard_design.sh" + - "Scripts/apply_glucose_guard_branding.sh" - ".github/workflows/bootstrap_glucose_guard.yml" permissions: diff --git a/Scripts/apply_glucose_guard_branding.sh b/Scripts/apply_glucose_guard_branding.sh index 0c5aca0e73..eb0029ee6b 100755 --- a/Scripts/apply_glucose_guard_branding.sh +++ b/Scripts/apply_glucose_guard_branding.sh @@ -29,25 +29,35 @@ resolve_source() { fi local bundled="${ROOT}/glucose-guard-design" - local url="https://github.com/${DESIGN_OWNER}/${DESIGN_REPO}.git" CLONE_DIR="$(mktemp -d)" + export GIT_TERMINAL_PROMPT=0 echo "Cloning ${DESIGN_OWNER}/${DESIGN_REPO}@${DESIGN_REF}" >&2 set +e if [[ -n "${GH_PAT:-}" ]]; then - git -c "http.extraHeader=Authorization: Bearer ${GH_PAT}" \ - clone --depth 1 --branch "${DESIGN_REF}" "${url}" "${CLONE_DIR}" + git clone --depth 1 --branch "${DESIGN_REF}" \ + "https://x-access-token:${GH_PAT}@github.com/${DESIGN_OWNER}/${DESIGN_REPO}.git" \ + "${CLONE_DIR}" else - git clone --depth 1 --branch "${DESIGN_REF}" "${url}" "${CLONE_DIR}" + git clone --depth 1 --branch "${DESIGN_REF}" \ + "https://github.com/${DESIGN_OWNER}/${DESIGN_REPO}.git" \ + "${CLONE_DIR}" fi local clone_status=$? set -e if [[ "${clone_status}" -eq 0 && -d "${CLONE_DIR}/ios" ]]; then + echo "Cloned https://github.com/${DESIGN_OWNER}/${DESIGN_REPO} @ $(git -C "${CLONE_DIR}" rev-parse --short HEAD)" >&2 printf '%s\n' "${CLONE_DIR}" return fi + # In CI the design repo must be used. Local Xcode can fall back to the bundle. + if [[ -n "${GH_PAT:-}" ]]; then + echo "Failed to clone https://github.com/${DESIGN_OWNER}/${DESIGN_REPO}. Not using the bundled fallback in CI." >&2 + exit 1 + fi + echo "Remote ${DESIGN_OWNER}/${DESIGN_REPO} is not available; using bundled glucose-guard-design." >&2 rm -rf "${CLONE_DIR}" CLONE_DIR="" From 8e1222777e6106038a43412ac27fa6c29603036d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 20:14:52 +0000 Subject: [PATCH 4/7] fix(branding): rename Home Screen label via InfoPlist.xcstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAIN_APP_DISPLAY_NAME was set but Loop still shows “Loop” because localized CFBundleDisplayName in InfoPlist.xcstrings overrides it. Rewrite those catalogs to GlucoseGuard after cloning iDustbin/glucoseguard. Co-authored-by: iDustBin --- Scripts/apply_glucose_guard_branding.sh | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Scripts/apply_glucose_guard_branding.sh b/Scripts/apply_glucose_guard_branding.sh index eb0029ee6b..e7d7e3f6cb 100755 --- a/Scripts/apply_glucose_guard_branding.sh +++ b/Scripts/apply_glucose_guard_branding.sh @@ -117,6 +117,58 @@ print(f"Set {replacement}") PY } +apply_localized_display_name() { + local name="$1" + python3 - "${ROOT}" "${name}" <<'PY' +import json +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +name = sys.argv[2] +keys = ("CFBundleDisplayName", "CFBundleName") +updated = 0 + +loop_root = root / "Loop" +if not loop_root.is_dir(): + print("Loop submodule is missing; cannot rewrite InfoPlist.xcstrings", file=sys.stderr) + sys.exit(1) + +for path in loop_root.rglob("InfoPlist.xcstrings"): + data = json.loads(path.read_text(encoding="utf-8")) + strings = data.get("strings") + if not isinstance(strings, dict): + continue + changed = False + for key in keys: + entry = strings.get(key) + if not isinstance(entry, dict): + continue + locs = entry.get("localizations") + if not isinstance(locs, dict): + continue + for loc in locs.values(): + if not isinstance(loc, dict): + continue + unit = loc.get("stringUnit") + if isinstance(unit, dict) and "value" in unit: + unit["value"] = name + changed = True + if changed: + path.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + updated += 1 + print(f"Set {name} in {path.relative_to(root)}") + +if updated == 0: + print("No Loop InfoPlist.xcstrings files were updated", file=sys.stderr) + sys.exit(1) +print(f"Updated CFBundleDisplayName in {updated} string catalog(s)") +PY +} + SRC="$(resolve_source)" copy_catalog \ "${SRC}/ios/OverrideAssetsLoop.xcassets" \ @@ -125,4 +177,21 @@ copy_catalog \ "${SRC}/ios/OverrideAssetsWatchApp.xcassets" \ "${ROOT}/OverrideAssetsWatchApp.xcassets" apply_display_name "${SRC}/ios/display_name.xcconfig" +DISPLAY_NAME="$(python3 - "${SRC}/ios/display_name.xcconfig" <<'PY' +from pathlib import Path +import sys +name = "GlucoseGuard" +p = Path(sys.argv[1]) +if p.is_file(): + for line in p.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if stripped.startswith("MAIN_APP_DISPLAY_NAME"): + _, _, value = stripped.partition("=") + if value.strip(): + name = value.strip() + break +print(name) +PY +)" +apply_localized_display_name "${DISPLAY_NAME}" echo "Glucose Guard branding applied from ${SRC}" From 2c2be82b7f9ce147458685960fbc4c3845612093 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 20:21:29 +0000 Subject: [PATCH 5/7] fix(branding): drive Home Screen name from display_name.xcconfig Keep MAIN_APP_DISPLAY_NAME = GlucoseGuard in the design and workspace configs, write Loop/LoopOverride.xcconfig at build time, and keep rewriting InfoPlist.xcstrings so the label is not stuck on Loop. Co-authored-by: iDustBin --- LoopConfigOverride.xcconfig | 3 ++- Scripts/apply_glucose_guard_branding.sh | 8 ++++++++ glucose-guard-design/docs/step-1.md | 3 ++- glucose-guard-design/ios/display_name.xcconfig | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/LoopConfigOverride.xcconfig b/LoopConfigOverride.xcconfig index b3bbbc9986..0af7136341 100644 --- a/LoopConfigOverride.xcconfig +++ b/LoopConfigOverride.xcconfig @@ -3,7 +3,8 @@ // Override this if you don't want the default com.${DEVELOPMENT_TEAM}.loopkit that loop uses // MAIN_APP_BUNDLE_IDENTIFIER = com.myname.loop -// Customize this to change the app name displayed +// Home-screen name. Source of truth is also +// glucose-guard-design/ios/display_name.xcconfig (published to iDustbin/glucoseguard). MAIN_APP_DISPLAY_NAME = GlucoseGuard // Customize this to change the URL to open Loop to something other than the display name diff --git a/Scripts/apply_glucose_guard_branding.sh b/Scripts/apply_glucose_guard_branding.sh index e7d7e3f6cb..a34aecc443 100755 --- a/Scripts/apply_glucose_guard_branding.sh +++ b/Scripts/apply_glucose_guard_branding.sh @@ -194,4 +194,12 @@ print(name) PY )" apply_localized_display_name "${DISPLAY_NAME}" + +# Loop.xcconfig includes this file after the default MAIN_APP_DISPLAY_NAME = Loop. +printf '%s\n' \ + "// Generated from glucose-guard-design/ios/display_name.xcconfig" \ + "MAIN_APP_DISPLAY_NAME = ${DISPLAY_NAME}" \ + > "${ROOT}/Loop/LoopOverride.xcconfig" +echo "Wrote Loop/LoopOverride.xcconfig (${DISPLAY_NAME})" + echo "Glucose Guard branding applied from ${SRC}" diff --git a/glucose-guard-design/docs/step-1.md b/glucose-guard-design/docs/step-1.md index 35d3dc8b73..85608af045 100644 --- a/glucose-guard-design/docs/step-1.md +++ b/glucose-guard-design/docs/step-1.md @@ -4,7 +4,8 @@ Branding source of truth for the Loop iOS fork (`iDustbin/LoopWorkspace`). ## Implemented now -- App name: **GlucoseGuard** +- App name: **GlucoseGuard** on the Home Screen (rewrites Loop `InfoPlist.xcstrings`, + which otherwise keep the localized name “Loop”) - Robot-cross logo on brand red (`#F4333C`) - iOS and watchOS override app icons - GitHub Actions on the Loop fork pull this tree **after** syncing diff --git a/glucose-guard-design/ios/display_name.xcconfig b/glucose-guard-design/ios/display_name.xcconfig index bb2416fa62..2e7f162671 100644 --- a/glucose-guard-design/ios/display_name.xcconfig +++ b/glucose-guard-design/ios/display_name.xcconfig @@ -1,3 +1,4 @@ // Applied by Scripts/apply_glucose_guard_branding.sh in LoopWorkspace. +// This is the Home Screen name (not the TestFlight / App Store Connect listing). // Do not put team IDs, API keys, or signing secrets in this file. MAIN_APP_DISPLAY_NAME = GlucoseGuard From 793f8f6460937dc18bdd924a807965380aa293da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 20:30:45 +0000 Subject: [PATCH 6/7] feat(ui): apply Figma mobile theme on TestFlight builds Loop was only getting a logo and display name. CI now pulls the Glucose Guard theme from iDustbin/glucoseguard and restyles HUD, toolbar, bolus, CGM picker, and A1C statistics after submodule checkout. Co-authored-by: iDustBin --- .github/workflows/bootstrap_glucose_guard.yml | 2 +- .github/workflows/build_loop.yml | 4 +- README.md | 6 +- Scripts/apply_glucose_guard_branding.sh | 10 + glucose-guard-design/.gitignore | 2 + glucose-guard-design/README.md | 6 +- glucose-guard-design/branding/tokens.json | 2 +- glucose-guard-design/docs/step-1.md | 34 +- glucose-guard-design/ios/apply_theme.py | 337 ++++++++++++++++++ .../overlays/GlucoseGuardHomeChrome.swift.txt | 71 ++++ .../overlays/GlucoseGuardStatistics.swift.txt | 126 +++++++ .../ios/overlays/LoopStateView.swift | 132 +++++++ .../glucose_guard_mark.imageset/Contents.json | 21 ++ .../glucose_guard_mark.imageset/mark.png | Bin 0 -> 59365 bytes .../ios/theme/accent.colorset/Contents.json | 20 ++ .../ios/theme/fresh.colorset/Contents.json | 20 ++ .../ios/theme/glucose.colorset/Contents.json | 38 ++ 17 files changed, 802 insertions(+), 29 deletions(-) create mode 100755 glucose-guard-design/ios/apply_theme.py create mode 100644 glucose-guard-design/ios/overlays/GlucoseGuardHomeChrome.swift.txt create mode 100644 glucose-guard-design/ios/overlays/GlucoseGuardStatistics.swift.txt create mode 100644 glucose-guard-design/ios/overlays/LoopStateView.swift create mode 100644 glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/Contents.json create mode 100644 glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/mark.png create mode 100644 glucose-guard-design/ios/theme/accent.colorset/Contents.json create mode 100644 glucose-guard-design/ios/theme/fresh.colorset/Contents.json create mode 100644 glucose-guard-design/ios/theme/glucose.colorset/Contents.json diff --git a/.github/workflows/bootstrap_glucose_guard.yml b/.github/workflows/bootstrap_glucose_guard.yml index ad09bc53af..9341c5a09b 100644 --- a/.github/workflows/bootstrap_glucose_guard.yml +++ b/.github/workflows/bootstrap_glucose_guard.yml @@ -44,4 +44,4 @@ jobs: gh workflow enable "4. Build Loop" || true gh workflow run "4. Build Loop" --ref "${GITHUB_REF_NAME}" echo "Dispatched 4. Build Loop on ${GITHUB_REF_NAME}" - echo "That job applies icons + GlucoseGuard name from iDustbin/glucoseguard, then Fastlane/TestFlight." + echo "That job applies icons, GlucoseGuard name, and the Figma mobile theme from iDustbin/glucoseguard, then Fastlane/TestFlight." diff --git a/.github/workflows/build_loop.yml b/.github/workflows/build_loop.yml index 5e80aab454..4a3ca40fae 100644 --- a/.github/workflows/build_loop.yml +++ b/.github/workflows/build_loop.yml @@ -191,8 +191,8 @@ jobs: # LoopWorkspace patches # -applies any patches located in the LoopWorkspace/patches/ directory - if $(ls ./patches/* &> /dev/null); then - git apply ./patches/* --allow-empty -v --whitespace=fix + if $(ls ./patches/*.patch &> /dev/null); then + git apply ./patches/*.patch --allow-empty -v --whitespace=fix fi # Submodule Loop patches: diff --git a/README.md b/README.md index 942ae5ca85..4b650d3ae4 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ This fork builds **Glucose Guard** on top of [LoopKit/LoopWorkspace](https://git Two GitHub pieces, then one iOS build: 1. This repo is already the fork of [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). -2. [iDustbin/glucoseguard](https://github.com/iDustbin/glucoseguard) holds logo, icons, and the **GlucoseGuard** display name. The **Bootstrap Glucose Guard** Action publishes that repo (via `GH_PAT`) and then starts **4. Build Loop**. -3. Build Loop clones the design repo, applies branding, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. +2. [iDustbin/glucoseguard](https://github.com/iDustbin/glucoseguard) holds logo, icons, the **GlucoseGuard** display name, and the Figma mobile theme. The **Bootstrap Glucose Guard** Action publishes that repo (via `GH_PAT`) and then starts **4. Build Loop**. +3. Build Loop clones the design repo, applies branding + theme, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. -Run **Bootstrap Glucose Guard** (or push to this branch) after `GH_PAT` is set. Then install the new TestFlight build to see icon and name. +Run **Bootstrap Glucose Guard** (or push to this branch) after `GH_PAT` is set. Then install the new TestFlight build to see icon, name, and the Glucose Guard home chrome. For a local Xcode icon/name refresh after an upstream pull: diff --git a/Scripts/apply_glucose_guard_branding.sh b/Scripts/apply_glucose_guard_branding.sh index a34aecc443..1f7de7de8a 100755 --- a/Scripts/apply_glucose_guard_branding.sh +++ b/Scripts/apply_glucose_guard_branding.sh @@ -202,4 +202,14 @@ printf '%s\n' \ > "${ROOT}/Loop/LoopOverride.xcconfig" echo "Wrote Loop/LoopOverride.xcconfig (${DISPLAY_NAME})" +THEME_SCRIPT="${SRC}/ios/apply_theme.py" +if [[ ! -f "${THEME_SCRIPT}" ]]; then + THEME_SCRIPT="${ROOT}/glucose-guard-design/ios/apply_theme.py" +fi +if [[ ! -d "${ROOT}/Loop/LoopUI" ]]; then + echo "Loop submodule is missing; cannot apply Glucose Guard theme" >&2 + exit 1 +fi +python3 "${THEME_SCRIPT}" --loop-root "${ROOT}/Loop" --design-root "${SRC}" + echo "Glucose Guard branding applied from ${SRC}" diff --git a/glucose-guard-design/.gitignore b/glucose-guard-design/.gitignore index 1a7cd0f3a9..1f719963bb 100644 --- a/glucose-guard-design/.gitignore +++ b/glucose-guard-design/.gitignore @@ -2,3 +2,5 @@ .idea/ .vscode/ *.log +__pycache__/ +*.py[cod] diff --git a/glucose-guard-design/README.md b/glucose-guard-design/README.md index 27c826acbe..1b05762eab 100644 --- a/glucose-guard-design/README.md +++ b/glucose-guard-design/README.md @@ -1,12 +1,12 @@ # Glucose Guard Design -iOS branding for **Glucose Guard**: logo, color tokens, and Loop override app icons. +iOS branding for **Glucose Guard**: logo, color tokens, Loop override app icons, +and the Figma mobile theme (HUD ring, Today/+ toolbar, A1C statistics). Step 1 is **iOS only**. The Loop fork [iDustbin/LoopWorkspace](https://github.com/iDustbin/LoopWorkspace) syncs from [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). GitHub Actions -then apply `ios/` (icons + display name) and publish to the existing TestFlight -account. +then apply `ios/` and publish to the existing TestFlight account. ## Layout diff --git a/glucose-guard-design/branding/tokens.json b/glucose-guard-design/branding/tokens.json index 49e4e8628e..99e5ba1c8e 100644 --- a/glucose-guard-design/branding/tokens.json +++ b/glucose-guard-design/branding/tokens.json @@ -10,7 +10,7 @@ }, "idle": { "color": "#F4F4F5", - "meaning": "White ring — awaiting a live connection (placeholder in step 1)" + "meaning": "White/aging ring — waiting for a fresh closed loop" }, "disconnected": { "color": "#F4333C", diff --git a/glucose-guard-design/docs/step-1.md b/glucose-guard-design/docs/step-1.md index 85608af045..7b7267badf 100644 --- a/glucose-guard-design/docs/step-1.md +++ b/glucose-guard-design/docs/step-1.md @@ -4,29 +4,25 @@ Branding source of truth for the Loop iOS fork (`iDustbin/LoopWorkspace`). ## Implemented now -- App name: **GlucoseGuard** on the Home Screen (rewrites Loop `InfoPlist.xcstrings`, - which otherwise keep the localized name “Loop”) +- App name: **GlucoseGuard** on the Home Screen (rewrites Loop `InfoPlist.xcstrings`) - Robot-cross logo on brand red (`#F4333C`) - iOS and watchOS override app icons -- GitHub Actions on the Loop fork pull this tree **after** syncing - `LoopKit/LoopWorkspace`, then Fastlane publishes to the existing TestFlight - account +- Mobile theme from the Figma screens: + - Accent / glucose charts in brand red, closed-loop ring in green (`#22C55E`) + - Logo inside the loop status ring (green/white/red by freshness) + - CGM and pump HUD pills with lifecycle bars (sensor ~14 days, pod ~3 days stay in those cards, not a extra top-nav box) + - Today / Statistics / **+** / Settings toolbar (One Drop–style add sheet: bolus, glucose, carbs, pre-meal, workout) + - Bolus screen shows the current recommended amount and a **Change Bolus** field + - Statistics: 3 / 7 / 30 / 90 day AVG, SD, **A1C** (GMI formula), TIR / TBR / TAR + - Add CGM overlay copy lists LibreLinkUp and Dexcom Share +- GitHub Actions pull this tree after `LoopKit/LoopWorkspace` sync, then Fastlane uploads to the existing TestFlight app ## Not in this step - Kubernetes / web dashboard -- Live CGM, pump, or Nightscout values -- Withings, Rex.fit, or other third-party health APIs -- Medical-person search and dataset access requests -- Food overview (owned by another company / designer) -- Custom Omnipod DASH communication patches +- Withings, Rex.fit, or clinician search +- Food overview / restaurant favorites (owned by another company) +- Learning and Healthway as separate product tabs (AID home stays Today) +- Custom Omnipod DASH radio patches (stay synced with LoopKit) -Omnipod DASH drops on newer InPlay/Atlas pods (especially iPhone 16 / 17e) are -handled upstream. Stay synced with LoopKit so Pod Keep Alive (Loop 3.14+) -arrives through the fork-sync workflow. - -## Later - -- Separate design repo on GitHub -- Web/Kubernetes shell -- CGM detail, A1C/GMI, averages +Omnipod DASH drops on newer InPlay/Atlas pods are handled upstream. Stay synced with LoopKit so Pod Keep Alive arrives through the fork-sync workflow. diff --git a/glucose-guard-design/ios/apply_theme.py b/glucose-guard-design/ios/apply_theme.py new file mode 100755 index 0000000000..8d746b0411 --- /dev/null +++ b/glucose-guard-design/ios/apply_theme.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +"""Apply Glucose Guard mobile theme onto a checked-out Loop submodule.""" + +from __future__ import annotations + +import argparse +import shutil +import sys +from pathlib import Path + +MARKER = "GLUCOSE_GUARD_THEME" + + +def die(message: str) -> None: + print(message, file=sys.stderr) + raise SystemExit(1) + + +def replace_once(path: Path, old: str, new: str, *, required: bool = True) -> bool: + text = path.read_text(encoding="utf-8") + if old not in text: + if new.strip()[:40] in text or MARKER in text and required is False: + return False + if required: + die(f"Could not find expected text in {path}") + return False + path.write_text(text.replace(old, new, 1), encoding="utf-8") + print(f"Patched {path}") + return True + + +def copy_colorset(src: Path, dest_catalog: Path, name: str) -> None: + source = src / f"{name}.colorset" + if not source.is_dir(): + die(f"Missing colorset {source}") + target = dest_catalog / f"{name}.colorset" + if target.exists(): + shutil.rmtree(target) + shutil.copytree(source, target) + print(f"Wrote {target}") + + +def apply_colors(design: Path, loop: Path) -> None: + theme = design / "ios" / "theme" + catalogs = [ + loop / "Loop" / "DerivedAssets.xcassets", + loop / "WatchApp" / "DerivedAssets.xcassets", + loop / "Loop Widget Extension" / "DerivedAssets.xcassets", + ] + for catalog in catalogs: + if not catalog.is_dir(): + print(f"Skip missing asset catalog {catalog}") + continue + for name in ("accent", "fresh", "glucose"): + copy_colorset(theme, catalog, name) + + +def apply_hud_mark(design: Path, loop: Path) -> None: + source = design / "ios" / "overlays" / "glucose_guard_mark.imageset" + dest = loop / "LoopUI" / "HUDAssets.xcassets" / "glucose_guard_mark.imageset" + if not source.is_dir(): + die(f"Missing HUD mark {source}") + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(source, dest) + print(f"Wrote {dest}") + + overlay = design / "ios" / "overlays" / "LoopStateView.swift" + target = loop / "LoopUI" / "Views" / "LoopStateView.swift" + if not overlay.is_file(): + die(f"Missing {overlay}") + shutil.copyfile(overlay, target) + print(f"Wrote {target}") + + +def apply_color_fallbacks(loop: Path) -> None: + uicolor = loop / "LoopUI" / "Extensions" / "UIColor.swift" + replace_once( + uicolor, + "@nonobjc static let fresh = UIColor(named: \"fresh\") ?? HIGGreenColor()", + "@nonobjc static let fresh = UIColor(named: \"fresh\") ?? UIColor(red: 0.133, green: 0.773, blue: 0.369, alpha: 1)", + ) + replace_once( + uicolor, + "@nonobjc static let glucose = UIColor(named: \"glucose\") ?? systemTeal", + "@nonobjc static let glucose = UIColor(named: \"glucose\") ?? UIColor(red: 0.957, green: 0.200, blue: 0.235, alpha: 1)", + ) + replace_once( + uicolor, + "@nonobjc public static let loopAccent = UIColor(named: \"accent\") ?? systemBlue", + "@nonobjc public static let loopAccent = UIColor(named: \"accent\") ?? UIColor(red: 0.957, green: 0.200, blue: 0.235, alpha: 1)", + ) + replace_once( + uicolor, + "@nonobjc public static let critical = systemRed", + "@nonobjc public static let critical = UIColor(red: 0.957, green: 0.200, blue: 0.235, alpha: 1)", + ) + + swift_color = loop / "LoopUI" / "Extensions" / "Color.swift" + replace_once( + swift_color, + " public static let critical = red", + " public static let critical = Color(red: 0.957, green: 0.200, blue: 0.235)", + ) + + +def apply_hud_chrome(loop: Path) -> None: + device = loop / "LoopUI" / "Views" / "DeviceStatusHUDView.swift" + replace_once( + device, + " progressView.tintColor = .systemGray", + " progressView.tintColor = UIColor(red: 0.957, green: 0.200, blue: 0.235, alpha: 1)", + ) + replace_once( + device, + " backgroundView.backgroundColor = .systemBackground\n backgroundView.layer.cornerRadius = 23", + " backgroundView.backgroundColor = .systemBackground\n backgroundView.layer.cornerRadius = 18\n backgroundView.layer.borderWidth = 1\n backgroundView.layer.borderColor = UIColor.separator.cgColor", + ) + + status_bar = loop / "LoopUI" / "Views" / "StatusBarHUDView.swift" + replace_once( + status_bar, + " self.backgroundColor = UIColor.secondarySystemBackground", + " self.backgroundColor = UIColor.systemBackground", + ) + + completion = loop / "LoopUI" / "Views" / "LoopCompletionHUDView.swift" + replace_once( + completion, + " return (title: LocalizedString(\"Loop Warning\", comment: \"Title of yellow loop message\"),", + " return (title: LocalizedString(\"Glucose Guard Warning\", comment: \"Title of yellow loop message\"),", + ) + replace_once( + completion, + " return (title: LocalizedString(\"Loop Failure\", comment: \"Title of red loop message\"),", + " return (title: LocalizedString(\"Glucose Guard Failure\", comment: \"Title of red loop message\"),", + ) + + +def apply_toolbar(loop: Path) -> None: + path = loop / "Loop" / "View Controllers" / "StatusTableViewController.swift" + text = path.read_text(encoding="utf-8") + if "presentGlucoseGuardLogSheet" in text: + print(f"Home chrome already applied in {path}") + return + + old_setup = """ private func setupToolbarItems() { + let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) + let carbs = UIBarButtonItem(image: UIImage(named: "carbs"), style: .plain, target: self, action: #selector(userTappedAddCarbs)) + let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) + let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) + + let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) + let workout = createWorkoutButtonItem(selected: false, isEnabled: true) + toolbarItems = [ + carbs, + space, + preMeal, + space, + bolus, + space, + workout, + space, + settings + ] + }""" + new_setup = """ private func setupToolbarItems() { + let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) + let symbol = UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold) + let today = UIBarButtonItem(image: UIImage(systemName: "sun.max.fill", withConfiguration: symbol), style: .plain, target: self, action: #selector(presentGlucoseGuardToday)) + let stats = UIBarButtonItem(image: UIImage(systemName: "chart.bar.fill", withConfiguration: symbol), style: .plain, target: self, action: #selector(presentGlucoseGuardStatistics)) + let add = UIBarButtonItem(image: UIImage(systemName: "plus.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .bold)), style: .plain, target: self, action: #selector(presentGlucoseGuardLogSheet)) + let settings = UIBarButtonItem(image: UIImage(systemName: "person.crop.circle", withConfiguration: symbol), style: .plain, target: self, action: #selector(onSettingsTapped)) + toolbarItems = [ + today, + space, + stats, + space, + add, + space, + settings + ] + }""" + replace_once(path, old_setup, new_setup) + + old_update = """ private func updateToolbarItems() { + let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true + + toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") + toolbarItems![0].isEnabled = isPumpOnboarded + toolbarItems![0].tintColor = UIColor.carbTintColor + toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") + toolbarItems![4].isEnabled = isPumpOnboarded + toolbarItems![4].tintColor = UIColor.insulinTintColor + toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") + toolbarItems![8].tintColor = UIColor.secondaryLabel + + toolbarItems![2] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) + toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) + }""" + new_update = """ private func updateToolbarItems() { + let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true + + toolbarItems![0].accessibilityLabel = NSLocalizedString("Today", comment: "The label of the Today toolbar item") + toolbarItems![0].isEnabled = true + toolbarItems![0].tintColor = UIColor.loopAccent + toolbarItems![2].accessibilityLabel = NSLocalizedString("Statistics", comment: "The label of the statistics toolbar item") + toolbarItems![2].isEnabled = true + toolbarItems![2].tintColor = UIColor.secondaryLabel + toolbarItems![4].accessibilityLabel = NSLocalizedString("Add", comment: "The label of the add toolbar item") + toolbarItems![4].isEnabled = isPumpOnboarded || deviceManager.cgmManager != nil + toolbarItems![4].tintColor = UIColor.loopAccent + toolbarItems![6].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") + toolbarItems![6].tintColor = UIColor.secondaryLabel + }""" + replace_once(path, old_update, new_update) + + replace_once( + path, + " tableView.backgroundColor = .secondarySystemBackground", + " tableView.backgroundColor = .systemBackground", + ) + + insert_after = """ present(navigationWrapper, animated: true) + deviceManager.analyticsServicesManager.didDisplayBolusScreen() + } +""" + chrome = Path(__file__).with_name("overlays") / "GlucoseGuardHomeChrome.swift.txt" + stats = Path(__file__).with_name("overlays") / "GlucoseGuardStatistics.swift.txt" + chrome_text = chrome.read_text(encoding="utf-8") + stats_text = stats.read_text(encoding="utf-8") + text = path.read_text(encoding="utf-8") + if insert_after not in text: + die(f"Could not insert home chrome into {path}") + text = text.replace(insert_after, insert_after + "\n" + chrome_text + "\n", 1) + if not text.endswith("\n"): + text += "\n" + text += "\n" + stats_text + if not text.endswith("\n"): + text += "\n" + path.write_text(text, encoding="utf-8") + print(f"Inserted Glucose Guard home chrome into {path}") + + +def apply_bolus_and_settings(loop: Path) -> None: + bolus = loop / "Loop" / "Views" / "BolusEntryView.swift" + replace_once( + bolus, + " Text(\"Bolus\", comment: \"Label for bolus entry row on bolus screen\")", + " Text(\"Change Bolus\", comment: \"Label for bolus entry row on bolus screen\")", + ) + replace_once( + bolus, + " bolusEntryRow\n }", + " bolusEntryRow\n Text(\"Current recommended bolus is above. Enter a new amount to change it.\", comment: \"Help text for changing the current bolus\")\n .font(.footnote)\n .foregroundColor(.secondary)\n }", + required=False, + ) + + settings = loop / "Loop" / "Views" / "SettingsView.swift" + replace_once( + settings, + """ case .cgmPicker: + return ActionSheet( + title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), + buttons: cgmChoices + )""", + """ case .cgmPicker: + return ActionSheet( + title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), + message: Text("LibreLinkUp, Dexcom Share, or another CGM source.", comment: "CGM chooser subtitle listing required sources"), + buttons: cgmChoices + )""", + ) + replace_once( + settings, + " descriptiveText: NSLocalizedString(\"Tap here to set up a CGM\", comment: \"Descriptive text for button to add CGM device\"))", + " descriptiveText: NSLocalizedString(\"LibreLinkUp, Dexcom Share, or another CGM. Sensor expiry is in CGM detail.\", comment: \"Descriptive text for button to add CGM device\"))", + ) + replace_once( + settings, + " .accentColor(Color(.systemGray))", + " .accentColor(Color(red: 0.957, green: 0.200, blue: 0.235))", + ) + settings_text = settings.read_text(encoding="utf-8") + if "glucoseGuardProfileSection" not in settings_text: + replace_once( + settings, + " loopSection", + " loopSection\n glucoseGuardProfileSection", + ) + + profile = ''' + private var glucoseGuardProfileSection: some View { + Section(header: SectionHeader(label: NSLocalizedString("Profile", comment: "Settings profile section"))) { + VStack(alignment: .leading, spacing: 6) { + Text("A1C, AVG, SD, TIR, TBR, and TAR") + .font(.headline) + Text("Open Statistics from the Today toolbar to switch 3, 7, 30, or 90 days.") + .font(.footnote) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + } +''' + text = settings.read_text(encoding="utf-8") + if "glucoseGuardProfileSection" in text and "private var glucoseGuardProfileSection" not in text: + # referenced but not defined — fall through to insert + pass + if "private var glucoseGuardProfileSection" not in text: + needle = " private var loopSection: some View {" + if needle not in text: + die(f"Could not insert profile section into {settings}") + settings.write_text(text.replace(needle, profile + "\n" + needle, 1), encoding="utf-8") + print(f"Inserted profile section into {settings}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--loop-root", required=True) + parser.add_argument("--design-root", required=True) + args = parser.parse_args() + loop = Path(args.loop_root) + design = Path(args.design_root) + if not (loop / "LoopUI").is_dir(): + die(f"Loop checkout is missing LoopUI: {loop}") + apply_colors(design, loop) + apply_hud_mark(design, loop) + apply_color_fallbacks(loop) + apply_hud_chrome(loop) + apply_toolbar(loop) + apply_bolus_and_settings(loop) + print("Glucose Guard mobile theme applied") + + +if __name__ == "__main__": + main() diff --git a/glucose-guard-design/ios/overlays/GlucoseGuardHomeChrome.swift.txt b/glucose-guard-design/ios/overlays/GlucoseGuardHomeChrome.swift.txt new file mode 100644 index 0000000000..c673c56001 --- /dev/null +++ b/glucose-guard-design/ios/overlays/GlucoseGuardHomeChrome.swift.txt @@ -0,0 +1,71 @@ + // GLUCOSE_GUARD_THEME home chrome + + @objc private func presentGlucoseGuardToday() { + tableView.setContentOffset(.zero, animated: true) + } + + @objc private func presentGlucoseGuardLogSheet() { + let sheet = UIAlertController( + title: NSLocalizedString("Add", comment: "Title of Glucose Guard add sheet"), + message: NSLocalizedString("Insulin bolus, measured glucose, or carbohydrates. The bolus screen shows the current recommended amount and lets you change it.", comment: "Subtitle of Glucose Guard add sheet"), + preferredStyle: .actionSheet + ) + sheet.addAction(UIAlertAction( + title: NSLocalizedString("Insulin Bolus", comment: "Add-sheet action to change bolus"), + style: .default, + handler: { [weak self] _ in self?.presentBolusScreen() } + )) + sheet.addAction(UIAlertAction( + title: NSLocalizedString("Glucose Value", comment: "Add-sheet action to enter glucose"), + style: .default, + handler: { [weak self] _ in self?.presentBolusEntryView(enableManualGlucoseEntry: true) } + )) + sheet.addAction(UIAlertAction( + title: NSLocalizedString("Carbohydrates", comment: "Add-sheet action to enter carbs"), + style: .default, + handler: { [weak self] _ in self?.userTappedAddCarbs() } + )) + let preMealAction = UIAlertAction( + title: NSLocalizedString("Pre-Meal Targets", comment: "Add-sheet action for pre-meal"), + style: .default, + handler: { [weak self] _ in self?.togglePreMealMode(confirm: false) } + ) + preMealAction.isEnabled = preMealModeAllowed + sheet.addAction(preMealAction) + let workoutAction = UIAlertAction( + title: NSLocalizedString("Workout Targets", comment: "Add-sheet action for workout"), + style: .default, + handler: { [weak self] _ in self?.presentCustomPresets(confirm: false) } + ) + workoutAction.isEnabled = workoutModeAllowed + sheet.addAction(workoutAction) + sheet.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel add sheet"), style: .cancel)) + present(sheet, animated: true) + } + + @objc private func presentGlucoseGuardStatistics() { + let unit = deviceManager.displayGlucosePreference.unit + let root = GlucoseGuardStatisticsView(displayUnit: unit) { start, completion in + self.deviceManager.glucoseStore.getGlucoseSamples(start: start, end: nil) { result in + let values: [Double] + switch result { + case .success(let samples): + values = samples.map { $0.quantity.doubleValue(for: .milligramsPerDeciliter) } + case .failure: + values = [] + } + DispatchQueue.main.async { + completion(values) + } + } + } + let hosting = UIHostingController(rootView: root) + hosting.title = NSLocalizedString("Statistics", comment: "Title of Glucose Guard statistics") + let navigation = UINavigationController(rootViewController: hosting) + hosting.navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: navigation, + action: #selector(UIViewController.dismissWithAnimation) + ) + present(navigation, animated: true) + } diff --git a/glucose-guard-design/ios/overlays/GlucoseGuardStatistics.swift.txt b/glucose-guard-design/ios/overlays/GlucoseGuardStatistics.swift.txt new file mode 100644 index 0000000000..a28ffdf578 --- /dev/null +++ b/glucose-guard-design/ios/overlays/GlucoseGuardStatistics.swift.txt @@ -0,0 +1,126 @@ +// GLUCOSE_GUARD_THEME statistics + +private struct GlucoseGuardStatisticsView: View { + let displayUnit: HKUnit + let loader: (Date, @escaping ([Double]) -> Void) -> Void + + @State private var periodDays = 7 + @State private var valuesMgdl: [Double] = [] + @State private var loading = true + + private let periods = [3, 7, 30, 90] + + var body: some View { + List { + Section(header: Text(NSLocalizedString("Period", comment: "Statistics period header"))) { + Picker(NSLocalizedString("Period", comment: "Statistics period picker"), selection: $periodDays) { + ForEach(periods, id: \.self) { days in + Text("\(days)d").tag(days) + } + } + .pickerStyle(.segmented) + .onChange(of: periodDays) { _ in + reload() + } + } + + Section(header: Text(NSLocalizedString("Glucose", comment: "Statistics glucose header"))) { + metricRow( + title: "AVG", + value: formattedAverage, + detail: NSLocalizedString("Average glucose for the selected days.", comment: "AVG help") + ) + metricRow( + title: "SD", + value: formattedSD, + detail: NSLocalizedString("Standard deviation of glucose.", comment: "SD help") + ) + metricRow( + title: "A1C", + value: formattedA1C, + detail: NSLocalizedString("Estimated A1C from mean glucose (GMI formula).", comment: "A1C help") + ) + } + + Section(header: Text(NSLocalizedString("Time in range", comment: "TIR section header"))) { + metricRow(title: "TIR", value: formattedPercent(inRange), detail: NSLocalizedString("70–180 mg/dL.", comment: "TIR help")) + metricRow(title: "TBR", value: formattedPercent(belowRange), detail: NSLocalizedString("Below 70 mg/dL.", comment: "TBR help")) + metricRow(title: "TAR", value: formattedPercent(aboveRange), detail: NSLocalizedString("Above 180 mg/dL.", comment: "TAR help")) + } + } + .listStyle(.insetGrouped) + .overlay { + if loading { + ProgressView() + } else if valuesMgdl.isEmpty { + Text(NSLocalizedString("No glucose values in this period.", comment: "Empty statistics")) + .foregroundColor(.secondary) + } + } + .onAppear(perform: reload) + } + + private func metricRow(title: String, value: String, detail: String) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(title) + .font(.headline) + Spacer() + Text(value) + .font(.title2.monospacedDigit()) + .foregroundColor(Color(red: 0.957, green: 0.200, blue: 0.235)) + } + Text(detail) + .font(.footnote) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + + private var averageMgdl: Double? { + guard !valuesMgdl.isEmpty else { return nil } + return valuesMgdl.reduce(0, +) / Double(valuesMgdl.count) + } + + private var formattedAverage: String { + guard let averageMgdl else { return "—" } + let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: averageMgdl) + return String(format: "%.1f %@", quantity.doubleValue(for: displayUnit), displayUnit.unitString) + } + + private var formattedSD: String { + guard let averageMgdl, valuesMgdl.count > 1 else { return "—" } + let variance = valuesMgdl.reduce(0) { $0 + pow($1 - averageMgdl, 2) } / Double(valuesMgdl.count) + let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: sqrt(variance)) + return String(format: "%.1f %@", quantity.doubleValue(for: displayUnit), displayUnit.unitString) + } + + private var formattedA1C: String { + guard let averageMgdl else { return "—" } + let a1c = 3.31 + 0.02392 * averageMgdl + return String(format: "%.1f%%", a1c) + } + + private var inRange: Double? { ratio { $0 >= 70 && $0 <= 180 } } + private var belowRange: Double? { ratio { $0 < 70 } } + private var aboveRange: Double? { ratio { $0 > 180 } } + + private func ratio(_ predicate: (Double) -> Bool) -> Double? { + guard !valuesMgdl.isEmpty else { return nil } + return Double(valuesMgdl.filter(predicate).count) / Double(valuesMgdl.count) + } + + private func formattedPercent(_ value: Double?) -> String { + guard let value else { return "—" } + return String(format: "%.0f%%", value * 100) + } + + private func reload() { + loading = true + let start = Calendar.current.date(byAdding: .day, value: -periodDays, to: Date()) ?? Date() + loader(start) { samples in + valuesMgdl = samples + loading = false + } + } +} diff --git a/glucose-guard-design/ios/overlays/LoopStateView.swift b/glucose-guard-design/ios/overlays/LoopStateView.swift new file mode 100644 index 0000000000..0dc6bc5d13 --- /dev/null +++ b/glucose-guard-design/ios/overlays/LoopStateView.swift @@ -0,0 +1,132 @@ +// +// LoopStateView.swift +// LoopUI +// +// Glucose Guard theme: brand mark inside the closed-loop status ring. +// GLUCOSE_GUARD_THEME +// + +import UIKit + +final class LoopStateView: UIView { + var firstDataUpdate = true + + private let markView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFit + view.clipsToBounds = true + view.isUserInteractionEnabled = false + view.image = UIImage(named: "glucose_guard_mark", in: Bundle(for: LoopStateView.self), compatibleWith: nil) + return view + }() + + override func tintColorDidChange() { + super.tintColorDidChange() + + updateTintColor() + } + + private func updateTintColor() { + shapeLayer.strokeColor = tintColor.cgColor + } + + var open = false { + didSet { + if open != oldValue { + shapeLayer.path = drawPath() + } + } + } + + override class var layerClass: AnyClass { + return CAShapeLayer.self + } + + private var shapeLayer: CAShapeLayer { + return layer as! CAShapeLayer + } + + override init(frame: CGRect) { + super.init(frame: frame) + configureLayer() + embedMark() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + configureLayer() + embedMark() + } + + private func configureLayer() { + shapeLayer.lineWidth = 6 + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.lineCap = .round + updateTintColor() + shapeLayer.path = drawPath() + } + + private func embedMark() { + if markView.superview == nil { + addSubview(markView) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + shapeLayer.path = drawPath() + let inset = max(8, shapeLayer.lineWidth + 3) + markView.frame = bounds.insetBy(dx: inset, dy: inset) + markView.layer.cornerRadius = markView.bounds.width * 0.22 + } + + private func drawPath(lineWidth: CGFloat? = nil) -> CGPath { + let center = CGPoint(x: bounds.midX, y: bounds.midY) + let lineWidth = lineWidth ?? shapeLayer.lineWidth + let radius = min(bounds.width / 2, bounds.height / 2) - lineWidth / 2 + + let startAngle = open ? -CGFloat.pi / 4 : 0 + let endAngle = open ? 5 * CGFloat.pi / 4 : 2 * CGFloat.pi + + let path = UIBezierPath( + arcCenter: center, + radius: radius, + startAngle: startAngle, + endAngle: endAngle, + clockwise: true + ) + + return path.cgPath + } + + private static let AnimationKey = "com.loudnate.Naterade.breatheAnimation" + + var animated: Bool = false { + didSet { + if animated != oldValue { + if animated { + let path = CABasicAnimation(keyPath: "path") + path.fromValue = shapeLayer.path ?? drawPath() + path.toValue = drawPath(lineWidth: 12) + + let width = CABasicAnimation(keyPath: "lineWidth") + width.fromValue = shapeLayer.lineWidth + width.toValue = 8 + + let group = CAAnimationGroup() + group.animations = [path, width] + group.duration = firstDataUpdate ? 0 : 1 + group.repeatCount = HUGE + group.autoreverses = true + group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + shapeLayer.add(group, forKey: type(of: self).AnimationKey) + } else { + shapeLayer.removeAnimation(forKey: type(of: self).AnimationKey) + } + } + firstDataUpdate = false + } + } +} diff --git a/glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/Contents.json b/glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/Contents.json new file mode 100644 index 0000000000..83c81ad630 --- /dev/null +++ b/glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "mark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/mark.png b/glucose-guard-design/ios/overlays/glucose_guard_mark.imageset/mark.png new file mode 100644 index 0000000000000000000000000000000000000000..b46d6f9f4e841e40e66ff29688fa7ba241c33acf GIT binary patch literal 59365 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�#pIaX? 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 literal 0 HcmV?d00001 diff --git a/glucose-guard-design/ios/theme/accent.colorset/Contents.json b/glucose-guard-design/ios/theme/accent.colorset/Contents.json new file mode 100644 index 0000000000..a748405c98 --- /dev/null +++ b/glucose-guard-design/ios/theme/accent.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "red" : "0.957", + "green" : "0.200", + "blue" : "0.235" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/glucose-guard-design/ios/theme/fresh.colorset/Contents.json b/glucose-guard-design/ios/theme/fresh.colorset/Contents.json new file mode 100644 index 0000000000..04ce722bc5 --- /dev/null +++ b/glucose-guard-design/ios/theme/fresh.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "red" : "0.133", + "green" : "0.773", + "blue" : "0.369" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/glucose-guard-design/ios/theme/glucose.colorset/Contents.json b/glucose-guard-design/ios/theme/glucose.colorset/Contents.json new file mode 100644 index 0000000000..e663b376fa --- /dev/null +++ b/glucose-guard-design/ios/theme/glucose.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "red" : "0.957", + "green" : "0.200", + "blue" : "0.235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "red" : "1.000", + "green" : "0.353", + "blue" : "0.380" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From b1e05bf92cedb0ad9c56d3d7544b4eef3228b884 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 21:48:28 +0000 Subject: [PATCH 7/7] feat(ui): ship Figma screens as native Xcode SwiftUI Build 82 only tinted Loop. The home chrome is now real SwiftUI: Today / Learning / Healthway / +, CGM and pump detail cards, and the Glucose Values dashboard, applied after Loop checkout. Co-authored-by: iDustBin --- README.md | 2 +- glucose-guard-design/README.md | 3 +- glucose-guard-design/docs/step-1.md | 17 +- glucose-guard-design/ios/apply_theme.py | 58 +++ .../overlays/GlucoseGuardTabBarHost.swift.txt | 188 +++++++++ .../GlucoseGuardXcodeDesign.swift.txt | 362 ++++++++++++++++++ 6 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 glucose-guard-design/ios/overlays/GlucoseGuardTabBarHost.swift.txt create mode 100644 glucose-guard-design/ios/overlays/GlucoseGuardXcodeDesign.swift.txt diff --git a/README.md b/README.md index 4b650d3ae4..f4e2cf3f0b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Two GitHub pieces, then one iOS build: 1. This repo is already the fork of [LoopKit/LoopWorkspace](https://github.com/LoopKit/LoopWorkspace). 2. [iDustbin/glucoseguard](https://github.com/iDustbin/glucoseguard) holds logo, icons, the **GlucoseGuard** display name, and the Figma mobile theme. The **Bootstrap Glucose Guard** Action publishes that repo (via `GH_PAT`) and then starts **4. Build Loop**. -3. Build Loop clones the design repo, applies branding + theme, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. +3. Build Loop clones the design repo, applies branding plus native Xcode/SwiftUI Figma screens, and uploads to the existing TestFlight app (`com.TEAMID.loopkit.Loop`). No new Apple App ID. Run **Bootstrap Glucose Guard** (or push to this branch) after `GH_PAT` is set. Then install the new TestFlight build to see icon, name, and the Glucose Guard home chrome. diff --git a/glucose-guard-design/README.md b/glucose-guard-design/README.md index 1b05762eab..ae9c200109 100644 --- a/glucose-guard-design/README.md +++ b/glucose-guard-design/README.md @@ -1,7 +1,8 @@ # Glucose Guard Design iOS branding for **Glucose Guard**: logo, color tokens, Loop override app icons, -and the Figma mobile theme (HUD ring, Today/+ toolbar, A1C statistics). +and native Xcode/SwiftUI screens from the Figma mobile theme +(Today / Learning / Healthway / +, CGM and pump detail, A1C). Step 1 is **iOS only**. The Loop fork [iDustbin/LoopWorkspace](https://github.com/iDustbin/LoopWorkspace) syncs from diff --git a/glucose-guard-design/docs/step-1.md b/glucose-guard-design/docs/step-1.md index 7b7267badf..737a4fcb68 100644 --- a/glucose-guard-design/docs/step-1.md +++ b/glucose-guard-design/docs/step-1.md @@ -7,14 +7,16 @@ Branding source of truth for the Loop iOS fork (`iDustbin/LoopWorkspace`). - App name: **GlucoseGuard** on the Home Screen (rewrites Loop `InfoPlist.xcstrings`) - Robot-cross logo on brand red (`#F4333C`) - iOS and watchOS override app icons -- Mobile theme from the Figma screens: +- Native **Xcode/SwiftUI** screens from the Figma mobile theme: - Accent / glucose charts in brand red, closed-loop ring in green (`#22C55E`) - - Logo inside the loop status ring (green/white/red by freshness) - - CGM and pump HUD pills with lifecycle bars (sensor ~14 days, pod ~3 days stay in those cards, not a extra top-nav box) - - Today / Statistics / **+** / Settings toolbar (One Drop–style add sheet: bolus, glucose, carbs, pre-meal, workout) - - Bolus screen shows the current recommended amount and a **Change Bolus** field - - Statistics: 3 / 7 / 30 / 90 day AVG, SD, **A1C** (GMI formula), TIR / TBR / TAR - - Add CGM overlay copy lists LibreLinkUp and Dexcom Share + - Logo inside the loop status ring + - SwiftUI tab bar: **Today · Learning · Healthway · +** + - CGM detail: 14-day sensor bar, last reading, AVG + - Pump detail: 3-day pod bar, basal, change bolus + - Learning: 3 / 7 / 30 / 90 day AVG, SD, **A1C**, TIR / TBR / TAR + - Healthway: Glucose Values dashboard + - Bolus: current recommended amount plus **Change Bolus** + - Add CGM overlay lists LibreLinkUp and Dexcom Share - GitHub Actions pull this tree after `LoopKit/LoopWorkspace` sync, then Fastlane uploads to the existing TestFlight app ## Not in this step @@ -22,7 +24,6 @@ Branding source of truth for the Loop iOS fork (`iDustbin/LoopWorkspace`). - Kubernetes / web dashboard - Withings, Rex.fit, or clinician search - Food overview / restaurant favorites (owned by another company) -- Learning and Healthway as separate product tabs (AID home stays Today) - Custom Omnipod DASH radio patches (stay synced with LoopKit) Omnipod DASH drops on newer InPlay/Atlas pods are handled upstream. Stay synced with LoopKit so Pod Keep Alive arrives through the fork-sync workflow. diff --git a/glucose-guard-design/ios/apply_theme.py b/glucose-guard-design/ios/apply_theme.py index 8d746b0411..95e3da0fe1 100755 --- a/glucose-guard-design/ios/apply_theme.py +++ b/glucose-guard-design/ios/apply_theme.py @@ -242,6 +242,63 @@ def apply_toolbar(loop: Path) -> None: print(f"Inserted Glucose Guard home chrome into {path}") +def apply_xcode_design(loop: Path) -> None: + path = loop / "Loop" / "View Controllers" / "StatusTableViewController.swift" + replace_once( + path, + " navigationController?.setToolbarHidden(false, animated: animated)", + " navigationController?.setToolbarHidden(true, animated: animated)", + ) + status_text = path.read_text(encoding="utf-8") + if "installGlucoseGuardTabBar()" not in status_text: + replace_once( + path, + " setupToolbarItems()\n", + " setupToolbarItems()\n installGlucoseGuardTabBar()\n", + ) + replace_once( + path, + """ @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { + if let pumpStatusView = sender.view as? PumpStatusHUDView { + executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD)) + } + }""", + """ @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { + presentGlucoseGuardPumpDetail() + }""", + ) + replace_once( + path, + """ @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { + executeHUDTapAction(deviceManager.didTapOnCGMStatus()) + }""", + """ @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { + presentGlucoseGuardCGMDetail() + }""", + ) + + text = path.read_text(encoding="utf-8") + if "installGlucoseGuardTabBar" in text and "GLUCOSE_GUARD_XCODE_DESIGN host" not in text: + host = Path(__file__).with_name("overlays") / "GlucoseGuardTabBarHost.swift.txt" + insert_after = """ present(navigationWrapper, animated: true) + deviceManager.analyticsServicesManager.didDisplayBolusScreen() + } +""" + if insert_after not in text: + die(f"Could not insert Xcode tab bar host into {path}") + text = text.replace(insert_after, insert_after + "\n" + host.read_text(encoding="utf-8") + "\n", 1) + path.write_text(text, encoding="utf-8") + print(f"Inserted Glucose Guard Xcode tab bar host into {path}") + + text = path.read_text(encoding="utf-8") + if "GLUCOSE_GUARD_XCODE_DESIGN\n" not in text: + design = Path(__file__).with_name("overlays") / "GlucoseGuardXcodeDesign.swift.txt" + if not text.endswith("\n"): + text += "\n" + path.write_text(text + "\n" + design.read_text(encoding="utf-8"), encoding="utf-8") + print(f"Appended Glucose Guard Xcode design screens to {path}") + + def apply_bolus_and_settings(loop: Path) -> None: bolus = loop / "Loop" / "Views" / "BolusEntryView.swift" replace_once( @@ -329,6 +386,7 @@ def main() -> None: apply_color_fallbacks(loop) apply_hud_chrome(loop) apply_toolbar(loop) + apply_xcode_design(loop) apply_bolus_and_settings(loop) print("Glucose Guard mobile theme applied") diff --git a/glucose-guard-design/ios/overlays/GlucoseGuardTabBarHost.swift.txt b/glucose-guard-design/ios/overlays/GlucoseGuardTabBarHost.swift.txt new file mode 100644 index 0000000000..4ae10fb22e --- /dev/null +++ b/glucose-guard-design/ios/overlays/GlucoseGuardTabBarHost.swift.txt @@ -0,0 +1,188 @@ + // GLUCOSE_GUARD_XCODE_DESIGN host + + private var glucoseGuardTabBarHost: UIHostingController? + + private func installGlucoseGuardTabBar() { + guard glucoseGuardTabBarHost == nil else { return } + navigationController?.setToolbarHidden(true, animated: false) + let bar = GlucoseGuardTabBar( + onToday: { [weak self] in self?.presentGlucoseGuardToday() }, + onLearning: { [weak self] in self?.presentGlucoseGuardLearning() }, + onHealthway: { [weak self] in self?.presentGlucoseGuardHealthway() }, + onAdd: { [weak self] in self?.presentGlucoseGuardLogSheet() } + ) + let host = UIHostingController(rootView: bar) + host.view.backgroundColor = .clear + addChild(host) + host.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(host.view) + NSLayoutConstraint.activate([ + host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + host.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + host.didMove(toParent: self) + glucoseGuardTabBarHost = host + tableView.contentInset.bottom = 96 + tableView.verticalScrollIndicatorInsets.bottom = 96 + } + + private func presentGlucoseGuardDesign(_ root: Content, title: String) { + let hosting = UIHostingController(rootView: root) + hosting.title = title + hosting.view.backgroundColor = UIColor(red: 0.039, green: 0.039, blue: 0.039, alpha: 1) + let navigation = UINavigationController(rootViewController: hosting) + hosting.navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: navigation, + action: #selector(UIViewController.dismissWithAnimation) + ) + present(navigation, animated: true) + } + + @objc private func presentGlucoseGuardLearning() { + let unit = deviceManager.displayGlucosePreference.unit + presentGlucoseGuardDesign( + GlucoseGuardLearningView(displayUnit: unit) { start, completion in + self.deviceManager.glucoseStore.getGlucoseSamples(start: start, end: nil) { result in + let values: [Double] + switch result { + case .success(let samples): + values = samples.map { $0.quantity.doubleValue(for: .milligramsPerDeciliter) } + case .failure: + values = [] + } + DispatchQueue.main.async { completion(values) } + } + }, + title: NSLocalizedString("Learning", comment: "Learning tab title") + ) + } + + @objc private func presentGlucoseGuardHealthway() { + let unit = deviceManager.displayGlucosePreference.unit + let latest = deviceManager.glucoseStore.latestGlucose + let current = latest.map { deviceManager.displayGlucosePreference.format($0.quantity) } ?? "—" + let start = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date() + deviceManager.glucoseStore.getGlucoseSamples(start: start, end: nil) { result in + let values: [Double] + switch result { + case .success(let samples): + values = samples.map { $0.quantity.doubleValue(for: .milligramsPerDeciliter) } + case .failure: + values = [] + } + let average = values.isEmpty ? nil : values.reduce(0, +) / Double(values.count) + let a1c = average.map { String(format: "%.1f%%", 3.31 + 0.02392 * $0) } ?? "—" + let avgText: String + if let average { + let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: average) + avgText = String(format: "%.1f %@", quantity.doubleValue(for: unit), unit.unitString) + } else { + avgText = "—" + } + let tir: String + if values.isEmpty { + tir = "—" + } else { + let inRange = values.filter { $0 >= 70 && $0 <= 180 }.count + tir = String(format: "%.0f%%", (Double(inRange) / Double(values.count)) * 100) + } + let spark = Array(values.suffix(48)) + DispatchQueue.main.async { + self.presentGlucoseGuardDesign( + GlucoseGuardHealthwayView( + currentGlucose: current, + a1c: a1c, + average: avgText, + tir: tir, + points: spark + ), + title: NSLocalizedString("Healthway", comment: "Healthway tab title") + ) + } + } + } + + private func presentGlucoseGuardCGMDetail() { + let unit = deviceManager.displayGlucosePreference.unit + let latest = deviceManager.glucoseStore.latestGlucose + let reading = latest.map { deviceManager.displayGlucosePreference.format($0.quantity) } ?? "— — —" + let minutes: String + if let date = latest?.startDate { + minutes = String(format: "%.0f min", abs(date.timeIntervalSinceNow) / 60) + } else { + minutes = "0 min" + } + let used = deviceManager.cgmLifecycleProgress?.percentComplete ?? 0 + let remaining = max(0, (1 - used) * 14) + let start = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date() + deviceManager.glucoseStore.getGlucoseSamples(start: start, end: nil) { result in + let values: [Double] + switch result { + case .success(let samples): + values = samples.map { $0.quantity.doubleValue(for: .milligramsPerDeciliter) } + case .failure: + values = [] + } + let average: String + if values.isEmpty { + average = "—" + } else { + let mean = values.reduce(0, +) / Double(values.count) + let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mean) + average = String(format: "%.1f %@", quantity.doubleValue(for: unit), unit.unitString) + } + DispatchQueue.main.async { + self.presentGlucoseGuardDesign( + GlucoseGuardCGMDetailView( + lastReading: reading, + minutesAgo: minutes, + remainingDays: remaining, + sensorLifeDays: 14, + average: average, + onOpenCGM: { + self.dismiss(animated: true) { + self.executeHUDTapAction(self.deviceManager.didTapOnCGMStatus()) + } + } + ), + title: "CGM" + ) + } + } + } + + private func presentGlucoseGuardPumpDetail() { + let used = deviceManager.pumpLifecycleProgress?.percentComplete ?? 0 + let remaining = max(0, (1 - used) * 3) + let name: String + if let pump = deviceManager.pumpManager { + name = type(of: pump).localizedTitle + } else { + name = "Insulin Pump" + } + let basal = hudView?.pumpStatusHUD.basalRateHUD.accessibilityValue ?? "—" + presentGlucoseGuardDesign( + GlucoseGuardPumpDetailView( + pumpName: name, + remainingDays: remaining, + podLifeDays: 3, + basal: basal, + remainingInsulin: "—", + onChangeBolus: { + self.dismiss(animated: true) { + self.presentBolusScreen() + } + }, + onOpenPump: { + self.dismiss(animated: true) { + if let pumpView = self.hudView?.pumpStatusHUD { + self.executeHUDTapAction(self.deviceManager.didTapOnPumpStatus(pumpView.pumpManagerProvidedHUD)) + } + } + } + ), + title: name + ) + } diff --git a/glucose-guard-design/ios/overlays/GlucoseGuardXcodeDesign.swift.txt b/glucose-guard-design/ios/overlays/GlucoseGuardXcodeDesign.swift.txt new file mode 100644 index 0000000000..cc35bc642d --- /dev/null +++ b/glucose-guard-design/ios/overlays/GlucoseGuardXcodeDesign.swift.txt @@ -0,0 +1,362 @@ +// GLUCOSE_GUARD_XCODE_DESIGN +// Native SwiftUI screens matching the Figma mobile theme. + +private enum GlucoseGuardTheme { + static let red = Color(red: 0.957, green: 0.200, blue: 0.235) + static let green = Color(red: 0.133, green: 0.773, blue: 0.369) + static let ink = Color(red: 0.039, green: 0.039, blue: 0.039) + static let surface = Color(red: 0.090, green: 0.090, blue: 0.090) + static let muted = Color(red: 0.639, green: 0.639, blue: 0.639) + static let cardRadius: CGFloat = 18 +} + +struct GlucoseGuardTabBar: View { + var onToday: () -> Void + var onLearning: () -> Void + var onHealthway: () -> Void + var onAdd: () -> Void + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + tabButton("Today", systemName: "sun.max.fill", action: onToday) + tabButton("Learning", systemName: "rectangle.stack.fill", action: onLearning) + tabButton("Healthway", systemName: "heart.text.square.fill", action: onHealthway) + Button(action: onAdd) { + ZStack { + Circle() + .fill(GlucoseGuardTheme.red) + .frame(width: 58, height: 58) + .shadow(color: GlucoseGuardTheme.red.opacity(0.35), radius: 8, y: 3) + Image(systemName: "plus") + .font(.system(size: 26, weight: .bold)) + .foregroundColor(.white) + } + } + .buttonStyle(.plain) + .accessibilityLabel(Text("Add")) + .padding(.bottom, 2) + } + .padding(.horizontal, 10) + .padding(.top, 10) + .padding(.bottom, 14) + .background(GlucoseGuardTheme.ink.edgesIgnoringSafeArea(.bottom)) + } + + private func tabButton(_ title: String, systemName: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + VStack(spacing: 4) { + Image(systemName: systemName) + .font(.system(size: 18, weight: .semibold)) + Text(title) + .font(.caption2.weight(.semibold)) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + } +} + +struct GlucoseGuardScreenChrome: View { + let title: String + let content: Content + + init(_ title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + + var body: some View { + ZStack { + GlucoseGuardTheme.ink.ignoresSafeArea() + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 10) { + Image("glucose_guard_mark", bundle: Bundle(for: StatusBarHUDView.self)) + .resizable() + .frame(width: 36, height: 36) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + Text(title) + .font(.title2.bold()) + .foregroundColor(.white) + Spacer() + } + content + Spacer(minLength: 0) + } + .padding(20) + } + } +} + +struct GlucoseGuardCard: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(GlucoseGuardTheme.surface) + .clipShape(RoundedRectangle(cornerRadius: GlucoseGuardTheme.cardRadius, style: .continuous)) + } +} + +struct GlucoseGuardProgressBar: View { + let progress: Double + var tint: Color = GlucoseGuardTheme.red + + var body: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule().fill(Color.white.opacity(0.12)) + Capsule() + .fill(tint) + .frame(width: max(8, geo.size.width * CGFloat(min(max(progress, 0), 1)))) + } + } + .frame(height: 8) + } +} + +struct GlucoseGuardCGMDetailView: View { + let lastReading: String + let minutesAgo: String + let remainingDays: Double + let sensorLifeDays: Double + let average: String + let onOpenCGM: () -> Void + + var body: some View { + GlucoseGuardScreenChrome("CGM") { + GlucoseGuardCard { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "waveform.path.ecg") + .foregroundColor(GlucoseGuardTheme.red) + Text(String(format: NSLocalizedString("Sensor expires in %.0f days", comment: "CGM remaining life"), remainingDays)) + .font(.headline) + .foregroundColor(.white) + } + GlucoseGuardProgressBar(progress: 1 - (remainingDays / max(sensorLifeDays, 1))) + Text(String(format: NSLocalizedString("%.0f of %.0f days remaining", comment: "CGM life caption"), remainingDays, sensorLifeDays)) + .font(.footnote) + .foregroundColor(GlucoseGuardTheme.muted) + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Last Reading") + .font(.caption) + .foregroundColor(GlucoseGuardTheme.muted) + Text(lastReading) + .font(.title.bold()) + .foregroundColor(.white) + } + Spacer() + Label(minutesAgo, systemImage: "clock") + .foregroundColor(GlucoseGuardTheme.red) + } + HStack { + Text("AVG") + .foregroundColor(GlucoseGuardTheme.muted) + Spacer() + Text(average) + .foregroundColor(.white) + .font(.headline) + } + } + } + Button(action: onOpenCGM) { + Text("LibreLinkUp, Dexcom Share, or CGM settings") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(GlucoseGuardTheme.red) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + .padding(.top, 8) + } + } +} + +struct GlucoseGuardPumpDetailView: View { + let pumpName: String + let remainingDays: Double + let podLifeDays: Double + let basal: String + let remainingInsulin: String + let onChangeBolus: () -> Void + let onOpenPump: () -> Void + + var body: some View { + GlucoseGuardScreenChrome(pumpName) { + GlucoseGuardCard { + VStack(alignment: .leading, spacing: 12) { + Text(String(format: NSLocalizedString("Pod expires in %.1f days", comment: "Pump remaining life"), remainingDays)) + .font(.headline) + .foregroundColor(.white) + GlucoseGuardProgressBar(progress: 1 - (remainingDays / max(podLifeDays, 1)), tint: .orange) + Text(String(format: NSLocalizedString("%.1f of %.0f days remaining", comment: "Pump life caption"), remainingDays, podLifeDays)) + .font(.footnote) + .foregroundColor(GlucoseGuardTheme.muted) + HStack { + VStack(alignment: .leading) { + Text("Planned basal") + .font(.caption) + .foregroundColor(GlucoseGuardTheme.muted) + Text(basal) + .font(.headline) + .foregroundColor(.white) + } + Spacer() + VStack(alignment: .trailing) { + Text("Remaining insulin") + .font(.caption) + .foregroundColor(GlucoseGuardTheme.muted) + Text(remainingInsulin) + .font(.headline) + .foregroundColor(.orange) + } + } + } + } + Button(action: onChangeBolus) { + Text("Current bolus — change") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(GlucoseGuardTheme.red) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + Button(action: onOpenPump) { + Text("Open pump settings") + .font(.subheadline.weight(.semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + } + } + } +} + +struct GlucoseGuardLearningView: View { + let displayUnit: HKUnit + let loader: (Date, @escaping ([Double]) -> Void) -> Void + + var body: some View { + GlucoseGuardStatisticsView(displayUnit: displayUnit, loader: loader) + .background(GlucoseGuardTheme.ink.ignoresSafeArea()) + } +} + +struct GlucoseGuardHealthwayView: View { + let currentGlucose: String + let a1c: String + let average: String + let tir: String + let points: [Double] + + @State private var range = 1 + + var body: some View { + ZStack { + Color(red: 0.980, green: 0.980, blue: 0.980).ignoresSafeArea() + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 10) { + Image("glucose_guard_mark", bundle: Bundle(for: StatusBarHUDView.self)) + .resizable() + .frame(width: 36, height: 36) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + Text("Glucose Values") + .font(.title2.bold()) + Spacer() + } + Picker("Range", selection: $range) { + Text("Day").tag(1) + Text("Week").tag(7) + Text("Month").tag(30) + Text("Year").tag(365) + } + .pickerStyle(.segmented) + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Glucose") + .foregroundColor(GlucoseGuardTheme.muted) + Spacer() + Text(currentGlucose) + .foregroundColor(GlucoseGuardTheme.green) + .font(.headline) + } + GlucoseGuardSparkline(points: points) + .frame(height: 160) + HStack { + metric("A1C", a1c) + metric("AVG", average) + metric("TIR", tir) + } + } + .padding(16) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .shadow(color: Color.black.opacity(0.06), radius: 8, y: 3) + Spacer() + } + .padding(20) + } + } + + private func metric(_ title: String, _ value: String) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption) + .foregroundColor(GlucoseGuardTheme.muted) + Text(value) + .font(.headline) + .foregroundColor(GlucoseGuardTheme.red) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct GlucoseGuardSparkline: View { + let points: [Double] + + var body: some View { + GeometryReader { geo in + let values = points.isEmpty ? [90, 110, 95, 130, 100] : points + let minV = (values.min() ?? 0) - 10 + let maxV = (values.max() ?? 1) + 10 + let span = max(maxV - minV, 1) + Path { path in + for (index, value) in values.enumerated() { + let x = geo.size.width * CGFloat(index) / CGFloat(max(values.count - 1, 1)) + let y = geo.size.height * (1 - CGFloat((value - minV) / span)) + if index == 0 { + path.move(to: CGPoint(x: x, y: geo.size.height)) + path.addLine(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + path.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height)) + path.closeSubpath() + } + .fill(GlucoseGuardTheme.red.opacity(0.28)) + Path { path in + for (index, value) in values.enumerated() { + let x = geo.size.width * CGFloat(index) / CGFloat(max(values.count - 1, 1)) + let y = geo.size.height * (1 - CGFloat((value - minV) / span)) + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + } + .stroke(GlucoseGuardTheme.red, lineWidth: 2) + } + } +}