@signalk/app-dock 0.2.1 → 0.3.0-beta.1
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 +32 -23
- package/eslint.config.js +2 -1
- package/package.json +1 -1
- package/plugin/index.js +0 -8
- package/public/805.js +1 -1
- package/public/dock.css +0 -13
- package/public/dock.js +118 -80
- package/public/index.html +1 -5
- package/src/configpanel/PluginConfigurationPanel.js +4 -16
- package/test/plugin.test.js +2 -6
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
|
|
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
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
|
50
|
-
| `position`
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
package/package.json
CHANGED
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:
|
|
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
|
|
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
|
|
|
@@ -479,82 +467,111 @@
|
|
|
479
467
|
hideDock()
|
|
480
468
|
}
|
|
481
469
|
|
|
482
|
-
// ───
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
470
|
+
// ─── Gesture: full-screen double-tap ────────────────────────────────────────
|
|
471
|
+
// No overlay element. We attach passive, non-preventDefault listeners to the
|
|
472
|
+
// parent document AND each same-origin iframe's contentDocument (recursively,
|
|
473
|
+
// to handle webapps like KIP that embed other apps), so webapps receive every
|
|
474
|
+
// tap/click normally. A double-tap anywhere opens the dock. Side effect:
|
|
475
|
+
// double-tapping a button fires that button once before the dock opens —
|
|
476
|
+
// users are expected to aim double-taps at non-interactive regions.
|
|
477
|
+
const DOUBLE_TAP_MS = 280
|
|
478
|
+
const DOUBLE_TAP_MIN_GAP_MS = 60
|
|
479
|
+
const DOUBLE_TAP_SLOP = 24
|
|
480
|
+
|
|
481
|
+
let lastTapTime = 0
|
|
482
|
+
let lastTapX = 0
|
|
483
|
+
let lastTapY = 0
|
|
484
|
+
|
|
485
|
+
function triggerDock() {
|
|
486
|
+
if (dockVisible) return
|
|
487
|
+
showDock()
|
|
488
|
+
if (navigator.vibrate) navigator.vibrate(8)
|
|
489
|
+
}
|
|
494
490
|
|
|
495
|
-
|
|
496
|
-
|
|
491
|
+
function handleTap(clientX, clientY) {
|
|
492
|
+
if (dockVisible) return
|
|
493
|
+
const now = Date.now()
|
|
494
|
+
const dx = clientX - lastTapX
|
|
495
|
+
const dy = clientY - lastTapY
|
|
496
|
+
const close = dx * dx + dy * dy <= DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP
|
|
497
|
+
const gap = now - lastTapTime
|
|
498
|
+
if (gap >= DOUBLE_TAP_MIN_GAP_MS && gap < DOUBLE_TAP_MS && close) {
|
|
499
|
+
lastTapTime = 0
|
|
500
|
+
triggerDock()
|
|
501
|
+
} else {
|
|
502
|
+
lastTapTime = now
|
|
503
|
+
lastTapX = clientX
|
|
504
|
+
lastTapY = clientY
|
|
505
|
+
}
|
|
497
506
|
}
|
|
498
507
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const DOUBLE_TAP_MS = 400
|
|
508
|
+
function attachTapListener(doc, frameEl) {
|
|
509
|
+
if (!doc || doc.__dockListenersAttached) return
|
|
510
|
+
doc.__dockListenersAttached = true
|
|
503
511
|
|
|
504
|
-
|
|
505
|
-
|
|
512
|
+
const offset = () => {
|
|
513
|
+
if (!frameEl) return { x: 0, y: 0 }
|
|
514
|
+
const r = frameEl.getBoundingClientRect()
|
|
515
|
+
return { x: r.left, y: r.top }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
doc.addEventListener(
|
|
519
|
+
'pointerdown',
|
|
506
520
|
(e) => {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
e.preventDefault()
|
|
510
|
-
lastTapTime = 0
|
|
511
|
-
showDock()
|
|
512
|
-
if (navigator.vibrate) navigator.vibrate(8)
|
|
513
|
-
} else {
|
|
514
|
-
lastTapTime = now
|
|
515
|
-
}
|
|
521
|
+
const o = offset()
|
|
522
|
+
handleTap(e.clientX + o.x, e.clientY + o.y)
|
|
516
523
|
},
|
|
517
|
-
{ passive:
|
|
524
|
+
{ passive: true, capture: true }
|
|
518
525
|
)
|
|
526
|
+
}
|
|
519
527
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
528
|
+
// Recursively attach to iframes at any nesting depth. Webapps like KIP
|
|
529
|
+
// embed other webapps (e.g. FSK) in their own iframes, so we walk the tree
|
|
530
|
+
// and observe each same-origin contentDocument for future iframe additions.
|
|
531
|
+
const attachedDocs = new WeakSet()
|
|
524
532
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
533
|
+
function attachToIframe(frame) {
|
|
534
|
+
const wire = () => {
|
|
535
|
+
let doc
|
|
536
|
+
try {
|
|
537
|
+
doc = frame.contentDocument
|
|
538
|
+
} catch {
|
|
539
|
+
return // cross-origin; can't reach
|
|
540
|
+
}
|
|
541
|
+
if (!doc || attachedDocs.has(doc)) return
|
|
542
|
+
attachedDocs.add(doc)
|
|
543
|
+
attachTapListener(doc, frame)
|
|
544
|
+
scanAndObserveDoc(doc)
|
|
545
|
+
}
|
|
546
|
+
frame.addEventListener('load', wire)
|
|
547
|
+
wire()
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function scanAndObserveDoc(doc) {
|
|
551
|
+
// Attach to iframes already in this document.
|
|
552
|
+
doc.querySelectorAll('iframe').forEach((f) => attachToIframe(f))
|
|
553
|
+
|
|
554
|
+
// Observe future iframe additions anywhere in the doc tree.
|
|
555
|
+
const root = doc.documentElement || doc.body || doc
|
|
556
|
+
const obs = new MutationObserver((records) => {
|
|
557
|
+
for (const r of records) {
|
|
558
|
+
r.addedNodes.forEach((n) => {
|
|
559
|
+
if (!n || n.nodeType !== 1) return
|
|
560
|
+
if (n.tagName === 'IFRAME') attachToIframe(n)
|
|
561
|
+
// Descendants: catch iframes added as part of a subtree.
|
|
562
|
+
const nested = n.querySelectorAll && n.querySelectorAll('iframe')
|
|
563
|
+
if (nested) nested.forEach((f) => attachToIframe(f))
|
|
564
|
+
})
|
|
552
565
|
}
|
|
553
566
|
})
|
|
567
|
+
obs.observe(root, { childList: true, subtree: true })
|
|
554
568
|
}
|
|
555
569
|
|
|
570
|
+
attachTapListener(document, null)
|
|
571
|
+
attachedDocs.add(document)
|
|
572
|
+
scanAndObserveDoc(document)
|
|
573
|
+
|
|
556
574
|
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
557
|
-
positionCornerZone()
|
|
558
575
|
buildDock()
|
|
559
576
|
|
|
560
577
|
if (cfg.showNightModeButton) {
|
|
@@ -562,26 +579,47 @@
|
|
|
562
579
|
setInterval(fetchCurrentMode, 5000)
|
|
563
580
|
}
|
|
564
581
|
|
|
582
|
+
// Changes to any of these require a full rebuild (dock classes, magnification
|
|
583
|
+
// constants, dock items, etc.) — easiest to just reload the page.
|
|
584
|
+
const STRUCTURAL_KEYS = [
|
|
585
|
+
'position',
|
|
586
|
+
'iframeMode',
|
|
587
|
+
'iconSize',
|
|
588
|
+
'magnification',
|
|
589
|
+
'magnificationScale',
|
|
590
|
+
'showNightModeButton',
|
|
591
|
+
'showExitButton'
|
|
592
|
+
]
|
|
593
|
+
|
|
565
594
|
async function refreshConfig() {
|
|
566
595
|
try {
|
|
567
596
|
const [configRes, settingsRes] = await Promise.all([
|
|
568
597
|
fetch('/plugins/signalk-app-dock/config'),
|
|
569
598
|
fetch('/plugins/signalk-app-dock/settings')
|
|
570
599
|
])
|
|
571
|
-
const prev =
|
|
600
|
+
const prev = cfg
|
|
601
|
+
let next = cfg
|
|
572
602
|
if (configRes.ok) {
|
|
573
603
|
const data = await configRes.json()
|
|
574
604
|
const pluginCfg = data.configuration || data
|
|
575
|
-
|
|
605
|
+
next = { ...DEFAULTS, ...pluginCfg, apps: prev.apps }
|
|
576
606
|
}
|
|
577
607
|
if (settingsRes.ok) {
|
|
578
608
|
const data = await settingsRes.json()
|
|
579
609
|
if (Array.isArray(data.apps) && data.apps.length > 0) {
|
|
580
|
-
|
|
610
|
+
next = { ...next, apps: data.apps }
|
|
581
611
|
}
|
|
582
612
|
}
|
|
583
|
-
|
|
613
|
+
const structuralChanged = STRUCTURAL_KEYS.some((k) => prev[k] !== next[k])
|
|
614
|
+
if (structuralChanged) {
|
|
615
|
+
window.location.reload()
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
if (JSON.stringify(next.apps) !== JSON.stringify(prev.apps)) {
|
|
619
|
+
cfg = next
|
|
584
620
|
buildDock()
|
|
621
|
+
} else {
|
|
622
|
+
cfg = next
|
|
585
623
|
}
|
|
586
624
|
} catch {
|
|
587
625
|
// 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
|
|
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}
|
package/test/plugin.test.js
CHANGED
|
@@ -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
|