@signalk/app-dock 0.2.1 → 0.3.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/README.md CHANGED
@@ -9,10 +9,10 @@ A macOS-style app dock for switching between Signal K webapps on touch screens.
9
9
  - **macOS dock magnification** -- icons scale up with parabolic falloff as your finger moves along the dock
10
10
  - **Frosted glass pill** with spring bounce animation, positioned on any screen edge
11
11
  - **Lazy loading** -- apps load only when first tapped (important for Raspberry Pi and low-power devices)
12
- - **Double-tap corner** to reveal the dock -- works on touch and mouse, no conflict with OS gestures or browser fullscreen
13
- - **Mouse edge hover** to reveal on desktop
12
+ - **Double-tap anywhere** to reveal the dock -- works on touch and mouse. Single taps and clicks pass through to the underlying webapp unchanged, so interactive elements (e.g. KIP's corner widgets) keep working. The pass-through recurses into nested iframes too, so double-tap still opens the dock when one webapp embeds another (e.g. KIP's split view with Freeboard-SK).
14
13
  - **Autostart** -- optionally load a default app immediately on open
15
14
  - **keep-alive or destroy** iframe lifecycle
15
+ - **Night/day mode toggle** -- optional sun/moon button that flips `environment.mode` via a PUT handler
16
16
  - **Active dot indicator**, label tooltip, haptic feedback
17
17
  - **Embedded config panel** in the admin UI with webapp discovery, drag-to-reorder, and live preview
18
18
 
@@ -29,9 +29,11 @@ Open: `http://your-sk-server:3000/@signalk/app-dock/`
29
29
 
30
30
  ## Usage
31
31
 
32
- **Double-tap** the configured screen corner (default: bottom-right) to open the dock. Tap an app icon to switch. The dock auto-dismisses after selection, or tap the backdrop to close it.
32
+ **Double-tap anywhere** on the screen to open the dock. Tap an app icon to switch. The dock auto-dismisses after selection, or tap the backdrop to close it. Double-click works the same way for mouse users.
33
33
 
34
- On desktop, move the mouse to the screen edge corresponding to the dock position.
34
+ Tip: double-tapping a clickable element in the underlying webapp will activate that element once before the dock opens -- aim double-taps at non-interactive regions (maps, gauge backgrounds, empty space).
35
+
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.
35
37
 
36
38
  ## Configuration
37
39
 
@@ -45,38 +47,45 @@ Open **Plugin Config > App Dock** in the admin UI. The embedded configurator pro
45
47
 
46
48
  ### Settings
47
49
 
48
- | Setting | Default | Description |
49
- | -------------------- | -------------- | ------------------------------------------- |
50
- | `position` | `bottom` | Dock edge: `bottom`, `top`, `left`, `right` |
51
- | `triggerCorner` | `bottom-right` | Which corner activates double-tap |
52
- | `iframeMode` | `keep-alive` | `keep-alive` or `destroy` |
53
- | `iconSize` | `56` | Base icon size in px |
54
- | `magnification` | `true` | Enable macOS-style magnification effect |
55
- | `magnificationScale` | `1.7` | Max icon scale (1.0-2.5) |
50
+ | Setting | Default | Description |
51
+ | --------------------- | ------------ | ------------------------------------------------------------------------------------------------ |
52
+ | `position` | `bottom` | Dock edge: `bottom`, `top`, `left`, `right` |
53
+ | `iframeMode` | `keep-alive` | `keep-alive` loads each app once and hides/shows (faster, more RAM); `destroy` reloads each time |
54
+ | `iconSize` | `56` | Base icon size in px |
55
+ | `magnification` | `true` | Enable macOS-style magnification effect |
56
+ | `magnificationScale` | `1.7` | Max icon scale (1.0-2.5) |
57
+ | `showNightModeButton` | `true` | Show sun/moon button in dock; PUT-writes `environment.mode` |
58
+ | `showExitButton` | `false` | Show X button that returns to the Signal K admin UI |
56
59
 
57
60
  ## Development
58
61
 
59
62
  ```bash
60
- cd ~/.signalk
63
+ cd ~/.signalk # or a throwaway SK config dir for dev, e.g. ~/.signalk-app-dock
61
64
  npm link /path/to/app-dock
62
65
  ```
63
66
 
