From 5fcfb806bee27a092694e254f67f92f6e4586c9e Mon Sep 17 00:00:00 2001 From: Matt Edholm Date: Wed, 6 May 2026 18:07:05 -0400 Subject: [PATCH] feat: ship as a real iOS-installable PWA, restructure bottom nav, fix safe-area - Add manifest.webmanifest with standalone display + warm-craft theme colors, apple-touch-icon, and 192/512/512-maskable icons (frame-with-sunset glyph). - Add PWA meta tags + viewport-fit=cover so add-to-home-screen produces a true standalone app on iOS instead of a Safari bookmark. - Drop the Shared bottom-nav tab; the in-page sub-tabs already cover that. Three nav tabs total (Home / Library / Settings); pending-share badge moves to the Library tab. Predicate-based isActive() now correctly disambiguates /library vs /library?tab=shared. - Safe-area handling: bottom nav, bottom sheet, upload overlay, and #app respect env(safe-area-inset-*); sticky Library tabs anchor below the iPhone status bar. Introduces --bottom-nav-height token consumed by Settings, Library, and the toast. - LibraryView reactively follows route.query.tab so deep-linking /library?tab=shared lands on the right sub-tab. - Theme-color meta syncs client-side via useTheme.applyTheme so the user's chosen theme follows them into Android Chrome's chrome bar. Test suite expanded to 278 tests / 100% line coverage (99.84% statements, 99.78% branches). Remaining gaps are unreachable defensive code. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/index.html | 13 +- frontend/public/icons/apple-touch-icon.png | Bin 0 -> 1119 bytes frontend/public/icons/icon-192.png | Bin 0 -> 1096 bytes frontend/public/icons/icon-512-maskable.png | Bin 0 -> 3289 bytes frontend/public/icons/icon-512.png | Bin 0 -> 3221 bytes frontend/public/manifest.webmanifest | 32 + frontend/src/App.vue | 7 +- frontend/src/components/BaseBottomSheet.vue | 2 +- frontend/src/components/BottomNav.vue | 36 +- frontend/src/composables/useTheme.ts | 6 + frontend/src/styles/global.scss | 4 + frontend/src/test/App.test.ts | 79 ++ .../src/test/components/ApproveCard.test.ts | 144 ++++ .../test/components/BaseBottomSheet.test.ts | 73 ++ frontend/src/test/components/BaseCard.test.ts | 16 + frontend/src/test/components/BaseChip.test.ts | 27 + .../src/test/components/BaseInput.test.ts | 58 ++ .../src/test/components/BaseToast.test.ts | 44 ++ .../src/test/components/BottomNav.test.ts | 106 +++ .../src/test/components/DevicePicker.test.ts | 40 + .../test/components/OrientationPicker.test.ts | 36 + .../src/test/components/ShareSheet.test.ts | 45 ++ .../src/test/components/StickerTray.test.ts | 53 ++ .../src/test/composables/useTheme.test.ts | 113 +++ frontend/src/test/setup.ts | 3 + frontend/src/test/stores/devices.test.ts | 46 ++ frontend/src/test/stores/images.test.ts | 233 ++++++ frontend/src/test/stores/upload.test.ts | 72 ++ frontend/src/test/views/HomeView.test.ts | 308 +++++++- frontend/src/test/views/LibraryView.test.ts | 707 +++++++++++++++++- frontend/src/test/views/SettingsView.test.ts | 65 ++ frontend/src/test/views/UploadView.test.ts | 512 +++++++++++++ frontend/src/views/LibraryView.vue | 19 +- frontend/src/views/SettingsView.vue | 2 +- frontend/src/views/UploadView.vue | 2 + frontend/vite.config.ts | 4 + ...zdI27OS.js => BaseBottomSheet-CO3Iefke.js} | 2 +- ...arQ8K.css => BaseBottomSheet-DYtuubxf.css} | 2 +- ...r-BjuU2ONb.js => DevicePicker-CfQ8y-l0.js} | 2 +- ...eView-CGlR0dxG.js => HomeView-3fATGLq0.js} | 2 +- public/build/assets/LibraryView-B6wq42Vx.js | 1 - public/build/assets/LibraryView-Bf9KbHdL.css | 1 + public/build/assets/LibraryView-BvFubSso.js | 1 + public/build/assets/LibraryView-C18KukkR.css | 1 - public/build/assets/SettingsView-BGXX7ONa.css | 1 + public/build/assets/SettingsView-CWIIisVW.css | 1 - ...w-DuiAU77B.js => SettingsView-CYdyGgyj.js} | 2 +- ...w-BdEsaB2u.css => UploadView-CH8tyGyv.css} | 2 +- ...iew-CPf-sdFJ.js => UploadView-lwth3Nb9.js} | 2 +- public/build/assets/index-BlLBHR1q.css | 1 + .../{index-DCUs53vX.js => index-D13oAsTG.js} | 4 +- public/build/assets/index-DlN2hqev.css | 1 - public/build/icons/apple-touch-icon.png | Bin 0 -> 1119 bytes public/build/icons/icon-192.png | Bin 0 -> 1096 bytes public/build/icons/icon-512-maskable.png | Bin 0 -> 3289 bytes public/build/icons/icon-512.png | Bin 0 -> 3221 bytes public/build/index.html | 17 +- public/build/manifest.webmanifest | 32 + 58 files changed, 2922 insertions(+), 60 deletions(-) create mode 100644 frontend/public/icons/apple-touch-icon.png create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512-maskable.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/src/test/App.test.ts create mode 100644 frontend/src/test/components/ApproveCard.test.ts create mode 100644 frontend/src/test/components/BaseBottomSheet.test.ts create mode 100644 frontend/src/test/components/BaseCard.test.ts create mode 100644 frontend/src/test/components/BaseChip.test.ts create mode 100644 frontend/src/test/components/BaseInput.test.ts create mode 100644 frontend/src/test/components/BaseToast.test.ts create mode 100644 frontend/src/test/components/BottomNav.test.ts create mode 100644 frontend/src/test/components/OrientationPicker.test.ts create mode 100644 frontend/src/test/components/StickerTray.test.ts create mode 100644 frontend/src/test/composables/useTheme.test.ts create mode 100644 frontend/src/test/views/SettingsView.test.ts create mode 100644 frontend/src/test/views/UploadView.test.ts rename public/build/assets/{BaseBottomSheet-CzdI27OS.js => BaseBottomSheet-CO3Iefke.js} (96%) rename public/build/assets/{BaseBottomSheet-MPNarQ8K.css => BaseBottomSheet-DYtuubxf.css} (56%) rename public/build/assets/{DevicePicker-BjuU2ONb.js => DevicePicker-CfQ8y-l0.js} (96%) rename public/build/assets/{HomeView-CGlR0dxG.js => HomeView-3fATGLq0.js} (98%) delete mode 100644 public/build/assets/LibraryView-B6wq42Vx.js create mode 100644 public/build/assets/LibraryView-Bf9KbHdL.css create mode 100644 public/build/assets/LibraryView-BvFubSso.js delete mode 100644 public/build/assets/LibraryView-C18KukkR.css create mode 100644 public/build/assets/SettingsView-BGXX7ONa.css delete mode 100644 public/build/assets/SettingsView-CWIIisVW.css rename public/build/assets/{SettingsView-DuiAU77B.js => SettingsView-CYdyGgyj.js} (90%) rename public/build/assets/{UploadView-BdEsaB2u.css => UploadView-CH8tyGyv.css} (70%) rename public/build/assets/{UploadView-CPf-sdFJ.js => UploadView-lwth3Nb9.js} (98%) create mode 100644 public/build/assets/index-BlLBHR1q.css rename public/build/assets/{index-DCUs53vX.js => index-D13oAsTG.js} (97%) delete mode 100644 public/build/assets/index-DlN2hqev.css create mode 100644 public/build/icons/apple-touch-icon.png create mode 100644 public/build/icons/icon-192.png create mode 100644 public/build/icons/icon-512-maskable.png create mode 100644 public/build/icons/icon-512.png create mode 100644 public/build/manifest.webmanifest diff --git a/frontend/index.html b/frontend/index.html index 096d706..9c4c796 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,18 @@ + + pictureFrame + - - frontend + + + + + + + +
diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..313b1d61b5eb3e71f2b101d85282bef191c5d1cd GIT binary patch literal 1119 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWiw)6U{UjQaSW-r_4e+`BHl!ahKF`@ zBQ#RevX`!yA-t2P+I7Pwg+r`Qm|_|VlT-IJ#x$h{B*}$s>#!7iwM2IcEBE1-c6aOR z|JsDE(h-@v|Ng&oHsfojdBYWJR%|u;#0nGcH|Vp>NYCbEx`%)I2;T&m_ZevX}efm34)``{x$63zt8C zwe!ikm0#TNhx$M7T)cehC8P57bJu_QuKoFAg^_U8qZ4QP=ZpL+y}#{e<@+N&)~yS29{WMiI>&s?KNi`FWb>zQpUGuc(tBR}sPm$sY5thhYFvls0>WrN9=!-UYlFYh(gU|NASdZQ<+%YBX=X(8l@p zb{Wu`n+s%ZCYEKmCO>7+W^W2BncQ{#`~P3rRkOcatyof)p?!S)JslpozsqdToj*0P zV>|a%TOR4w+@~2jPuLGvTbR8QD`B>FFZZwc`0#`LG~V4Txeoj{cej7FH+fyoxoh6M z-JvD#?=C(jwg_%8OM5%ltqy}_tas-!-d#7R z`5v=&`y$r2PtToF%&>iJ`HpYxi)ZG1!nZ067O}2Pw=GwAF=P49+!XI^i~g??y)OAx z?uUP~!NSwC_Ez3LX})S+_{7J&f0F&wOv3m;37Dl9nt;*MGALQI@Pd=I;{~8P2vZQG z1;kVaC;up+FCT4B`^gDQwTXRs@@9_a*$eO9FQ}b)VaA2B%%HvNmp19`F6)Ro6IwpC z{BNh)wOSe9w+8EUB3`%J?1-z++^F$;_L=#%57q^BZpvO$=%G=`FVdQ&MBb@0N%q2EC2ui literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..6b2df89a7ca8d668a38df34b1281b4c3ec62993e GIT binary patch literal 1096 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 literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1248f147e4a6fa75e58878d664247068b5e8ca GIT binary patch literal 3221 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 R=06}1I&u1h { const stamped = document.documentElement.dataset.theme - if (stamped && auth.user) { - auth.user.theme = stamped - } else if (auth.user?.theme) { - applyTheme(auth.user.theme) - } + const resolved = stamped || auth.user?.theme + if (resolved) applyTheme(resolved) }) diff --git a/frontend/src/components/BaseBottomSheet.vue b/frontend/src/components/BaseBottomSheet.vue index f954c32..0075bba 100644 --- a/frontend/src/components/BaseBottomSheet.vue +++ b/frontend/src/components/BaseBottomSheet.vue @@ -68,7 +68,7 @@ watch(() => props.modelValue, async (open) => { width: 100%; background: var(--color-surface); border-radius: var(--radius-lg) var(--radius-lg) 0 0; - padding: var(--space-3) var(--space-4) var(--space-6); + padding: var(--space-3) var(--space-4) calc(var(--space-6) + env(safe-area-inset-bottom)); max-height: 90dvh; overflow-y: auto; outline: none; diff --git a/frontend/src/components/BottomNav.vue b/frontend/src/components/BottomNav.vue index 2dcb63b..6451c3d 100644 --- a/frontend/src/components/BottomNav.vue +++ b/frontend/src/components/BottomNav.vue @@ -4,13 +4,13 @@ v-for="tab in tabs" :key="tab.name" :to="tab.to" - :class="['bottom-nav__tab', { 'bottom-nav__tab--active': isActive(tab.to) }]" + :class="['bottom-nav__tab', { 'bottom-nav__tab--active': tab.isActive(route) }]" :aria-label="tab.label" - :aria-current="isActive(tab.to) ? 'page' : undefined" + :aria-current="tab.isActive(route) ? 'page' : undefined" >