feat(pwa): installable app — manifest + SW + Settings install button
CI / test (push) Has been cancelled

The captive-portal Step-2 QR opens pictureframe.edholm.me in Safari,
which is the perfect moment to also offer "pin this to your home
screen" so the recipient gets one-tap access without typing the URL
again. Two pieces:

* Service worker at /sw.js (document root, scope "/"). Minimal —
  install/activate calls skipWaiting + clients.claim, fetch is
  passthrough. Real offline caching is intentionally out of scope;
  we only need the SW to exist so Chrome's PWA-install heuristic
  fires.

* Settings → Install app section, hidden when display-mode standalone.
  Android Chrome path: native beforeinstallprompt button.
  iOS Safari (and any other non-prompt browser): button opens a
  modal with step-by-step Share → Add to Home Screen instructions.

usePwaInstall composable handles the singleton lifecycle —
beforeinstallprompt fires once per page load and may fire before the
user navigates to Settings, so we register on module import and stash
the event for later.

Tests cover: install button rendered when not standalone, modal opens
on click without a native prompt, modal close button works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 13:49:12 -04:00
parent 2be153a103
commit 00121aaec9
25 changed files with 347 additions and 16 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{J as e,M as t,P as n,S as r,_ as i,b as a,g as o,h as s,p as c,t as l,x as u,y as d,z as f}from"./_plugin-vue_export-helper-eepT72yB.js";import{n as p,t as m}from"./BaseBottomSheet-BMI-Oljh.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=l(r({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(r,{emit:l}){let y=r,b=l;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=s(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(s,l)=>(t(),i(m,{"model-value":r.modelValue,label:`Choose frames`,"onUpdate:modelValue":l[1]||=e=>s.$emit(`update:modelValue`,e)},{default:f(()=>[l[2]||=o(`h2`,{class:`device-picker__title`},`Add to frames`,-1),l[3]||=o(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),o(`div`,h,[(t(!0),d(c,null,n(r.devices,n=>(t(),d(`label`,{key:n.id,class:`device-picker__row`},[o(`input`,{type:`checkbox`,class:`device-picker__check`,checked:r.selected.includes(n.id),onChange:e=>x(n.id)},null,40,g),o(`span`,_,e(n.name),1),o(`span`,v,e(n.orientation),1)]))),128))]),u(p,{variant:`primary`,class:`device-picker__confirm`,disabled:r.selected.length===0||r.uploading,onClick:l[0]||=e=>s.$emit(`confirm`)},{default:f(()=>[a(e(r.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
@@ -1 +0,0 @@
import{_ as e,d as t,f as n,g as r,j as i,k as a,m as o,pt as s,s as c,t as l,u,v as d,z as f}from"./_plugin-vue_export-helper-DRLwVS0w.js";import{n as p,t as m}from"./BaseBottomSheet-D5oDq9XR.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=l(d({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(l,{emit:d}){let y=l,b=d;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=u(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(u,d)=>(a(),n(m,{"model-value":l.modelValue,label:`Choose frames`,"onUpdate:modelValue":d[1]||=e=>u.$emit(`update:modelValue`,e)},{default:f(()=>[d[2]||=t(`h2`,{class:`device-picker__title`},`Add to frames`,-1),d[3]||=t(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),t(`div`,h,[(a(!0),o(c,null,i(l.devices,e=>(a(),o(`label`,{key:e.id,class:`device-picker__row`},[t(`input`,{type:`checkbox`,class:`device-picker__check`,checked:l.selected.includes(e.id),onChange:t=>x(e.id)},null,40,g),t(`span`,_,s(e.name),1),t(`span`,v,s(e.orientation),1)]))),128))]),e(p,{variant:`primary`,class:`device-picker__confirm`,disabled:l.selected.length===0||l.uploading,onClick:d[0]||=e=>u.$emit(`confirm`)},{default:f(()=>[r(s(l.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{F as e,H as t,K as n,M as r,S as i,g as a,h as o,q as s,t as c,y as l}from"./_plugin-vue_export-helper-eepT72yB.js";var u={key:1,class:`ptr__spinner`,role:`status`,"aria-label":`Refreshing`},d=c(i({__name:`PullToRefresh`,props:{isAtTop:{},onRefresh:{},threshold:{default:80},maxPull:{default:140}},setup(i){let c=i,d=t(0),f=t(!1),p=0,m=0,h=!1,g=null,_=o(()=>Math.min(d.value/c.threshold,1)),v=o(()=>f.value?1:Math.min(d.value/c.threshold,1)),y=o(()=>({transform:`translateY(${d.value}px)`,transition:h?`none`:`transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1)`}));function b(e){f.value||c.isAtTop&&!c.isAtTop()||e.touches.length===1&&(p=e.touches[0].clientY,m=e.touches[0].clientX,h=!0,g=null)}function x(e){if(!h)return;let t=e.touches[0].clientX-m,n=e.touches[0].clientY-p;if(g===null){if(Math.abs(t)<6&&Math.abs(n)<6)return;g=Math.abs(t)>Math.abs(n)?`x`:`y`}if(g===`x`||n<=0){h=!1,d.value=0;return}if(c.isAtTop&&!c.isAtTop()){h=!1,d.value=0;return}let r=n<c.maxPull?n*.5:c.maxPull*.5+(n-c.maxPull)*.1;d.value=Math.min(r,c.maxPull*.7),e.cancelable&&e.preventDefault()}async function S(){if(h)if(h=!1,d.value>=c.threshold){f.value=!0,d.value=c.threshold*.7;try{await c.onRefresh()}catch(e){console.error(`Pull-to-refresh failed:`,e)}finally{f.value=!1,d.value=0}}else d.value=0}return(t,i)=>(r(),l(`div`,{class:`ptr`,onTouchstartPassive:b,onTouchmove:x,onTouchend:S,onTouchcancel:S},[a(`div`,{class:n([`ptr__indicator`,{"ptr__indicator--ready":_.value>=1}]),style:s({opacity:v.value,transform:`translateY(${d.value*.6}px)`}),"aria-hidden":`true`},[f.value?(r(),l(`div`,u)):(r(),l(`svg`,{key:0,class:`ptr__arrow`,viewBox:`0 0 24 24`,style:s({transform:`rotate(${_.value*180}deg)`})},[...i[0]||=[a(`line`,{x1:`12`,y1:`5`,x2:`12`,y2:`19`,stroke:`currentColor`,"stroke-width":`2.5`,"stroke-linecap":`round`},null,-1),a(`polyline`,{points:`6 13 12 19 18 13`,stroke:`currentColor`,"stroke-width":`2.5`,fill:`none`,"stroke-linecap":`round`,"stroke-linejoin":`round`},null,-1)]],4))],6),a(`div`,{class:`ptr__content`,style:s(y.value)},[e(t.$slots,`default`,{},void 0,!0)],4)],32))}}),[[`__scopeId`,`data-v-e2242f3c`]]);export{d as t};
@@ -1 +0,0 @@
import{H as e,M as t,d as n,dt as r,ft as i,k as a,m as o,t as s,u as c,v as l}from"./_plugin-vue_export-helper-DRLwVS0w.js";var u={key:1,class:`ptr__spinner`,role:`status`,"aria-label":`Refreshing`},d=s(l({__name:`PullToRefresh`,props:{isAtTop:{},onRefresh:{},threshold:{default:80},maxPull:{default:140}},setup(s){let l=s,d=e(0),f=e(!1),p=0,m=0,h=!1,g=null,_=c(()=>Math.min(d.value/l.threshold,1)),v=c(()=>f.value?1:Math.min(d.value/l.threshold,1)),y=c(()=>({transform:`translateY(${d.value}px)`,transition:h?`none`:`transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1)`}));function b(e){f.value||l.isAtTop&&!l.isAtTop()||e.touches.length===1&&(p=e.touches[0].clientY,m=e.touches[0].clientX,h=!0,g=null)}function x(e){if(!h)return;let t=e.touches[0].clientX-m,n=e.touches[0].clientY-p;if(g===null){if(Math.abs(t)<6&&Math.abs(n)<6)return;g=Math.abs(t)>Math.abs(n)?`x`:`y`}if(g===`x`||n<=0){h=!1,d.value=0;return}if(l.isAtTop&&!l.isAtTop()){h=!1,d.value=0;return}let r=n<l.maxPull?n*.5:l.maxPull*.5+(n-l.maxPull)*.1;d.value=Math.min(r,l.maxPull*.7),e.cancelable&&e.preventDefault()}async function S(){if(h)if(h=!1,d.value>=l.threshold){f.value=!0,d.value=l.threshold*.7;try{await l.onRefresh()}catch(e){console.error(`Pull-to-refresh failed:`,e)}finally{f.value=!1,d.value=0}}else d.value=0}return(e,s)=>(a(),o(`div`,{class:`ptr`,onTouchstartPassive:b,onTouchmove:x,onTouchend:S,onTouchcancel:S},[n(`div`,{class:r([`ptr__indicator`,{"ptr__indicator--ready":_.value>=1}]),style:i({opacity:v.value,transform:`translateY(${d.value*.6}px)`}),"aria-hidden":`true`},[f.value?(a(),o(`div`,u)):(a(),o(`svg`,{key:0,class:`ptr__arrow`,viewBox:`0 0 24 24`,style:i({transform:`rotate(${_.value*180}deg)`})},[...s[0]||=[n(`line`,{x1:`12`,y1:`5`,x2:`12`,y2:`19`,stroke:`currentColor`,"stroke-width":`2.5`,"stroke-linecap":`round`},null,-1),n(`polyline`,{points:`6 13 12 19 18 13`,stroke:`currentColor`,"stroke-width":`2.5`,fill:`none`,"stroke-linecap":`round`,"stroke-linejoin":`round`},null,-1)]],4))],6),n(`div`,{class:`ptr__content`,style:i(y.value)},[t(e.$slots,`default`,{},void 0,!0)],4)],32))}}),[[`__scopeId`,`data-v-e2242f3c`]]);export{d as t};
@@ -1 +0,0 @@
import{K as e,d as t,dt as n,ft as r,j as i,k as a,m as o,p as s,pt as c,s as l,t as u,u as d,v as f}from"./_plugin-vue_export-helper-DRLwVS0w.js";import{n as p,r as m,t as h}from"./index-CCy85pot.js";var g={class:`settings`},_={class:`settings__section`},v={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},y=[`aria-checked`,`aria-label`,`onClick`],b={class:`theme-swatch__label`},x={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},S={class:`settings__section`},C={class:`settings__row`},w={class:`settings__row-value`},T=u(f({__name:`SettingsView`,setup(u){let f=m(),{saveTheme:T}=p(),E=d(()=>f.user?.theme??`warm-craft`);function D(e){T(e)}return(u,d)=>(a(),o(`main`,g,[d[5]||=t(`h1`,{class:`settings__title`},`Settings`,-1),t(`section`,_,[d[1]||=t(`h2`,{class:`settings__section-title`},`Theme`,-1),t(`div`,v,[(a(!0),o(l,null,i(e(h),e=>(a(),o(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":E.value===e.id,"aria-label":e.label,class:n([`theme-swatch`,{"theme-swatch--active":E.value===e.id}]),style:r({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>D(e.id)},[d[0]||=t(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[t(`span`,{class:`theme-swatch__bar`}),t(`span`,{class:`theme-swatch__dot`})],-1),t(`span`,b,c(e.label),1),E.value===e.id?(a(),o(`span`,x,``)):s(``,!0)],14,y))),128))])]),t(`section`,S,[d[3]||=t(`h2`,{class:`settings__section-title`},`Account`,-1),t(`div`,C,[d[2]||=t(`span`,{class:`settings__row-label`},`Signed in as`,-1),t(`span`,w,c(e(f).user?.email),1)]),d[4]||=t(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)])]))}}),[[`__scopeId`,`data-v-76ec3881`]]);export{T as default};
@@ -1 +0,0 @@
.settings[data-v-76ec3881]{padding:var(--space-4) var(--space-4) calc(var(--bottom-nav-height) + var(--space-6));max-width:480px;margin:0 auto}.settings__title[data-v-76ec3881]{font-size:var(--text-xl);margin-bottom:var(--space-6);font-weight:700}.settings__section[data-v-76ec3881]{margin-bottom:var(--space-6)}.settings__section-title[data-v-76ec3881]{font-size:var(--text-sm);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:var(--space-3);font-weight:700}.settings__row[data-v-76ec3881]{padding:var(--space-3) 0;border-bottom:1px solid var(--color-border);font-size:var(--text-base);justify-content:space-between;align-items:center;display:flex}.settings__row-label[data-v-76ec3881]{color:var(--color-text-muted)}.settings__row-value[data-v-76ec3881]{font-weight:600}.settings__logout[data-v-76ec3881]{min-height:var(--touch-min);padding:var(--space-3) 0;color:var(--color-destructive);font-weight:600;font-size:var(--text-base);align-items:center;text-decoration:none;display:flex}.theme-grid[data-v-76ec3881]{gap:var(--space-3);grid-template-columns:repeat(3,1fr);display:grid}.theme-swatch[data-v-76ec3881]{align-items:center;gap:var(--space-2);padding:var(--space-3);background:var(--swatch-bg);border-radius:var(--radius-md);cursor:pointer;transition:border-color var(--duration-fast);min-height:var(--touch-min);border:2px solid #0000;flex-direction:column;display:flex;position:relative}.theme-swatch--active[data-v-76ec3881]{border-color:var(--swatch-primary)}.theme-swatch__preview[data-v-76ec3881]{border-radius:var(--radius-sm);background:var(--swatch-bg);border:1px solid color-mix(in srgb, var(--swatch-text) 15%, transparent);flex-direction:column;justify-content:center;gap:4px;width:100%;height:36px;padding:0 6px;display:flex}.theme-swatch__bar[data-v-76ec3881]{background:var(--swatch-primary);border-radius:3px;width:60%;height:6px;display:block}.theme-swatch__dot[data-v-76ec3881]{background:var(--swatch-text);opacity:.4;border-radius:2px;width:80%;height:4px;display:block}.theme-swatch__label[data-v-76ec3881]{font-size:var(--text-xs);color:var(--color-text);text-align:center;font-weight:600;line-height:1.2}.theme-swatch__check[data-v-76ec3881]{top:var(--space-1);right:var(--space-2);font-size:var(--text-sm);color:var(--swatch-primary);font-weight:700;position:absolute}
@@ -0,0 +1 @@
import{G as e,H as t,J as n,K as r,M as i,P as a,S as o,b as s,f as c,g as l,h as u,p as d,q as f,t as p,v as m,y as h}from"./_plugin-vue_export-helper-eepT72yB.js";import{n as g,r as _,t as v}from"./index-BO5caB_f.js";var y=t(null),b=t(!1);function x(){return typeof window>`u`?!1:window.matchMedia?.(`(display-mode: standalone)`).matches?!0:window.navigator.standalone===!0}function S(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent,t=e.includes(`Mac`)&&navigator.maxTouchPoints>1;return/iPhone|iPod/.test(e)||t}var C=!1;function w(){C||typeof window>`u`||(C=!0,b.value=x(),window.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),y.value=e}),window.addEventListener(`appinstalled`,()=>{y.value=null,b.value=!0}),window.matchMedia?.(`(display-mode: standalone)`).addEventListener(`change`,e=>{b.value=e.matches}))}w();function T(){let e=S(),t=u(()=>y.value!==null);async function n(){let e=y.value;if(!e)return!1;await e.prompt();let t=await e.userChoice;return y.value=null,t.outcome===`accepted`}return{isStandalone:b,isIOS:e,canPromptInstall:t,install:n}}var E={class:`settings`},D={key:0,class:`settings__section`},O={class:`settings__section`},k={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},A=[`aria-checked`,`aria-label`,`onClick`],j={class:`theme-swatch__label`},M={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},N={class:`settings__section`},P={class:`settings__row`},F={class:`settings__row-value`},I={class:`install-modal__card`},L={id:`install-modal-title`,class:`install-modal__title`},R={class:`install-modal__steps`},z={key:0},B={key:1},V={key:0},H=p(o({__name:`SettingsView`,setup(o){let p=_(),{saveTheme:y}=g(),{isStandalone:b,isIOS:x,canPromptInstall:S,install:C}=T(),w=u(()=>p.user?.theme??`warm-craft`),H=t(!1);function U(e){y(e)}async function W(){!await C()&&!S.value&&(H.value=!0)}return(t,o)=>(i(),h(`main`,E,[o[18]||=l(`h1`,{class:`settings__title`},`Settings`,-1),e(b)?m(``,!0):(i(),h(`section`,D,[o[3]||=l(`h2`,{class:`settings__section-title`},`Install app`,-1),o[4]||=l(`p`,{class:`settings__hint`},` Pin pictureFrame to your home screen so it opens like a native app. `,-1),e(S)?(i(),h(`button`,{key:0,type:`button`,class:`settings__install`,onClick:W},` Install pictureFrame `)):(i(),h(`button`,{key:1,type:`button`,class:`settings__install`,onClick:o[0]||=e=>H.value=!0},` Add to Home Screen `))])),l(`section`,O,[o[6]||=l(`h2`,{class:`settings__section-title`},`Theme`,-1),l(`div`,k,[(i(!0),h(d,null,a(e(v),e=>(i(),h(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":w.value===e.id,"aria-label":e.label,class:r([`theme-swatch`,{"theme-swatch--active":w.value===e.id}]),style:f({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>U(e.id)},[o[5]||=l(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[l(`span`,{class:`theme-swatch__bar`}),l(`span`,{class:`theme-swatch__dot`})],-1),l(`span`,j,n(e.label),1),w.value===e.id?(i(),h(`span`,M,``)):m(``,!0)],14,A))),128))])]),l(`section`,N,[o[8]||=l(`h2`,{class:`settings__section-title`},`Account`,-1),l(`div`,P,[o[7]||=l(`span`,{class:`settings__row-label`},`Signed in as`,-1),l(`span`,F,n(e(p).user?.email),1)]),o[9]||=l(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)]),H.value?(i(),h(`div`,{key:1,class:`install-modal`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`install-modal-title`,onClick:o[2]||=c(e=>H.value=!1,[`self`])},[l(`div`,I,[l(`button`,{type:`button`,class:`install-modal__close`,"aria-label":`Close`,onClick:o[1]||=e=>H.value=!1},`×`),l(`h2`,L,n(e(x)?`Add to your iPhone home screen`:`Add to your home screen`),1),l(`ol`,R,[e(x)?(i(),h(`li`,z,[...o[10]||=[s(` Tap the `,-1),l(`strong`,null,`Share`,-1),s(` icon at the bottom of Safari (the square with an up-arrow). `,-1)]])):(i(),h(`li`,B,[...o[11]||=[s(` Open your browser's menu (usually the three dots `,-1),l(`strong`,null,``,-1),s(` in the top right). `,-1)]])),l(`li`,null,[o[13]||=s(` Scroll down and tap `,-1),o[14]||=l(`strong`,null,`Add to Home Screen`,-1),e(x)?m(``,!0):(i(),h(`span`,V,[...o[12]||=[s(`or `,-1),l(`strong`,null,`Install app`,-1)]])),o[15]||=s(`. `,-1)]),o[16]||=l(`li`,null,[s(` Tap `),l(`strong`,null,`Add`),s(` in the top right to confirm. `)],-1)]),o[17]||=l(`p`,{class:`install-modal__footer`},` The app will appear on your home screen and open without browser chrome the next time you launch it. `,-1)])])):m(``,!0)]))}}),[[`__scopeId`,`data-v-6f8a8b72`]]);export{H as default};
@@ -0,0 +1 @@
.settings[data-v-6f8a8b72]{padding:var(--space-4) var(--space-4) calc(var(--bottom-nav-height) + var(--space-6));max-width:480px;margin:0 auto}.settings__title[data-v-6f8a8b72]{font-size:var(--text-xl);margin-bottom:var(--space-6);font-weight:700}.settings__section[data-v-6f8a8b72]{margin-bottom:var(--space-6)}.settings__section-title[data-v-6f8a8b72]{font-size:var(--text-sm);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:var(--space-3);font-weight:700}.settings__row[data-v-6f8a8b72]{padding:var(--space-3) 0;border-bottom:1px solid var(--color-border);font-size:var(--text-base);justify-content:space-between;align-items:center;display:flex}.settings__row-label[data-v-6f8a8b72]{color:var(--color-text-muted)}.settings__row-value[data-v-6f8a8b72]{font-weight:600}.settings__logout[data-v-6f8a8b72]{min-height:var(--touch-min);padding:var(--space-3) 0;color:var(--color-destructive);font-weight:600;font-size:var(--text-base);align-items:center;text-decoration:none;display:flex}.settings__hint[data-v-6f8a8b72]{color:var(--color-text-muted);font-size:var(--text-sm);margin-bottom:var(--space-3);line-height:1.4}.settings__install[data-v-6f8a8b72]{width:100%;min-height:var(--touch-min);background:var(--color-primary);color:var(--color-on-primary);border-radius:var(--radius-pill,9999px);font-size:var(--text-base);cursor:pointer;border:none;justify-content:center;align-items:center;font-weight:700;display:flex}.install-modal[data-v-6f8a8b72]{padding:var(--space-4);z-index:100;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.install-modal__card[data-v-6f8a8b72]{background:var(--color-surface);color:var(--color-text);border-radius:var(--radius-lg,16px);padding:var(--space-5);width:100%;max-width:380px;position:relative;box-shadow:0 20px 60px #00000040}.install-modal__close[data-v-6f8a8b72]{top:var(--space-2);right:var(--space-3);color:var(--color-text-muted);cursor:pointer;padding:var(--space-1) var(--space-2);background:0 0;border:none;font-size:1.75rem;line-height:1;position:absolute}.install-modal__title[data-v-6f8a8b72]{font-size:var(--text-lg);margin-bottom:var(--space-3);padding-right:var(--space-5);font-weight:700}.install-modal__steps[data-v-6f8a8b72]{margin:0 0 var(--space-3) var(--space-4);padding:0;line-height:1.5}.install-modal__steps li[data-v-6f8a8b72]{margin-bottom:var(--space-2)}.install-modal__footer[data-v-6f8a8b72]{color:var(--color-text-muted);font-size:var(--text-sm);margin-top:var(--space-3);border-top:1px solid var(--color-border);padding-top:var(--space-3);line-height:1.4}.theme-grid[data-v-6f8a8b72]{gap:var(--space-3);grid-template-columns:repeat(3,1fr);display:grid}.theme-swatch[data-v-6f8a8b72]{align-items:center;gap:var(--space-2);padding:var(--space-3);background:var(--swatch-bg);border-radius:var(--radius-md);cursor:pointer;transition:border-color var(--duration-fast);min-height:var(--touch-min);border:2px solid #0000;flex-direction:column;display:flex;position:relative}.theme-swatch--active[data-v-6f8a8b72]{border-color:var(--swatch-primary)}.theme-swatch__preview[data-v-6f8a8b72]{border-radius:var(--radius-sm);background:var(--swatch-bg);border:1px solid color-mix(in srgb, var(--swatch-text) 15%, transparent);flex-direction:column;justify-content:center;gap:4px;width:100%;height:36px;padding:0 6px;display:flex}.theme-swatch__bar[data-v-6f8a8b72]{background:var(--swatch-primary);border-radius:3px;width:60%;height:6px;display:block}.theme-swatch__dot[data-v-6f8a8b72]{background:var(--swatch-text);opacity:.4;border-radius:2px;width:80%;height:4px;display:block}.theme-swatch__label[data-v-6f8a8b72]{font-size:var(--text-xs);color:var(--color-text);text-align:center;font-weight:600;line-height:1.2}.theme-swatch__check[data-v-6f8a8b72]{top:var(--space-1);right:var(--space-2);font-size:var(--text-sm);color:var(--swatch-primary);font-weight:700;position:absolute}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -14,8 +14,8 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
<script type="module" crossorigin src="/build/assets/index-CCy85pot.js"></script>
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-DRLwVS0w.js">
<script type="module" crossorigin src="/build/assets/index-BO5caB_f.js"></script>
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-eepT72yB.js">
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
</head>
<body>
+25
View File
@@ -0,0 +1,25 @@
// Minimal service worker — present so Chrome's PWA install criteria
// pass and the beforeinstallprompt event fires. Real offline caching
// is intentionally out of scope; we don't pre-cache the SPA shell or
// intercept fetches in any meaningful way.
//
// Lives at /sw.js (document root, not /build/sw.js) so its default
// scope is /, matching the manifest's "scope": "/". Don't move it
// without setting Service-Worker-Allowed: / on the response.
self.addEventListener('install', () => {
// Activate immediately, don't wait for old tabs to close
self.skipWaiting()
})
self.addEventListener('activate', (event) => {
// Take control of all clients (including ones that loaded before
// this SW activated) so subsequent navigations go through us.
event.waitUntil(self.clients.claim())
})
self.addEventListener('fetch', () => {
// Passthrough — we don't call event.respondWith(), so the browser
// proceeds with its normal network fetch. The handler existing at
// all is what Chrome's PWA install heuristic looks for.
})