64
- Edit files in `public/`, reload the browser. No build step needed for the dock itself.
65
-
66
- To rebuild the config panel (after changing `src/configpanel/`):
67
+ For side-by-side development without touching your real SK setup, point the server at a dedicated config dir:
67
68
 
68
69
  ```bash
69
- npm run build:config
70
+ PORT=4000 npm start -- -c ~/.signalk-app-dock
70
71
  ```
71
72
 
73
+ ### Architecture
74
+
75
+ The repo ships three cooperating surfaces:
76
+
77
+ - **`plugin/index.js`** -- CommonJS SK-server plugin. Registers the schema, seeds default apps on first run, exposes HTTP endpoints for the dock and config panel, and owns the `environment.mode` PUT handler.
78
+ - **`public/dock.js` + `dock.css` + `index.html`** -- the dock UI itself. Plain JS, served as-is. **No build step**: edit and reload the browser.
79
+ - **`src/configpanel/`** -- React 19 component exposed to the admin UI via Webpack Module Federation. Rebuild with `npm run build:config` after changes here; output lands in `public/remoteEntry.js` and companion chunks.
80
+
72
81
  ### Scripts
73
82
 
74
- | Command | Description |
75
- | ---------------------- | --------------------------------- |
76
- | `npm test` | Run tests |
77
- | `npm run format` | Prettier + ESLint fix |
78
- | `npm run lint` | ESLint check |
79
- | `npm run build:config` | Rebuild the admin UI config panel |
83
+ | Command | Description |
84
+ | ---------------------- | ----------------------------------------------------------------- |
85
+ | `npm test` | Run plugin tests (`node --test`) |
86
+ | `npm run format` | Prettier + ESLint fix |
87
+ | `npm run lint` | ESLint check |
88
+ | `npm run build:config` | Rebuild the admin UI config panel (required after `src/` changes) |
80
89
 
81
90
  ## License
82
91
 
package/eslint.config.js CHANGED
@@ -44,7 +44,10 @@ module.exports = [
44
44
  requestAnimationFrame: 'readonly',
45
45
  Promise: 'readonly',
46
46
  Set: 'readonly',
47
- Infinity: 'readonly'
47
+ Infinity: 'readonly',
48
+ MutationObserver: 'readonly',
49
+ URLSearchParams: 'readonly',
50
+ location: 'readonly'
48
51
  }
49
52
  },
50
53
  rules: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/app-dock",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "macOS-style configurable app dock for switching between SignalK webapps on touch screens",
5
5
  "repository": {
6
6
  "url": "https://github.com/SignalK/app-dock"
package/plugin/index.js CHANGED
@@ -141,14 +141,6 @@ module.exports = (app) => {
141
141
  default: 'bottom'
142
142
  },
143
143
 
144
- triggerCorner: {
145
- type: 'string',
146
- title: 'Double-tap corner',
147
- description: 'Screen corner that activates the dock on double-tap',
148
- enum: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
149
- default: 'bottom-right'
150
- },
151
-
152
144
  iframeMode: {
153
145
  type: 'string',
154
146
  title: 'iFrame lifecycle',
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.triggerCorner||"bottom-right"),[b,f]=(0,a.useState)(n.iframeMode||"keep-alive"),[h,v]=(0,a.useState)(n.iconSize||56),[y,k]=(0,a.useState)(!1!==n.magnification),[E,S]=(0,a.useState)(n.magnificationScale||1.7),[x,w]=(0,a.useState)(n.showNightModeButton||!1),[C,z]=(0,a.useState)(n.showExitButton||!1),[D,R]=(0,a.useState)(""),[B,T]=(0,a.useState)(!1),[I,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,triggerCorner:p,iframeMode:b,iconSize:h,magnification:y,magnificationScale:E,showNightModeButton:x,showExitButton:C,apps:e}),[u,p,b,h,y,E,x,C]),L=(0,a.useCallback)(async()=>{U.current&&clearTimeout(U.current),M(!0),R("Saving…"),T(!1);try{const e=t(j(s));e&&"function"==typeof e.then&&await e,R("Configuration saved — plugin will restart"),T(!1),U.current=setTimeout(()=>R(""),5e3)}catch(e){R("Save failed: "+(e.message||e)),T(!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:"Double-tap corner",value:p,onChange:m,options:[{value:"bottom-right",label:"Bottom-right"},{value:"bottom-left",label:"Bottom-left"},{value:"top-right",label:"Top-right"},{value:"top-left",label:"Top-left"}]}),a.createElement(i,{label:"iFrame lifecycle",value:b,onChange:f,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:h,onChange:v,hint:"px"}),a.createElement(c,{label:"Magnification effect",checked:y,onChange:k}),y&&a.createElement(r,{label:"Magnification scale",value:E,onChange:S,hint:"1.0 - 2.5"}),a.createElement("div",{style:l.sectionTitle},"Utility Buttons"),a.createElement(c,{label:"Night/Day mode toggle",checked:x,onChange:w,hint:"Sun/moon button to toggle environment.mode"}),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,...I?{opacity:.6}:{}},onClick:async()=>{F(!0),R(""),T(!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?"),T(!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.`),T(!1)}catch(e){R("Discovery failed: "+e.message),T(!0)}F(!1)},disabled:I},I?"Discovering…":"Discover Installed Webapps"),D&&a.createElement("div",{style:{...l.status,color:B?"#ef4444":"#10b981"}},D),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||x||C)&&a.createElement(a.Fragment,null,a.createElement("div",{style:l.sectionTitle},"Preview"),a.createElement("div",{style:l.preview},x&&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})),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,...A?{opacity:.6}:{}},onClick:L,disabled:A},A?"Saving…":"Save Configuration"),D&&a.createElement("div",{style:{...l.status,color:B?"#ef4444":"#10b981"}},D)))}}}]);
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)))}}}]);
package/public/dock.css CHANGED
@@ -396,19 +396,6 @@ body {
396
396
  background: rgba(239, 68, 68, 0.25);
397
397
  }
