@signalk/app-dock 1.0.0 → 1.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +5 -0
- package/.github/workflows/stale.yml +42 -0
- package/README.md +22 -0
- package/package.json +1 -1
- package/plugin/index.js +10 -0
- package/public/805.js +1 -1
- package/public/app-icon-180.png +0 -0
- package/public/app-icon-maskable.svg +24 -0
- package/public/dock.js +43 -0
- package/public/index.html +9 -0
- package/public/manifest.webmanifest +31 -0
- package/src/configpanel/PluginConfigurationPanel.js +43 -17
- package/test/plugin.test.js +18 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Marks issues labeled "awaiting response" as stale after 30 days of inactivity
|
|
2
|
+
# and closes them 30 days after that (60 days total). Issues labeled
|
|
3
|
+
# "good first task" or "help wanted" are exempt. Pull requests are never
|
|
4
|
+
# touched. The workflow runs daily and can also be triggered manually.
|
|
5
|
+
name: Stale issues
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '17 1 * * *'
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
issues: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: stale
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
stale:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
timeout-minutes: 10
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/stale@v10
|
|
26
|
+
with:
|
|
27
|
+
days-before-issue-stale: 30
|
|
28
|
+
days-before-issue-close: 30
|
|
29
|
+
stale-issue-label: 'stale'
|
|
30
|
+
any-of-issue-labels: 'awaiting response'
|
|
31
|
+
exempt-issue-labels: 'good first task,help wanted'
|
|
32
|
+
stale-issue-message: >
|
|
33
|
+
This issue is marked as awaiting a response from the reporter and
|
|
34
|
+
has had no activity for 30 days. It will be closed in another 30
|
|
35
|
+
days if there is no further update. If the issue is still
|
|
36
|
+
relevant, please reply with any missing information.
|
|
37
|
+
close-issue-message: >
|
|
38
|
+
Closing this issue because it has been stale for 60 days without
|
|
39
|
+
a response. Please reopen if this is still relevant.
|
|
40
|
+
days-before-pr-stale: -1
|
|
41
|
+
days-before-pr-close: -1
|
|
42
|
+
operations-per-run: 100
|
package/README.md
CHANGED
|
@@ -35,6 +35,18 @@ Tip: double-tapping a clickable element in the underlying webapp will activate t
|
|
|
35
35
|
|
|
36
36
|
Config changes made in **Plugin Config** apply automatically within ~5 seconds: structural changes (position, icon size, etc.) reload the dock page; changes to the app list hot-update in place.
|
|
37
37
|
|
|
38
|
+
## Install as an app
|
|
39
|
+
|
|
40
|
+
The dock ships a web app manifest, so iOS and Android can install it to the home screen and launch it like a native app -- no Safari/Chrome chrome, no URL bar.
|
|
41
|
+
|
|
42
|
+
- **iPad / iPhone (Safari)**: open the dock URL, tap the Share icon, choose **Add to Home Screen**.
|
|
43
|
+
- **Android (Chrome)**: open the dock URL, tap the ⋮ menu, choose **Install app** (or **Add to Home Screen**).
|
|
44
|
+
- **Desktop (Chrome, Edge)**: an install icon appears in the address bar; click it.
|
|
45
|
+
|
|
46
|
+
HTTPS is **not required** for Add-to-Home-Screen -- the dock installs cleanly over plain `http://your-sk-server:3000/`. It works equally over HTTPS with a self-signed or local-CA cert, provided the device has been configured to trust that CA (the usual iPad profile flow: Settings > General > VPN & Device Management > trust the profile > Settings > General > About > Certificate Trust Settings > enable full trust).
|
|
47
|
+
|
|
48
|
+
If the installed dock ever displays a blank screen after a long idle period (rare; only if the server's caching layer ignores the dock's `no-cache` directives), remove the home-screen icon and re-add it from Safari.
|
|
49
|
+
|
|
38
50
|
## Configuration
|
|
39
51
|
|
|
40
52
|
Open **Plugin Config > App Dock** in the admin UI. The embedded configurator provides:
|
|
@@ -87,6 +99,16 @@ The repo ships three cooperating surfaces:
|
|
|
87
99
|
| `npm run lint` | ESLint check |
|
|
88
100
|
| `npm run build:config` | Rebuild the admin UI config panel (required after `src/` changes) |
|
|
89
101
|
|
|
102
|
+
### Icon assets
|
|
103
|
+
|
|
104
|
+
If `public/app-icon.svg` changes, regenerate the 180-px apple-touch-icon:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
rsvg-convert -w 180 -h 180 public/app-icon.svg -o public/app-icon-180.png
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`public/app-icon-maskable.svg` is a hand-authored wrapper that places the artwork inside Android's 80% safe zone; if the main icon's layout changes meaningfully, update it by hand to match.
|
|
111
|
+
|
|
90
112
|
## License
|
|
91
113
|
|
|
92
114
|
Apache-2.0
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -51,6 +51,7 @@ module.exports = (app) => {
|
|
|
51
51
|
const seeded = DEFAULT_APPS.map((a) => ({ ...a }))
|
|
52
52
|
const seededSettings = {
|
|
53
53
|
showNightModeButton: true,
|
|
54
|
+
showFullscreenButton: false,
|
|
54
55
|
showExitButton: false,
|
|
55
56
|
...settings,
|
|
56
57
|
apps: seeded
|
|
@@ -176,6 +177,15 @@ module.exports = (app) => {
|
|
|
176
177
|
default: true
|
|
177
178
|
},
|
|
178
179
|
|
|
180
|
+
showFullscreenButton: {
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
title: 'Show fullscreen toggle',
|
|
183
|
+
description:
|
|
184
|
+
'Adds a button to the dock that toggles browser fullscreen. ' +
|
|
185
|
+
'Hidden automatically when the Fullscreen API is not available (e.g. iPadOS Safari — use Add to Home Screen instead).',
|
|
186
|
+
default: false
|
|
187
|
+
},
|
|
188
|
+
|
|
179
189
|
showExitButton: {
|
|
180
190
|
type: 'boolean',
|
|
181
191
|
title: 'Show exit button',
|
package/public/805.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(self.webpackChunk_signalk_app_dock=self.webpackChunk_signalk_app_dock||[]).push([[805],{805(e,t,n){n.r(t),n.d(t,{default:()=>s});var a=n(231);const l={root:{fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',color:"#333",padding:"16px 0"},sectionTitle:{fontSize:13,fontWeight:600,color:"#888",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:10,marginTop:24},btn:{display:"inline-flex",alignItems:"center",gap:8,padding:"8px 16px",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},btnPrimary:{background:"#3b82f6",color:"#fff"},btnSave:{background:"#10b981",color:"#fff"},btnDanger:{background:"#ef4444",color:"#fff",padding:"4px 10px",fontSize:11},status:{marginTop:8,fontSize:12,minHeight:18},item:{display:"flex",alignItems:"center",gap:10,padding:"8px 12px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:8,marginBottom:6,cursor:"grab"},itemDisabled:{opacity:.4},dragOver:{borderColor:"#3b82f6",background:"#eef4ff"},handle:{color:"#bbb",fontSize:16,cursor:"grab",userSelect:"none",flexShrink:0,width:18,textAlign:"center"},iconBox:{width:36,height:36,borderRadius:8,display:"flex",alignItems:"center",justifyContent:"center",background:"#e2e8f0",overflow:"hidden",flexShrink:0,fontSize:18},iconImg:{width:"100%",height:"100%",objectFit:"contain",padding:5},info:{flex:1,minWidth:0},name:{fontSize:13,fontWeight:600,color:"#333"},url:{fontSize:10,color:"#999",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},toggle:{flexShrink:0},checkbox:{width:16,height:16,cursor:"pointer",accentColor:"#3b82f6"},preview:{display:"flex",alignItems:"flex-end",justifyContent:"center",gap:6,padding:"12px 18px",background:"rgba(30,30,32,0.85)",border:"1px solid rgba(255,255,255,0.18)",borderRadius:18,margin:"16px auto",width:"fit-content"},actions:{display:"flex",gap:10,alignItems:"center",marginTop:16},empty:{textAlign:"center",padding:"30px 16px",color:"#999",fontSize:13},fieldRow:{display:"flex",alignItems:"center",gap:12,marginBottom:10},label:{fontSize:13,fontWeight:500,color:"#555",width:160,flexShrink:0},select:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333"},input:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,width:80,background:"#fff",color:"#333"},checkboxField:{width:16,height:16,accentColor:"#3b82f6"},hint:{fontSize:11,color:"#aaa",marginLeft:8}};function o({icon:e,label:t,color:n,size:o}){const i=o||36,r={...l.iconBox,width:i,height:i,borderRadius:.22*i,...n?{background:n}:{}},c=e||"";return c.startsWith("/")||c.startsWith("http")?a.createElement("div",{style:r},a.createElement("img",{src:c,alt:t,style:{...l.iconImg,padding:.12*i},onError:e=>{e.target.style.display="none",e.target.parentNode.textContent=(t||"??").slice(0,2).toUpperCase()}})):a.createElement("div",{style:r},c||(t||"??").slice(0,2).toUpperCase())}function i({label:e,value:t,options:n,onChange:o,hint:i}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("select",{style:l.select,value:t,onChange:e=>o(e.target.value)},n.map(e=>a.createElement("option",{key:e.value,value:e.value},e.label))),i&&a.createElement("span",{style:l.hint},i))}function r({label:e,value:t,onChange:n,hint:o}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("input",{style:l.input,type:"number",value:t,onChange:e=>n(Number(e.target.value))}),o&&a.createElement("span",{style:l.hint},o))}function c({label:e,checked:t,onChange:n,hint:o}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("input",{type:"checkbox",style:l.checkboxField,checked:t,onChange:e=>n(e.target.checked)}),o&&a.createElement("span",{style:l.hint},o))}function s({configuration:e,save:t}){const n=e||{},[s,d]=(0,a.useState)(()=>n.apps||[]),[u,g]=(0,a.useState)(n.position||"bottom"),[p,m]=(0,a.useState)(n.iframeMode||"keep-alive"),[b,f]=(0,a.useState)(n.iconSize||56),[h,y]=(0,a.useState)(!1!==n.magnification),[v,k]=(0,a.useState)(n.magnificationScale||1.7),[E,S]=(0,a.useState)(n.showNightModeButton||!1),[x,w]=(0,a.useState)(n.showExitButton||!1),[C,D]=(0,a.useState)(n.tourDismissed||!1),[z,R]=(0,a.useState)(""),[I,B]=(0,a.useState)(!1),[T,F]=(0,a.useState)(!1),[A,M]=(0,a.useState)(!1),[N,W]=(0,a.useState)(null),[$,_]=(0,a.useState)(null),O=(0,a.useRef)(s);O.current=s;const U=(0,a.useRef)(null);(0,a.useEffect)(()=>()=>{U.current&&clearTimeout(U.current)},[]);const j=(0,a.useCallback)(e=>({position:u,iframeMode:p,iconSize:b,magnification:h,magnificationScale:v,showNightModeButton:E,showExitButton:x,tourDismissed:C,apps:e}),[u,p,b,h,v,E,x,C]),L=(0,a.useCallback)(async()=>{U.current&&clearTimeout(U.current),M(!0),R("Saving…"),B(!1);try{const e=t(j(s));e&&"function"==typeof e.then&&await e,R("Configuration saved — plugin will restart"),B(!1),U.current=setTimeout(()=>R(""),5e3)}catch(e){R("Save failed: "+(e.message||e)),B(!0)}finally{M(!1)}},[s,j,t]),P=()=>_(null),K=()=>{W(null),_(null)},H=s.filter(e=>!1!==e.enabled);return a.createElement("div",{style:l.root},a.createElement("div",{style:l.sectionTitle},"Dock Settings"),a.createElement(i,{label:"Dock position",value:u,onChange:g,options:[{value:"bottom",label:"Bottom"},{value:"top",label:"Top"},{value:"left",label:"Left"},{value:"right",label:"Right"}]}),a.createElement(i,{label:"iFrame lifecycle",value:p,onChange:m,options:[{value:"keep-alive",label:"Keep-alive (faster switching, more RAM)"},{value:"destroy",label:"Destroy (reload each time, less RAM)"}]}),a.createElement(r,{label:"Icon size",value:b,onChange:f,hint:"px"}),a.createElement(c,{label:"Magnification effect",checked:h,onChange:y}),h&&a.createElement(r,{label:"Magnification scale",value:v,onChange:k,hint:"1.0 - 2.5"}),a.createElement("div",{style:l.sectionTitle},"Utility Buttons"),a.createElement(c,{label:"Night/Day mode toggle",checked:E,onChange:S,hint:"Sun/moon button to toggle environment.mode"}),a.createElement(c,{label:"Exit button",checked:x,onChange:w,hint:"X button to return to Signal K admin UI"}),a.createElement("div",{style:l.sectionTitle},"Webapps"),a.createElement("button",{style:{...l.btn,...l.btnPrimary,...T?{opacity:.6}:{}},onClick:async()=>{F(!0),R(""),B(!1);try{const e=await fetch("/skServer/webapps"),t=(await e.json()).filter(e=>"@signalk/app-dock"!==e.name).map(e=>"@signalk/server-admin-ui"===e.name?{name:e.name,label:"Settings",url:"/admin/",icon:"/@signalk/app-dock/icon-settings.svg",isAdmin:!0}:{name:e.name,label:e.signalk?.displayName||e.name,url:`/${e.name}/`,icon:e.signalk?.appIcon?`/${e.name}/${e.signalk.appIcon}`:null});if(0===t.length)return R("No webapps found. Is the server fully started?"),B(!0),void F(!1);const n=O.current,a=new Set(n.map(e=>e.url));let l=0;const o=[...n];for(const e of t)a.has(e.url)||(o.push({enabled:!0,autostart:!1,url:e.url,label:e.label||"",icon:e.icon||"",color:e.isAdmin?"#78788c":""}),l++);d(o),R(l>0?`Found ${t.length} webapps, added ${l} new.`:`All ${t.length} webapps already in list.`),B(!1)}catch(e){R("Discovery failed: "+e.message),B(!0)}F(!1)},disabled:T},T?"Discovering…":"Discover Installed Webapps"),z&&a.createElement("div",{style:{...l.status,color:I?"#ef4444":"#10b981"}},z),a.createElement("div",{style:l.sectionTitle},"Dock Apps (drag to reorder)"),0===s.length?a.createElement("div",{style:l.empty},"No apps yet. Click Discover above."):a.createElement("div",null,s.map((e,t)=>a.createElement("div",{key:e.url+t,draggable:!0,onDragStart:()=>(e=>W(e))(t),onDragOver:e=>((e,t)=>{e.preventDefault(),_(t)})(e,t),onDragLeave:P,onDrop:e=>((e,t)=>{if(e.preventDefault(),_(null),null!==N&&N!==t){const e=[...s],[n]=e.splice(N,1);e.splice(t,0,n),d(e)}W(null)})(e,t),onDragEnd:K,style:{...l.item,...!1===e.enabled?l.itemDisabled:{},...$===t?l.dragOver:{},...N===t?{opacity:.4}:{}}},a.createElement("span",{style:l.handle},"≡"),a.createElement(o,{icon:e.icon,label:e.label||e.url,color:e.color}),a.createElement("div",{style:l.info},a.createElement("div",{style:l.name},e.label||e.url),a.createElement("div",{style:l.url},e.url)),a.createElement("div",{style:l.toggle,title:"Enabled"},a.createElement("input",{type:"checkbox",checked:!1!==e.enabled,onChange:()=>(e=>d(s.map((t,n)=>n===e?{...t,enabled:!t.enabled}:t)))(t),style:l.checkbox})),a.createElement("div",{style:{...l.toggle,cursor:"pointer",fontSize:16,opacity:e.autostart?1:.25},onClick:()=>(e=>d(s.map((t,n)=>n===e?{...t,autostart:!t.autostart}:{...t,autostart:!1})))(t),title:e.autostart?"Autostart: ON (click to disable)":"Click to set as autostart app"},"▶"),a.createElement("button",{style:{...l.btn,...l.btnDanger},onClick:()=>(e=>d(s.filter((t,n)=>n!==e)))(t)},"Remove")))),(H.length>0||E||x)&&a.createElement(a.Fragment,null,a.createElement("div",{style:l.sectionTitle},"Preview"),a.createElement("div",{style:l.preview},E&&a.createElement(a.Fragment,null,a.createElement("div",{style:{...l.iconBox,width:40,height:40,background:"rgba(255,255,255,0.08)",border:"1px solid rgba(255,255,255,0.12)",fontSize:20,borderRadius:10}},"☀️"),a.createElement("div",{style:{width:1,height:24,background:"rgba(255,255,255,0.15)",alignSelf:"center"}})),H.map((e,t)=>a.createElement(o,{key:t,icon:e.icon,label:e.label||e.url,color:e.color,size:40})),x&&a.createElement(a.Fragment,null,a.createElement("div",{style:{width:1,height:24,background:"rgba(255,255,255,0.15)",alignSelf:"center"}}),a.createElement("div",{style:{...l.iconBox,width:40,height:40,background:"rgba(255,255,255,0.08)",border:"1px solid rgba(255,255,255,0.12)",fontSize:16,borderRadius:10,color:"#fff"}},"✕")))),a.createElement("div",{style:l.actions},a.createElement("button",{style:{...l.btn,...l.btnSave,...A?{opacity:.6}:{}},onClick:L,disabled:A},A?"Saving…":"Save Configuration"),z&&a.createElement("div",{style:{...l.status,color:I?"#ef4444":"#10b981"}},z)))}}}]);
|
|
1
|
+
"use strict";(self.webpackChunk_signalk_app_dock=self.webpackChunk_signalk_app_dock||[]).push([[805],{805(e,t,n){n.r(t),n.d(t,{default:()=>s});var a=n(231);const l={root:{fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',color:"#333",padding:"16px 0"},sectionTitle:{fontSize:13,fontWeight:600,color:"#888",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:10,marginTop:24},btn:{display:"inline-flex",alignItems:"center",gap:8,padding:"8px 16px",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},btnPrimary:{background:"#3b82f6",color:"#fff"},btnSave:{background:"#10b981",color:"#fff"},btnDanger:{background:"#ef4444",color:"#fff",padding:"4px 10px",fontSize:11},status:{marginTop:8,fontSize:12,minHeight:18},item:{display:"flex",alignItems:"center",gap:10,padding:"8px 12px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:8,marginBottom:6,cursor:"grab"},itemDisabled:{opacity:.4},dragOver:{borderColor:"#3b82f6",background:"#eef4ff"},handle:{color:"#bbb",fontSize:16,cursor:"grab",userSelect:"none",flexShrink:0,width:18,textAlign:"center"},iconBox:{width:36,height:36,borderRadius:8,display:"flex",alignItems:"center",justifyContent:"center",background:"#e2e8f0",overflow:"hidden",flexShrink:0,fontSize:18},iconImg:{width:"100%",height:"100%",objectFit:"contain",padding:5},info:{flex:1,minWidth:0},name:{fontSize:13,fontWeight:600,color:"#333"},url:{fontSize:10,color:"#999",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},toggle:{flexShrink:0},checkbox:{width:16,height:16,cursor:"pointer",accentColor:"#3b82f6"},preview:{display:"flex",alignItems:"flex-end",justifyContent:"center",gap:6,padding:"12px 18px",background:"rgba(30,30,32,0.85)",border:"1px solid rgba(255,255,255,0.18)",borderRadius:18,margin:"16px auto",width:"fit-content"},actions:{display:"flex",gap:10,alignItems:"center",marginTop:16},empty:{textAlign:"center",padding:"30px 16px",color:"#999",fontSize:13},fieldRow:{display:"flex",alignItems:"center",gap:12,marginBottom:10},label:{fontSize:13,fontWeight:500,color:"#555",width:160,flexShrink:0},select:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333"},input:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,width:80,background:"#fff",color:"#333"},checkboxField:{width:16,height:16,accentColor:"#3b82f6"},hint:{fontSize:11,color:"#aaa",marginLeft:8}};function o({icon:e,label:t,color:n,size:o}){const i=o||36,r={...l.iconBox,width:i,height:i,borderRadius:.22*i,...n?{background:n}:{}},c=e||"";return c.startsWith("/")||c.startsWith("http")?a.createElement("div",{style:r},a.createElement("img",{src:c,alt:t,style:{...l.iconImg,padding:.12*i},onError:e=>{e.target.style.display="none",e.target.parentNode.textContent=(t||"??").slice(0,2).toUpperCase()}})):a.createElement("div",{style:r},c||(t||"??").slice(0,2).toUpperCase())}function i({label:e,value:t,options:n,onChange:o,hint:i}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("select",{style:l.select,value:t,onChange:e=>o(e.target.value)},n.map(e=>a.createElement("option",{key:e.value,value:e.value},e.label))),i&&a.createElement("span",{style:l.hint},i))}function r({label:e,value:t,onChange:n,hint:o}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("input",{style:l.input,type:"number",value:t,onChange:e=>n(Number(e.target.value))}),o&&a.createElement("span",{style:l.hint},o))}function c({label:e,checked:t,onChange:n,hint:o}){return a.createElement("div",{style:l.fieldRow},a.createElement("span",{style:l.label},e),a.createElement("input",{type:"checkbox",style:l.checkboxField,checked:t,onChange:e=>n(e.target.checked)}),o&&a.createElement("span",{style:l.hint},o))}function s({configuration:e,save:t}){const n=e||{},[s,d]=(0,a.useState)(()=>n.apps||[]),[u,g]=(0,a.useState)(n.position||"bottom"),[p,m]=(0,a.useState)(n.iframeMode||"keep-alive"),[b,f]=(0,a.useState)(n.iconSize||56),[h,y]=(0,a.useState)(!1!==n.magnification),[v,k]=(0,a.useState)(n.magnificationScale||1.7),[E,S]=(0,a.useState)(n.showNightModeButton||!1),[x,w]=(0,a.useState)(n.showFullscreenButton||!1),[C,z]=(0,a.useState)(n.showExitButton||!1),[D,R]=(0,a.useState)(n.tourDismissed||!1),[B,I]=(0,a.useState)(""),[T,F]=(0,a.useState)(!1),[A,M]=(0,a.useState)(!1),[N,W]=(0,a.useState)(!1),[O,$]=(0,a.useState)(null),[_,P]=(0,a.useState)(null),U=(0,a.useRef)(s);U.current=s;const j=(0,a.useRef)(null);(0,a.useEffect)(()=>()=>{j.current&&clearTimeout(j.current)},[]);const L=(0,a.useCallback)(e=>({position:u,iframeMode:p,iconSize:b,magnification:h,magnificationScale:v,showNightModeButton:E,showFullscreenButton:x,showExitButton:C,tourDismissed:D,apps:e}),[u,p,b,h,v,E,x,C,D]),H=(0,a.useCallback)(async()=>{j.current&&clearTimeout(j.current),W(!0),I("Saving…"),F(!1);try{const e=t(L(s));e&&"function"==typeof e.then&&await e,I("Configuration saved — plugin will restart"),F(!1),j.current=setTimeout(()=>I(""),5e3)}catch(e){I("Save failed: "+(e.message||e)),F(!0)}finally{W(!1)}},[s,L,t]),K=()=>P(null),X=()=>{$(null),P(null)},q=s.filter(e=>!1!==e.enabled);return a.createElement("div",{style:l.root},a.createElement("div",{style:l.sectionTitle},"Dock Settings"),a.createElement(i,{label:"Dock position",value:u,onChange:g,options:[{value:"bottom",label:"Bottom"},{value:"top",label:"Top"},{value:"left",label:"Left"},{value:"right",label:"Right"}]}),a.createElement(i,{label:"iFrame lifecycle",value:p,onChange:m,options:[{value:"keep-alive",label:"Keep-alive (faster switching, more RAM)"},{value:"destroy",label:"Destroy (reload each time, less RAM)"}]}),a.createElement(r,{label:"Icon size",value:b,onChange:f,hint:"px"}),a.createElement(c,{label:"Magnification effect",checked:h,onChange:y}),h&&a.createElement(r,{label:"Magnification scale",value:v,onChange:k,hint:"1.0 - 2.5"}),a.createElement("div",{style:l.sectionTitle},"Utility Buttons"),a.createElement(c,{label:"Night/Day mode toggle",checked:E,onChange:S,hint:"Sun/moon button to toggle environment.mode"}),a.createElement(c,{label:"Fullscreen toggle",checked:x,onChange:w,hint:"Toggles browser fullscreen. Hidden on devices without Fullscreen API support (e.g. iPadOS Safari)."}),a.createElement(c,{label:"Exit button",checked:C,onChange:z,hint:"X button to return to Signal K admin UI"}),a.createElement("div",{style:l.sectionTitle},"Webapps"),a.createElement("button",{style:{...l.btn,...l.btnPrimary,...A?{opacity:.6}:{}},onClick:async()=>{M(!0),I(""),F(!1);try{const e=await fetch("/skServer/webapps"),t=(await e.json()).filter(e=>"@signalk/app-dock"!==e.name).map(e=>"@signalk/server-admin-ui"===e.name?{name:e.name,label:"Settings",url:"/admin/",icon:"/@signalk/app-dock/icon-settings.svg",isAdmin:!0}:{name:e.name,label:e.signalk?.displayName||e.name,url:`/${e.name}/`,icon:e.signalk?.appIcon?`/${e.name}/${e.signalk.appIcon}`:null});if(0===t.length)return I("No webapps found. Is the server fully started?"),F(!0),void M(!1);const n=U.current,a=new Set(n.map(e=>e.url));let l=0;const o=[...n];for(const e of t)a.has(e.url)||(o.push({enabled:!0,autostart:!1,url:e.url,label:e.label||"",icon:e.icon||"",color:e.isAdmin?"#78788c":""}),l++);d(o),I(l>0?`Found ${t.length} webapps, added ${l} new.`:`All ${t.length} webapps already in list.`),F(!1)}catch(e){I("Discovery failed: "+e.message),F(!0)}M(!1)},disabled:A},A?"Discovering…":"Discover Installed Webapps"),B&&a.createElement("div",{style:{...l.status,color:T?"#ef4444":"#10b981"}},B),a.createElement("div",{style:l.sectionTitle},"Dock Apps (drag to reorder)"),0===s.length?a.createElement("div",{style:l.empty},"No apps yet. Click Discover above."):a.createElement("div",null,s.map((e,t)=>a.createElement("div",{key:e.url+t,draggable:!0,onDragStart:()=>(e=>$(e))(t),onDragOver:e=>((e,t)=>{e.preventDefault(),P(t)})(e,t),onDragLeave:K,onDrop:e=>((e,t)=>{if(e.preventDefault(),P(null),null!==O&&O!==t){const e=[...s],[n]=e.splice(O,1);e.splice(t,0,n),d(e)}$(null)})(e,t),onDragEnd:X,style:{...l.item,...!1===e.enabled?l.itemDisabled:{},..._===t?l.dragOver:{},...O===t?{opacity:.4}:{}}},a.createElement("span",{style:l.handle},"≡"),a.createElement(o,{icon:e.icon,label:e.label||e.url,color:e.color}),a.createElement("div",{style:l.info},a.createElement("div",{style:l.name},e.label||e.url),a.createElement("div",{style:l.url},e.url)),a.createElement("div",{style:l.toggle,title:"Enabled"},a.createElement("input",{type:"checkbox",checked:!1!==e.enabled,onChange:()=>(e=>d(s.map((t,n)=>n===e?{...t,enabled:!t.enabled}:t)))(t),style:l.checkbox})),a.createElement("div",{style:{...l.toggle,cursor:"pointer",fontSize:16,opacity:e.autostart?1:.25},onClick:()=>(e=>d(s.map((t,n)=>n===e?{...t,autostart:!t.autostart}:{...t,autostart:!1})))(t),title:e.autostart?"Autostart: ON (click to disable)":"Click to set as autostart app"},"▶"),a.createElement("button",{style:{...l.btn,...l.btnDanger},onClick:()=>(e=>d(s.filter((t,n)=>n!==e)))(t)},"Remove")))),(q.length>0||E||x||C)&&a.createElement(a.Fragment,null,a.createElement("div",{style:l.sectionTitle},"Preview"),a.createElement("div",{style:l.preview},E&&a.createElement("div",{style:{...l.iconBox,width:40,height:40,background:"rgba(255,255,255,0.08)",border:"1px solid rgba(255,255,255,0.12)",fontSize:20,borderRadius:10}},"☀️"),x&&a.createElement("div",{style:{...l.iconBox,width:40,height:40,background:"rgba(255,255,255,0.08)",border:"1px solid rgba(255,255,255,0.12)",fontSize:20,borderRadius:10,color:"#fff"}},"⛶"),(E||x)&&a.createElement("div",{style:{width:1,height:24,background:"rgba(255,255,255,0.15)",alignSelf:"center"}}),q.map((e,t)=>a.createElement(o,{key:t,icon:e.icon,label:e.label||e.url,color:e.color,size:40})),C&&a.createElement(a.Fragment,null,a.createElement("div",{style:{width:1,height:24,background:"rgba(255,255,255,0.15)",alignSelf:"center"}}),a.createElement("div",{style:{...l.iconBox,width:40,height:40,background:"rgba(255,255,255,0.08)",border:"1px solid rgba(255,255,255,0.12)",fontSize:16,borderRadius:10,color:"#fff"}},"✕")))),a.createElement("div",{style:l.actions},a.createElement("button",{style:{...l.btn,...l.btnSave,...N?{opacity:.6}:{}},onClick:H,disabled:N},N?"Saving…":"Save Configuration"),B&&a.createElement("div",{style:{...l.status,color:T?"#ef4444":"#10b981"}},B)))}}}]);
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#60a5fa"/>
|
|
5
|
+
<stop offset="100%" stop-color="#2563eb"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="100" height="100" fill="url(#bg)"/>
|
|
9
|
+
<g transform="translate(14 14)">
|
|
10
|
+
<rect x="8" y="48" width="56" height="16" rx="8" fill="#ffffff" fill-opacity="0.25" stroke="#ffffff" stroke-opacity="0.4" stroke-width="0.5"/>
|
|
11
|
+
<rect x="14" y="51" width="10" height="10" rx="2.5" fill="#3b82f6" opacity="0.9"/>
|
|
12
|
+
<rect x="31" y="51" width="10" height="10" rx="2.5" fill="#10b981" opacity="0.9"/>
|
|
13
|
+
<rect x="48" y="51" width="10" height="10" rx="2.5" fill="#f59e0b" opacity="0.9"/>
|
|
14
|
+
<rect x="28" y="34" width="16" height="16" rx="3.5" fill="#10b981" opacity="0.95"/>
|
|
15
|
+
<circle cx="36" cy="66" r="1.5" fill="#ffffff" fill-opacity="0.8"/>
|
|
16
|
+
<circle cx="36" cy="22" r="10" fill="none" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1"/>
|
|
17
|
+
<line x1="36" y1="14" x2="36" y2="18" stroke="#ffffff" stroke-opacity="0.3" stroke-width="1" stroke-linecap="round"/>
|
|
18
|
+
<line x1="36" y1="26" x2="36" y2="30" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
19
|
+
<line x1="28" y1="22" x2="32" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
20
|
+
<line x1="40" y1="22" x2="44" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
|
|
21
|
+
<polygon points="36,15 34,22 36,20 38,22" fill="#ef4444" fill-opacity="0.7"/>
|
|
22
|
+
<polygon points="36,29 34,22 36,24 38,22" fill="#ffffff" fill-opacity="0.25"/>
|
|
23
|
+
</g>
|
|
24
|
+
</svg>
|
package/public/dock.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
magnification: true,
|
|
37
37
|
magnificationScale: 1.7,
|
|
38
38
|
showNightModeButton: true,
|
|
39
|
+
showFullscreenButton: false,
|
|
39
40
|
showExitButton: false,
|
|
40
41
|
apps: []
|
|
41
42
|
}
|
|
@@ -188,6 +189,35 @@
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
// ─── Fullscreen toggle ───────────────────────────────────────────────────────
|
|
193
|
+
// Targets documentElement so the iframe (a descendant) goes fullscreen with
|
|
194
|
+
// the dock. Targeting body or the iframe directly would either fail or make
|
|
195
|
+
// the iframe the fullscreen element, which breaks gesture passthrough.
|
|
196
|
+
function fullscreenSupported() {
|
|
197
|
+
return !!(document.fullscreenEnabled && document.documentElement.requestFullscreen)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isFullscreen() {
|
|
201
|
+
return !!document.fullscreenElement
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function toggleFullscreen() {
|
|
205
|
+
if (!fullscreenSupported()) return
|
|
206
|
+
if (isFullscreen()) {
|
|
207
|
+
document.exitFullscreen().catch((e) => console.warn('[Dock] exitFullscreen', e))
|
|
208
|
+
} else {
|
|
209
|
+
document.documentElement.requestFullscreen().catch((e) => console.warn('[Dock] requestFullscreen', e))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateFullscreenIcon() {
|
|
214
|
+
const icon = document.querySelector('.dock-item-fullscreen .dock-icon')
|
|
215
|
+
if (!icon) return
|
|
216
|
+
icon.textContent = isFullscreen() ? '\u2922' : '\u26F6'
|
|
217
|
+
const label = document.querySelector('.dock-item-fullscreen .dock-label')
|
|
218
|
+
if (label) label.textContent = isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'
|
|
219
|
+
}
|
|
220
|
+
|
|
191
221
|
// ─── Apply dock position class & alignment ───────────────────────────────────
|
|
192
222
|
const pos = cfg.position
|
|
193
223
|
$dock.classList.add(`pos-${pos}`)
|
|
@@ -310,6 +340,13 @@
|
|
|
310
340
|
|
|
311
341
|
if (cfg.showNightModeButton) {
|
|
312
342
|
$dockInner.appendChild(createUtilityItem('dock-item-nightmode', '\u2600\uFE0F', 'Night mode', toggleNightMode))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (cfg.showFullscreenButton && fullscreenSupported()) {
|
|
346
|
+
$dockInner.appendChild(createUtilityItem('dock-item-fullscreen', '\u26F6', 'Fullscreen', toggleFullscreen))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (cfg.showNightModeButton || (cfg.showFullscreenButton && fullscreenSupported())) {
|
|
313
350
|
$dockInner.appendChild(createSeparator())
|
|
314
351
|
}
|
|
315
352
|
|
|
@@ -377,6 +414,7 @@
|
|
|
377
414
|
}
|
|
378
415
|
|
|
379
416
|
if (cfg.showNightModeButton) updateNightModeIcon()
|
|
417
|
+
if (cfg.showFullscreenButton && fullscreenSupported()) updateFullscreenIcon()
|
|
380
418
|
}
|
|
381
419
|
|
|
382
420
|
// ─── Magnification ───────────────────────────────────────────────────────────
|
|
@@ -734,6 +772,10 @@
|
|
|
734
772
|
setInterval(fetchCurrentMode, 5000)
|
|
735
773
|
}
|
|
736
774
|
|
|
775
|
+
if (cfg.showFullscreenButton && fullscreenSupported()) {
|
|
776
|
+
document.addEventListener('fullscreenchange', updateFullscreenIcon)
|
|
777
|
+
}
|
|
778
|
+
|
|
737
779
|
// Changes to any of these require a full rebuild (dock classes, magnification
|
|
738
780
|
// constants, dock items, etc.) — easiest to just reload the page.
|
|
739
781
|
const STRUCTURAL_KEYS = [
|
|
@@ -743,6 +785,7 @@
|
|
|
743
785
|
'magnification',
|
|
744
786
|
'magnificationScale',
|
|
745
787
|
'showNightModeButton',
|
|
788
|
+
'showFullscreenButton',
|
|
746
789
|
'showExitButton'
|
|
747
790
|
]
|
|
748
791
|
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
6
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
7
|
+
<link rel="manifest" href="manifest.webmanifest" />
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="app-icon.svg" />
|
|
9
|
+
<link rel="apple-touch-icon" href="app-icon-180.png" />
|
|
10
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
11
|
+
<meta name="apple-mobile-web-app-title" content="App Dock" />
|
|
12
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
|
+
<meta name="theme-color" content="#000000" />
|
|
14
|
+
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
|
|
15
|
+
<meta http-equiv="Pragma" content="no-cache" />
|
|
7
16
|
<title>SK Dock</title>
|
|
8
17
|
<link rel="stylesheet" href="dock.css" />
|
|
9
18
|
</head>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Signal K App Dock",
|
|
3
|
+
"short_name": "App Dock",
|
|
4
|
+
"description": "macOS-style dock to switch between Signal K webapps",
|
|
5
|
+
"start_url": "/@signalk/app-dock/",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"orientation": "any",
|
|
9
|
+
"background_color": "#000000",
|
|
10
|
+
"theme_color": "#000000",
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "app-icon.svg",
|
|
14
|
+
"sizes": "any",
|
|
15
|
+
"type": "image/svg+xml",
|
|
16
|
+
"purpose": "any"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"src": "app-icon-180.png",
|
|
20
|
+
"sizes": "180x180",
|
|
21
|
+
"type": "image/png",
|
|
22
|
+
"purpose": "any"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"src": "app-icon-maskable.svg",
|
|
26
|
+
"sizes": "any",
|
|
27
|
+
"type": "image/svg+xml",
|
|
28
|
+
"purpose": "maskable"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -180,6 +180,7 @@ export default function PluginConfigurationPanel({ configuration, save }) {
|
|
|
180
180
|
const [magnification, setMagnification] = useState(cfg.magnification !== false)
|
|
181
181
|
const [magnificationScale, setMagnificationScale] = useState(cfg.magnificationScale || 1.7)
|
|
182
182
|
const [showNightModeButton, setShowNightModeButton] = useState(cfg.showNightModeButton || false)
|
|
183
|
+
const [showFullscreenButton, setShowFullscreenButton] = useState(cfg.showFullscreenButton || false)
|
|
183
184
|
const [showExitButton, setShowExitButton] = useState(cfg.showExitButton || false)
|
|
184
185
|
const [tourDismissed, setTourDismissed] = useState(cfg.tourDismissed || false)
|
|
185
186
|
|
|
@@ -208,6 +209,7 @@ export default function PluginConfigurationPanel({ configuration, save }) {
|
|
|
208
209
|
magnification,
|
|
209
210
|
magnificationScale,
|
|
210
211
|
showNightModeButton,
|
|
212
|
+
showFullscreenButton,
|
|
211
213
|
showExitButton,
|
|
212
214
|
tourDismissed,
|
|
213
215
|
apps: appsList
|
|
@@ -219,6 +221,7 @@ export default function PluginConfigurationPanel({ configuration, save }) {
|
|
|
219
221
|
magnification,
|
|
220
222
|
magnificationScale,
|
|
221
223
|
showNightModeButton,
|
|
224
|
+
showFullscreenButton,
|
|
222
225
|
showExitButton,
|
|
223
226
|
tourDismissed
|
|
224
227
|
]
|
|
@@ -388,6 +391,13 @@ export default function PluginConfigurationPanel({ configuration, save }) {
|
|
|
388
391
|
hint="Sun/moon button to toggle environment.mode"
|
|
389
392
|
/>
|
|
390
393
|
|
|
394
|
+
<CheckboxField
|
|
395
|
+
label="Fullscreen toggle"
|
|
396
|
+
checked={showFullscreenButton}
|
|
397
|
+
onChange={setShowFullscreenButton}
|
|
398
|
+
hint="Toggles browser fullscreen. Hidden on devices without Fullscreen API support (e.g. iPadOS Safari)."
|
|
399
|
+
/>
|
|
400
|
+
|
|
391
401
|
<CheckboxField
|
|
392
402
|
label="Exit button"
|
|
393
403
|
checked={showExitButton}
|
|
@@ -460,27 +470,43 @@ export default function PluginConfigurationPanel({ configuration, save }) {
|
|
|
460
470
|
</div>
|
|
461
471
|
)}
|
|
462
472
|
|
|
463
|
-
{(enabledApps.length > 0 || showNightModeButton || showExitButton) && (
|
|
473
|
+
{(enabledApps.length > 0 || showNightModeButton || showFullscreenButton || showExitButton) && (
|
|
464
474
|
<>
|
|
465
475
|
<div style={S.sectionTitle}>Preview</div>
|
|
466
476
|
<div style={S.preview}>
|
|
467
477
|
{showNightModeButton && (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
478
|
+
<div
|
|
479
|
+
style={{
|
|
480
|
+
...S.iconBox,
|
|
481
|
+
width: 40,
|
|
482
|
+
height: 40,
|
|
483
|
+
background: 'rgba(255,255,255,0.08)',
|
|
484
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
485
|
+
fontSize: 20,
|
|
486
|
+
borderRadius: 10
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
{'\u2600\uFE0F'}
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
{showFullscreenButton && (
|
|
493
|
+
<div
|
|
494
|
+
style={{
|
|
495
|
+
...S.iconBox,
|
|
496
|
+
width: 40,
|
|
497
|
+
height: 40,
|
|
498
|
+
background: 'rgba(255,255,255,0.08)',
|
|
499
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
500
|
+
fontSize: 20,
|
|
501
|
+
borderRadius: 10,
|
|
502
|
+
color: '#fff'
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
{'\u26F6'}
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
508
|
+
{(showNightModeButton || showFullscreenButton) && (
|
|
509
|
+
<div style={{ width: 1, height: 24, background: 'rgba(255,255,255,0.15)', alignSelf: 'center' }} />
|
|
484
510
|
)}
|
|
485
511
|
{enabledApps.map((app, i) => (
|
|
486
512
|
<IconPreview key={i} icon={app.icon} label={app.label || app.url} color={app.color} size={40} />
|
package/test/plugin.test.js
CHANGED
|
@@ -521,4 +521,22 @@ describe('public files', () => {
|
|
|
521
521
|
it('icon-settings.svg exists', () => {
|
|
522
522
|
assert.ok(fs.existsSync(path.join(publicDir, 'icon-settings.svg')))
|
|
523
523
|
})
|
|
524
|
+
|
|
525
|
+
it('manifest.webmanifest exists and is valid JSON with required PWA fields', () => {
|
|
526
|
+
const manifestPath = path.join(publicDir, 'manifest.webmanifest')
|
|
527
|
+
assert.ok(fs.existsSync(manifestPath))
|
|
528
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
529
|
+
assert.equal(typeof manifest.name, 'string')
|
|
530
|
+
assert.equal(typeof manifest.start_url, 'string')
|
|
531
|
+
assert.equal(manifest.display, 'standalone')
|
|
532
|
+
assert.ok(Array.isArray(manifest.icons) && manifest.icons.length > 0)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('app-icon-180.png exists (apple-touch-icon)', () => {
|
|
536
|
+
assert.ok(fs.existsSync(path.join(publicDir, 'app-icon-180.png')))
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('app-icon-maskable.svg exists', () => {
|
|
540
|
+
assert.ok(fs.existsSync(path.join(publicDir, 'app-icon-maskable.svg')))
|
|
541
|
+
})
|
|
524
542
|
})
|