@signalk/app-dock 1.0.0 → 1.1.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/.gitattributes ADDED
@@ -0,0 +1,5 @@
1
+ # Force LF line endings on checkout regardless of the host's
2
+ # core.autocrlf setting. Prettier/eslint enforce LF; without this,
3
+ # Windows CI runners check out CRLF and `prettier --check` reports
4
+ # every line as "Delete `␍`".
5
+ * text=auto eol=lf
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/app-dock",
3
- "version": "1.0.0",
3
+ "version": "1.1.0-beta.1",
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
@@ -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)))}}}]);
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
 
@@ -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
- <div
470
- style={{
471
- ...S.iconBox,
472
- width: 40,
473
- height: 40,
474
- background: 'rgba(255,255,255,0.08)',
475
- border: '1px solid rgba(255,255,255,0.12)',
476
- fontSize: 20,
477
- borderRadius: 10
478
- }}
479
- >
480
- {'\u2600\uFE0F'}
481
- </div>
482
- <div style={{ width: 1, height: 24, background: 'rgba(255,255,255,0.15)', alignSelf: 'center' }} />
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} />