398
398
 
399
- /* ─── Invisible corner trigger zone ────────────────────────────────────────── */
400
- #trigger-corner {
401
- position: fixed;
402
- z-index: 9;
403
- width: 120px;
404
- height: 120px;
405
- pointer-events: auto;
406
- touch-action: none;
407
- -webkit-touch-callout: none;
408
- -webkit-user-select: none;
409
- user-select: none;
410
- }
411
-
412
399
  /* ─── Loading overlay ───────────────────────────────────────────────────────── */
413
400
  #loading-overlay {
414
401
  position: fixed;
package/public/dock.js CHANGED
@@ -3,12 +3,11 @@
3
3
 
4
4
  const DEFAULTS = {
5
5
  position: 'bottom',
6
- triggerCorner: 'bottom-right',
7
6
  iframeMode: 'keep-alive',
8
7
  iconSize: 56,
9
8
  magnification: true,
10
9
  magnificationScale: 1.7,
11
- showNightModeButton: false,
10
+ showNightModeButton: true,
12
11
  showExitButton: false,
13
12
  apps: []
14
13
  }
@@ -44,7 +43,6 @@
44
43
  const $backdrop = document.getElementById('dock-backdrop')
45
44
  const $dock = document.getElementById('dock')
46
45
  const $dockInner = document.getElementById('dock-inner')
47
- const $triggerCorner = document.getElementById('trigger-corner')
48
46
  const $loadingOverlay = document.getElementById('loading-overlay')
49
47
  const $loadingLabel = document.getElementById('loading-label')
50
48
  const $idleHint = document.getElementById('idle-hint')
@@ -130,18 +128,8 @@
130
128
  return
131
129
  }
132
130
 
133
- const cornerNames = {
134
- 'bottom-right': 'bottom-right',
135
- 'bottom-left': 'bottom-left',
136
- 'top-right': 'top-right',
137
- 'top-left': 'top-left'
138
- }
139
- const corner = cornerNames[cfg.triggerCorner] || 'bottom-right'
140
-
141
131
  $idleHintText.innerHTML =
142
- 'Double-tap the <strong>' +
143
- corner +
144
- ' corner</strong> to open the dock' +
132
+ 'Double-tap <strong>anywhere</strong> to open the dock' +
145
133
  '<br><br><span style="font-size:12px;opacity:0.6">Configure in Admin UI \u2192 Plugin Config \u2192 App Dock</span>'
146
134
  }
147
135
 
@@ -411,9 +399,13 @@
411
399
  }
412
400
 
413
401
  // ─── Show / hide dock ────────────────────────────────────────────────────────
402
+ let dockShownAt = 0
403
+ const DOCK_CLICK_IGNORE_MS = 500
404
+
414
405
  function showDock() {
415
406
  if (dockVisible) return
416
407
  dockVisible = true
408
+ dockShownAt = Date.now()
417
409
  $dock.classList.add('visible')
418
410
  $backdrop.classList.add('visible')
419
411
  }
@@ -427,7 +419,17 @@
427
419
  resetMagnification()
428
420
  }
429
421
 
430
- $backdrop.addEventListener('click', hideDock)
422
+ $backdrop.addEventListener('click', (e) => {
423
+ // Windows touch synthesizes a click on whatever's under the finger after
424
+ // pointerdown. If that click lands on the just-shown backdrop, it would
425
+ // immediately close the dock. Ignore clicks within a short window after
426
+ // the dock opens.
427
+ if (Date.now() - dockShownAt < DOCK_CLICK_IGNORE_MS) {
428
+ e.stopPropagation()
429
+ return
430
+ }
431
+ hideDock()
432
+ })
431
433
 
432
434
  // ─── Loading overlay helpers ──────────────────────────────────────────────────
433
435
  function showLoading(label) {
@@ -479,82 +481,150 @@
479
481
  hideDock()
480
482
  }
481
483
 
482
- // ─── Corner trigger zone: position it ────────────────────────────────────────
483
- function positionCornerZone() {
484
- const [vert, horiz] = cfg.triggerCorner.split('-')
485
- const sz = 120
486
-
487
- $triggerCorner.style.width = sz + 'px'
488
- $triggerCorner.style.height = sz + 'px'
484
+ // ─── Gesture: full-screen double-tap ────────────────────────────────────────
485
+ // No overlay element. We attach passive, non-preventDefault listeners to the
486
+ // parent document AND each same-origin iframe's contentDocument (recursively,
487
+ // to handle webapps like KIP that embed other apps), so webapps receive every
488
+ // tap/click normally. A double-tap anywhere opens the dock. Side effect:
489
+ // double-tapping a button fires that button once before the dock opens —
490
+ // users are expected to aim double-taps at non-interactive regions.
491
+ const DOUBLE_TAP_MS = 450
492
+ const DOUBLE_TAP_SLOP = 40
493
+
494
+ let lastTapTime = 0
495
+ let lastTapX = 0
496
+ let lastTapY = 0
497
+
498
+ function triggerDock() {
499
+ if (DEBUG) console.log('[Dock] triggerDock called, dockVisible=', dockVisible)
500
+ if (dockVisible) return
501
+ showDock()
502
+ if (DEBUG) {
503
+ const r = $dock.getBoundingClientRect()
504
+ console.log('[Dock] after showDock: dock rect', r, 'classes', $dock.className)
505
+ }
506
+ if (navigator.vibrate) navigator.vibrate(8)
507
+ }
489
508
 
490
- $triggerCorner.style.top = ''
491
- $triggerCorner.style.bottom = ''
492
- $triggerCorner.style.left = ''
493
- $triggerCorner.style.right = ''
509
+ const DEBUG = new URLSearchParams(location.search).has('debugDock')
494
510
 
495
- $triggerCorner.style[vert] = '0'
496
- $triggerCorner.style[horiz] = '0'
511
+ function handleTap(clientX, clientY) {
512
+ if (dockVisible) return
513
+ const now = Date.now()
514
+ const dx = clientX - lastTapX
515
+ const dy = clientY - lastTapY
516
+ const dist = Math.round(Math.sqrt(dx * dx + dy * dy))
517
+ const close = dist <= DOUBLE_TAP_SLOP
518
+ const gap = now - lastTapTime
519
+ const match = gap < DOUBLE_TAP_MS && close
520
+ if (DEBUG) {
521
+ console.log('[Dock] tap', { x: Math.round(clientX), y: Math.round(clientY), gap, dist, close, match })
522
+ }
523
+ if (match) {
524
+ lastTapTime = 0
525
+ triggerDock()
526
+ } else {
527
+ lastTapTime = now
528
+ lastTapX = clientX
529
+ lastTapY = clientY
530
+ }
497
531
  }
498
532
 
499
- // ─── Gesture: double-tap on corner (touch) + double-click (mouse) ─────────────
500
- {
501
- let lastTapTime = 0
502
- const DOUBLE_TAP_MS = 400
533
+ function attachTapListener(doc, frameEl) {
534
+ if (!doc || doc.__dockListenersAttached) return
535
+ doc.__dockListenersAttached = true
503
536
 
504
- $triggerCorner.addEventListener(
505
- 'touchend',
537
+ const offset = () => {
538
+ if (!frameEl) return { x: 0, y: 0 }
539
+ const r = frameEl.getBoundingClientRect()
540
+ return { x: r.left, y: r.top }
541
+ }
542
+
543
+ // Dedupe: touchstart and pointerdown often both fire for the same gesture.
544
+ // We accept whichever arrives first and ignore the other if it's within
545
+ // 50 ms (the browser's synthesized-event coalescence window).
546
+ let lastEventAt = 0
547
+ const COALESCE_MS = 50
548
+
549
+ const onDown = (x, y) => {
550
+ const now = Date.now()
551
+ if (now - lastEventAt < COALESCE_MS) return
552
+ lastEventAt = now
553
+ handleTap(x, y)
554
+ }
555
+
556
+ doc.addEventListener(
557
+ 'pointerdown',
506
558
  (e) => {
507
- const now = Date.now()
508
- if (now - lastTapTime < DOUBLE_TAP_MS) {
509
- e.preventDefault()
510
- lastTapTime = 0
511
- showDock()
512
- if (navigator.vibrate) navigator.vibrate(8)
513
- } else {
514
- lastTapTime = now
515
- }
559
+ const o = offset()
560
+ onDown(e.clientX + o.x, e.clientY + o.y)
516
561
  },
517
- { passive: false }
562
+ { passive: true, capture: true }
518
563
  )
519
564
 
520
- $triggerCorner.addEventListener('dblclick', (e) => {
521
- e.preventDefault()
522
- showDock()
523
- })
565
+ // Fallback for Windows touchscreens + some browsers where the root
566
+ // document does not receive pointerdown for finger input (observed on
567
+ // W11 with touch-action: none). touchstart is the universally-supported
568
+ // event for direct touch.
569
+ doc.addEventListener(
570
+ 'touchstart',
571
+ (e) => {
572
+ if (!e.touches || e.touches.length === 0) return
573
+ const t = e.touches[0]
574
+ const o = offset()
575
+ onDown(t.clientX + o.x, t.clientY + o.y)
576
+ },
577
+ { passive: true, capture: true }
578
+ )
579
+ }
580
+
581
+ // Recursively attach to iframes at any nesting depth. Webapps like KIP
582
+ // embed other webapps (e.g. FSK) in their own iframes, so we walk the tree
583
+ // and observe each same-origin contentDocument for future iframe additions.
584
+ const attachedDocs = new WeakSet()
524
585
 
525
- $triggerCorner.addEventListener('contextmenu', (e) => e.preventDefault())
526
- }
527
-
528
- // ─── Gesture: mouse hover at screen edge ─────────────────────────────────────
529
- {
530
- const EDGE_ZONE = 4
531
- let edgeTimer = null
532
-
533
- document.addEventListener('mousemove', (e) => {
534
- if (dockVisible) return
535
- const W = window.innerWidth
536
- const H = window.innerHeight
537
- let atEdge = false
538
-
539
- if (pos === 'bottom' && e.clientY >= H - EDGE_ZONE) atEdge = true
540
- if (pos === 'top' && e.clientY <= EDGE_ZONE) atEdge = true
541
- if (pos === 'left' && e.clientX <= EDGE_ZONE) atEdge = true
542
- if (pos === 'right' && e.clientX >= W - EDGE_ZONE) atEdge = true
543
-
544
- if (atEdge && !edgeTimer) {
545
- edgeTimer = setTimeout(() => {
546
- showDock()
547
- edgeTimer = null
548
- }, 300)
549
- } else if (!atEdge && edgeTimer) {
550
- clearTimeout(edgeTimer)
551
- edgeTimer = null
586
+ function attachToIframe(frame) {
587
+ const wire = () => {
588
+ let doc
589
+ try {
590
+ doc = frame.contentDocument
591
+ } catch {
592
+ return // cross-origin; can't reach
593
+ }
594
+ if (!doc || attachedDocs.has(doc)) return
595
+ attachedDocs.add(doc)
596
+ attachTapListener(doc, frame)
597
+ scanAndObserveDoc(doc)
598
+ }
599
+ frame.addEventListener('load', wire)
600
+ wire()
601
+ }
602
+
603
+ function scanAndObserveDoc(doc) {
604
+ // Attach to iframes already in this document.
605
+ doc.querySelectorAll('iframe').forEach((f) => attachToIframe(f))
606
+
607
+ // Observe future iframe additions anywhere in the doc tree.
608
+ const root = doc.documentElement || doc.body || doc
609
+ const obs = new MutationObserver((records) => {
610
+ for (const r of records) {
611
+ r.addedNodes.forEach((n) => {
612
+ if (!n || n.nodeType !== 1) return
613
+ if (n.tagName === 'IFRAME') attachToIframe(n)
614
+ // Descendants: catch iframes added as part of a subtree.
615
+ const nested = n.querySelectorAll && n.querySelectorAll('iframe')
616
+ if (nested) nested.forEach((f) => attachToIframe(f))
617
+ })
552
618
  }
553
619
  })
620
+ obs.observe(root, { childList: true, subtree: true })
554
621
  }
555
622
 
623
+ attachTapListener(document, null)
624
+ attachedDocs.add(document)
625
+ scanAndObserveDoc(document)
626
+
556
627
  // ─── Init ────────────────────────────────────────────────────────────────────
557
- positionCornerZone()
558
628
  buildDock()
559
629
 
560
630
  if (cfg.showNightModeButton) {
@@ -562,26 +632,47 @@
562
632
  setInterval(fetchCurrentMode, 5000)
563
633
  }
564
634
 
635
+ // Changes to any of these require a full rebuild (dock classes, magnification
636
+ // constants, dock items, etc.) — easiest to just reload the page.
637
+ const STRUCTURAL_KEYS = [
638
+ 'position',
639
+ 'iframeMode',
640
+ 'iconSize',
641
+ 'magnification',
642
+ 'magnificationScale',
643
+ 'showNightModeButton',
644
+ 'showExitButton'
645
+ ]
646
+
565
647
  async function refreshConfig() {
566
648
  try {
567
649
  const [configRes, settingsRes] = await Promise.all([
568
650
  fetch('/plugins/signalk-app-dock/config'),
569
651
  fetch('/plugins/signalk-app-dock/settings')
570
652
  ])
571
- const prev = JSON.stringify(cfg)
653
+ const prev = cfg
654
+ let next = cfg
572
655
  if (configRes.ok) {
573
656
  const data = await configRes.json()
574
657
  const pluginCfg = data.configuration || data
575
- cfg = { ...DEFAULTS, ...pluginCfg }
658
+ next = { ...DEFAULTS, ...pluginCfg, apps: prev.apps }
576
659
  }
577
660
  if (settingsRes.ok) {
578
661
  const data = await settingsRes.json()
579
662
  if (Array.isArray(data.apps) && data.apps.length > 0) {
580
- cfg.apps = data.apps
663
+ next = { ...next, apps: data.apps }
581
664
  }
582
665
  }
583
- if (JSON.stringify(cfg) !== prev) {
666
+ const structuralChanged = STRUCTURAL_KEYS.some((k) => prev[k] !== next[k])
667
+ if (structuralChanged) {
668
+ window.location.reload()
669
+ return
670
+ }
671
+ if (JSON.stringify(next.apps) !== JSON.stringify(prev.apps)) {
672
+ cfg = next
584
673
  buildDock()
674
+ } else {
675
+ cfg = next
585
676
  }
586
677
  } catch {
587
678
  // ignore transient errors (e.g. during plugin restart)
package/public/index.html CHANGED
@@ -27,9 +27,6 @@
27
27
  <div id="dock-inner"></div>
28
28
  </nav>
29
29
 
30
- <!-- Invisible corner hot-zone for long-press trigger -->
31
- <div id="trigger-corner" aria-hidden="true"></div>
32
-
33
30
  <!-- Loading indicator shown while an iframe is loading for the first time -->
34
31
  <div id="loading-overlay" aria-hidden="true">
35
32
  <div class="spinner"></div>
@@ -43,8 +40,7 @@
43
40
  <div id="tour-body">
44
41
  <p>App Dock gives you a macOS-style dock to quickly switch between Signal K webapps on touch screens.</p>
45
42
  <p>
46
- <strong>Double-tap the bottom-right corner</strong> of the screen to open the dock, then tap an icon to
47
- launch that app.
43
+ <strong>Double-tap anywhere</strong> on the screen to open the dock, then tap an icon to launch that app.
48
44
  </p>
49
45
  <p>
50
46
  Freeboard-SK is loading in the background and KIP is one tap away. Customize the apps, dock position, and
@@ -175,13 +175,13 @@ export default function PluginConfigurationPanel({ configuration, save }) {
175
175
  const cfg = configuration || {}
176
176
  const [apps, setApps] = useState(() => cfg.apps || [])
177
177
  const [position, setPosition] = useState(cfg.position || 'bottom')
178
- const [triggerCorner, setTriggerCorner] = useState(cfg.triggerCorner || 'bottom-right')
179
178
  const [iframeMode, setIframeMode] = useState(cfg.iframeMode || 'keep-alive')
180
179
  const [iconSize, setIconSize] = useState(cfg.iconSize || 56)
181
180
  const [magnification, setMagnification] = useState(cfg.magnification !== false)
182
181
  const [magnificationScale, setMagnificationScale] = useState(cfg.magnificationScale || 1.7)
183
182
  const [showNightModeButton, setShowNightModeButton] = useState(cfg.showNightModeButton || false)
184
183
  const [showExitButton, setShowExitButton] = useState(cfg.showExitButton || false)
184
+ const [tourDismissed, setTourDismissed] = useState(cfg.tourDismissed || false)
185
185
 
186
186
  const [status, setStatus] = useState('')
187
187
  const [statusError, setStatusError] = useState(false)
@@ -203,24 +203,24 @@ export default function PluginConfigurationPanel({ configuration, save }) {
203
203
  const buildConfig = useCallback(
204
204
  (appsList) => ({
205
205
  position,
206
- triggerCorner,
207
206
  iframeMode,
208
207
  iconSize,
209
208
  magnification,
210
209
  magnificationScale,
211
210
  showNightModeButton,
212
211
  showExitButton,
212
+ tourDismissed,
213
213
  apps: appsList
214
214
  }),
215
215
  [
216
216
  position,
217
- triggerCorner,
218
217
  iframeMode,
219
218
  iconSize,
220
219
  magnification,
221
220
  magnificationScale,
222
221
  showNightModeButton,
223
- showExitButton
222
+ showExitButton,
223
+ tourDismissed
224
224
  ]
225
225
  )
226
226
 
@@ -356,18 +356,6 @@ export default function PluginConfigurationPanel({ configuration, save }) {
356
356
  ]}
357
357
  />
358
358
 
359
- <SelectField
360
- label="Double-tap corner"
361
- value={triggerCorner}
362
- onChange={setTriggerCorner}
363
- options={[
364
- { value: 'bottom-right', label: 'Bottom-right' },
365
- { value: 'bottom-left', label: 'Bottom-left' },
366
- { value: 'top-right', label: 'Top-right' },
367
- { value: 'top-left', label: 'Top-left' }
368
- ]}
369
- />
370
-
371
359
  <SelectField
372
360
  label="iFrame lifecycle"
373
361
  value={iframeMode}
@@ -58,12 +58,13 @@ describe('schema', () => {
58
58
  const plugin = pluginFactory(createMockApp())
59
59
  const props = Object.keys(plugin.schema.properties)
60
60
  assert.ok(props.includes('position'))
61
- assert.ok(props.includes('triggerCorner'))
62
61
  assert.ok(props.includes('iframeMode'))
63
62
  assert.ok(props.includes('iconSize'))
64
63
  assert.ok(props.includes('magnification'))
65
64
  assert.ok(props.includes('magnificationScale'))
66
65
  assert.ok(props.includes('apps'))
66
+ assert.ok(!props.includes('triggerCorner'), 'triggerCorner should be removed')
67
+ assert.ok(!props.includes('longPressActivation'), 'longPressActivation should be removed')
67
68
  })
68
69
 
69
70
  it('position enum contains all four edges', () => {
@@ -92,11 +93,6 @@ describe('schema', () => {
92
93
  assert.equal(prop.default, false)
93
94
  })
94
95
 
95
- it('triggerCorner defaults to bottom-right', () => {
96
- const plugin = pluginFactory(createMockApp())
97
- assert.equal(plugin.schema.properties.triggerCorner.default, 'bottom-right')
98
- })
99
-
100
96
  it('defines tourDismissed with default false', () => {
101
97
  const plugin = pluginFactory(createMockApp())
102
98
  const prop = plugin.schema.properties.tourDismissed