@signalk/app-dock 1.4.0 → 1.4.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.
@@ -4,232 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>App Dock - Configuration</title>
7
- <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
- body {
14
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
15
- background: #1a1a2e;
16
- color: #e0e0e0;
17
- padding: 24px;
18
- min-height: 100vh;
19
- }
20
- h1 {
21
- font-size: 22px;
22
- font-weight: 600;
23
- margin-bottom: 8px;
24
- }
25
- .subtitle {
26
- color: #888;
27
- font-size: 13px;
28
- margin-bottom: 24px;
29
- }
30
- .section {
31
- margin-bottom: 32px;
32
- }
33
- .section-title {
34
- font-size: 14px;
35
- font-weight: 600;
36
- color: #aaa;
37
- text-transform: uppercase;
38
- letter-spacing: 0.05em;
39
- margin-bottom: 12px;
40
- }
41
-
42
- /* Discover button */
43
- .btn {
44
- display: inline-flex;
45
- align-items: center;
46
- gap: 8px;
47
- padding: 10px 20px;
48
- border: none;
49
- border-radius: 8px;
50
- font-size: 14px;
51
- font-weight: 600;
52
- cursor: pointer;
53
- transition: all 0.15s ease;
54
- }
55
- .btn-primary {
56
- background: #3b82f6;
57
- color: #fff;
58
- }
59
- .btn-primary:hover {
60
- background: #2563eb;
61
- }
62
- .btn-primary:disabled {
63
- background: #555;
64
- cursor: not-allowed;
65
- }
66
- .btn-save {
67
- background: #10b981;
68
- color: #fff;
69
- }
70
- .btn-save:hover {
71
- background: #059669;
72
- }
73
- .btn-danger {
74
- background: #ef4444;
75
- color: #fff;
76
- padding: 6px 12px;
77
- font-size: 12px;
78
- }
79
- .btn-danger:hover {
80
- background: #dc2626;
81
- }
82
-
83
- /* App list */
84
- .app-list {
85
- list-style: none;
86
- }
87
- .app-item {
88
- display: flex;
89
- align-items: center;
90
- gap: 12px;
91
- padding: 10px 14px;
92
- background: rgba(255, 255, 255, 0.05);
93
- border: 1px solid rgba(255, 255, 255, 0.08);
94
- border-radius: 10px;
95
- margin-bottom: 8px;
96
- cursor: grab;
97
- transition:
98
- background 0.15s ease,
99
- border-color 0.15s ease;
100
- }
101
- .app-item:hover {
102
- background: rgba(255, 255, 255, 0.08);
103
- border-color: rgba(255, 255, 255, 0.15);
104
- }
105
- .app-item.dragging {
106
- opacity: 0.5;
107
- background: rgba(59, 130, 246, 0.15);
108
- }
109
- .app-item.drag-over {
110
- border-color: #3b82f6;
111
- }
112
-
113
- .drag-handle {
114
- color: #555;
115
- font-size: 18px;
116
- cursor: grab;
117
- user-select: none;
118
- flex-shrink: 0;
119
- width: 20px;
120
- text-align: center;
121
- }
122
-
123
- .app-icon-preview {
124
- width: 40px;
125
- height: 40px;
126
- border-radius: 10px;
127
- display: flex;
128
- align-items: center;
129
- justify-content: center;
130
- background: rgba(58, 58, 62, 0.9);
131
- overflow: hidden;
132
- flex-shrink: 0;
133
- font-size: 20px;
134
- }
135
- .app-icon-preview img {
136
- width: 100%;
137
- height: 100%;
138
- object-fit: contain;
139
- padding: 6px;
140
- }
141
-
142
- .app-info {
143
- flex: 1;
144
- min-width: 0;
145
- }
146
- .app-name {
147
- font-size: 14px;
148
- font-weight: 600;
149
- }
150
- .app-url {
151
- font-size: 11px;
152
- color: #777;
153
- overflow: hidden;
154
- text-overflow: ellipsis;
155
- white-space: nowrap;
156
- }
157
-
158
- .app-toggle {
159
- flex-shrink: 0;
160
- }
161
- .app-toggle input[type='checkbox'] {
162
- width: 18px;
163
- height: 18px;
164
- cursor: pointer;
165
- accent-color: #3b82f6;
166
- }
167
-
168
- .app-actions {
169
- flex-shrink: 0;
170
- }
171
-
172
- .empty-state {
173
- text-align: center;
174
- padding: 40px 20px;
175
- color: #666;
176
- font-size: 14px;
177
- }
178
-
179
- .status {
180
- margin-top: 12px;
181
- font-size: 13px;
182
- color: #10b981;
183
- min-height: 20px;
184
- }
185
- .status.error {
186
- color: #ef4444;
187
- }
188
-
189
- .actions-bar {
190
- display: flex;
191
- gap: 12px;
192
- align-items: center;
193
- margin-top: 20px;
194
- }
195
-
196
- /* Dock preview */
197
- .dock-preview {
198
- display: flex;
199
- align-items: flex-end;
200
- justify-content: center;
201
- gap: 8px;
202
- padding: 14px 20px;
203
- background: rgba(30, 30, 32, 0.55);
204
- backdrop-filter: blur(60px);
205
- border: 1px solid rgba(255, 255, 255, 0.18);
206
- border-radius: 22px;
207
- box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
208
- margin: 20px auto;
209
- width: fit-content;
210
- }
211
- .dock-preview-icon {
212
- width: 48px;
213
- height: 48px;
214
- border-radius: 12px;
215
- display: flex;
216
- align-items: center;
217
- justify-content: center;
218
- background: rgba(58, 58, 62, 0.9);
219
- overflow: hidden;
220
- font-size: 22px;
221
- transition: transform 0.15s ease;
222
- }
223
- .dock-preview-icon:hover {
224
- transform: scale(1.3);
225
- }
226
- .dock-preview-icon img {
227
- width: 100%;
228
- height: 100%;
229
- object-fit: contain;
230
- padding: 6px;
231
- }
232
- </style>
7
+ <style>*{box-sizing:border-box;margin:0;padding:0}body{color:#e0e0e0;background:#1a1a2e;min-height:100vh;padding:24px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}h1{margin-bottom:8px;font-size:22px;font-weight:600}.subtitle{color:#888;margin-bottom:24px;font-size:13px}.section{margin-bottom:32px}.section-title{color:#aaa;text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px;font-size:14px;font-weight:600}.btn{cursor:pointer;border:none;border-radius:8px;align-items:center;gap:8px;padding:10px 20px;font-size:14px;font-weight:600;transition:all .15s;display:inline-flex}.btn-primary{color:#fff;background:#3b82f6}.btn-primary:hover{background:#2563eb}.btn-primary:disabled{cursor:not-allowed;background:#555}.btn-save{color:#fff;background:#10b981}.btn-save:hover{background:#059669}.btn-danger{color:#fff;background:#ef4444;padding:6px 12px;font-size:12px}.btn-danger:hover{background:#dc2626}.app-list{list-style:none}.app-item{cursor:grab;background:#ffffff0d;border:1px solid #ffffff14;border-radius:10px;align-items:center;gap:12px;margin-bottom:8px;padding:10px 14px;transition:background .15s,border-color .15s;display:flex}.app-item:hover{background:#ffffff14;border-color:#ffffff26}.app-item.dragging{opacity:.5;background:#3b82f626}.app-item.drag-over{border-color:#3b82f6}.drag-handle{color:#555;cursor:grab;-webkit-user-select:none;user-select:none;text-align:center;flex-shrink:0;width:20px;font-size:18px}.app-icon-preview{background:#3a3a3ee6;border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;width:40px;height:40px;font-size:20px;display:flex;overflow:hidden}.app-icon-preview img{object-fit:contain;width:100%;height:100%;padding:6px}.app-info{flex:1;min-width:0}.app-name{font-size:14px;font-weight:600}.app-url{color:#777;text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.app-toggle{flex-shrink:0}.app-toggle input[type=checkbox]{cursor:pointer;accent-color:#3b82f6;width:18px;height:18px}.app-actions{flex-shrink:0}.empty-state{text-align:center;color:#666;padding:40px 20px;font-size:14px}.status{color:#10b981;min-height:20px;margin-top:12px;font-size:13px}.status.error{color:#ef4444}.actions-bar{align-items:center;gap:12px;margin-top:20px;display:flex}.dock-preview{-webkit-backdrop-filter:blur(60px);backdrop-filter:blur(60px);background:#1e1e208c;border:1px solid #ffffff2e;border-radius:22px;justify-content:center;align-items:flex-end;gap:8px;width:-moz-fit-content;width:fit-content;margin:20px auto;padding:14px 20px;display:flex;box-shadow:0 16px 48px #00000080}.dock-preview-icon{background:#3a3a3ee6;border-radius:12px;justify-content:center;align-items:center;width:48px;height:48px;font-size:22px;transition:transform .15s;display:flex;overflow:hidden}.dock-preview-icon:hover{transform:scale(1.3)}.dock-preview-icon img{object-fit:contain;width:100%;height:100%;padding:6px}</style>
8
+ <script type="module" crossorigin src="./config.js"></script>
233
9
  </head>
234
10
  <body>
235
11
  <h1>App Dock Configuration</h1>
@@ -237,7 +13,7 @@
237
13
 
238
14
  <div class="section">
239
15
  <div class="section-title">Webapps</div>
240
- <button class="btn btn-primary" id="discover-btn" onclick="discover()">Discover Installed Webapps</button>
16
+ <button class="btn btn-primary" id="discover-btn">Discover Installed Webapps</button>
241
17
  <div class="status" id="status"></div>
242
18
  </div>
243
19
 
@@ -253,217 +29,8 @@
253
29
  </div>
254
30
 
255
31
  <div class="actions-bar">
256
- <button class="btn btn-save" onclick="save()">Save Configuration</button>
32
+ <button class="btn btn-save" id="save-btn">Save Configuration</button>
257
33
  </div>
258
34
 
259
- <script>
260
- let apps = []
261
-
262
- async function loadCurrent() {
263
- try {
264
- const res = await fetch('/plugins/signalk-app-dock/config')
265
- if (res.ok) {
266
- const data = await res.json()
267
- apps = (data.configuration && data.configuration.apps) || []
268
- render()
269
- }
270
- } catch (e) {
271
- console.warn('Could not load current config', e)
272
- }
273
- }
274
-
275
- async function discover() {
276
- const btn = document.getElementById('discover-btn')
277
- const status = document.getElementById('status')
278
- btn.disabled = true
279
- btn.textContent = 'Discovering...'
280
- status.textContent = ''
281
- status.className = 'status'
282
-
283
- try {
284
- const res = await fetch('/skServer/webapps')
285
- const allWebapps = await res.json()
286
- const webapps = allWebapps
287
- .filter((w) => w.name !== '@signalk/app-dock' && w.name !== '@signalk/server-admin-ui')
288
- .map((w) => ({
289
- name: w.name,
290
- label: w.signalk?.displayName || w.name,
291
- url: `/${w.name}/`,
292
- icon: w.signalk?.appIcon ? `/${w.name}/${w.signalk.appIcon}` : null
293
- }))
294
-
295
- if (webapps.length === 0) {
296
- status.textContent = 'No webapps found. Is the server fully started?'
297
- status.className = 'status error'
298
- btn.disabled = false
299
- btn.textContent = 'Discover Installed Webapps'
300
- return
301
- }
302
-
303
- const existingUrls = new Set(apps.map((a) => a.url))
304
- let added = 0
305
- for (const w of webapps) {
306
- if (!existingUrls.has(w.url)) {
307
- apps.push({
308
- enabled: true,
309
- url: w.url,
310
- label: w.label || '',
311
- icon: w.icon || '',
312
- color: ''
313
- })
314
- added++
315
- }
316
- }
317
-
318
- status.textContent =
319
- added > 0
320
- ? `Found ${webapps.length} webapps, added ${added} new.`
321
- : `All ${webapps.length} webapps already in list.`
322
- render()
323
- } catch (e) {
324
- status.textContent = 'Discovery failed: ' + e.message
325
- status.className = 'status error'
326
- }
327
-
328
- btn.disabled = false
329
- btn.textContent = 'Discover Installed Webapps'
330
- }
331
-
332
- async function save() {
333
- const status = document.getElementById('status')
334
- try {
335
- const cfgRes = await fetch('/plugins/signalk-app-dock/config')
336
- const current = await cfgRes.json()
337
- const newConfig = {
338
- ...current,
339
- configuration: {
340
- ...(current.configuration || {}),
341
- apps: apps
342
- }
343
- }
344
-
345
- const res = await fetch('/plugins/signalk-app-dock/config', {
346
- method: 'POST',
347
- headers: { 'Content-Type': 'application/json' },
348
- body: JSON.stringify(newConfig)
349
- })
350
-
351
- if (res.ok) {
352
- status.textContent = 'Saved! Plugin will restart with new config.'
353
- status.className = 'status'
354
- } else {
355
- status.textContent = 'Save failed: ' + res.statusText
356
- status.className = 'status error'
357
- }
358
- } catch (e) {
359
- status.textContent = 'Save failed: ' + e.message
360
- status.className = 'status error'
361
- }
362
- }
363
-
364
- function removeApp(index) {
365
- apps.splice(index, 1)
366
- render()
367
- }
368
-
369
- function toggleApp(index) {
370
- apps[index].enabled = !apps[index].enabled
371
- render()
372
- }
373
-
374
- function renderIcon(app) {
375
- const iconVal = app.icon || ''
376
- if (iconVal.startsWith('/') || iconVal.startsWith('http')) {
377
- return `<img src="${iconVal}" alt="${app.label}" onerror="this.parentNode.textContent='${(app.label || app.url || '??').slice(0, 2).toUpperCase()}'">`
378
- }
379
- return iconVal || (app.label || app.url || '??').slice(0, 2).toUpperCase()
380
- }
381
-
382
- function render() {
383
- const list = document.getElementById('app-list')
384
- const empty = document.getElementById('empty-state')
385
- const preview = document.getElementById('dock-preview')
386
-
387
- if (apps.length === 0) {
388
- list.innerHTML = ''
389
- empty.style.display = ''
390
- preview.innerHTML = '<span style="color:#555;font-size:13px">No apps</span>'
391
- return
392
- }
393
-
394
- empty.style.display = 'none'
395
-
396
- list.innerHTML = apps
397
- .map(
398
- (app, i) => `
399
- <li class="app-item" draggable="true" data-index="${i}" style="opacity:${app.enabled === false ? 0.4 : 1}">
400
- <span class="drag-handle">\u2261</span>
401
- <div class="app-icon-preview" ${app.color ? `style="background:${app.color}"` : ''}>
402
- ${renderIcon(app)}
403
- </div>
404
- <div class="app-info">
405
- <div class="app-name">${app.label || app.url}</div>
406
- <div class="app-url">${app.url}</div>
407
- </div>
408
- <div class="app-toggle">
409
- <input type="checkbox" ${app.enabled !== false ? 'checked' : ''} onchange="toggleApp(${i})" title="Enable/disable">
410
- </div>
411
- <div class="app-actions">
412
- <button class="btn btn-danger" onclick="removeApp(${i})">Remove</button>
413
- </div>
414
- </li>
415
- `
416
- )
417
- .join('')
418
-
419
- initDragDrop()
420
-
421
- preview.innerHTML = apps
422
- .filter((a) => a.enabled !== false)
423
- .map(
424
- (app) => `
425
- <div class="dock-preview-icon" ${app.color ? `style="background:${app.color}"` : ''}>
426
- ${renderIcon(app)}
427
- </div>
428
- `
429
- )
430
- .join('')
431
- }
432
-
433
- let dragIndex = null
434
- function initDragDrop() {
435
- const items = document.querySelectorAll('.app-item')
436
- items.forEach((item) => {
437
- item.addEventListener('dragstart', (e) => {
438
- dragIndex = parseInt(item.dataset.index)
439
- item.classList.add('dragging')
440
- e.dataTransfer.effectAllowed = 'move'
441
- })
442
- item.addEventListener('dragend', () => {
443
- item.classList.remove('dragging')
444
- document.querySelectorAll('.app-item').forEach((el) => el.classList.remove('drag-over'))
445
- })
446
- item.addEventListener('dragover', (e) => {
447
- e.preventDefault()
448
- e.dataTransfer.dropEffect = 'move'
449
- item.classList.add('drag-over')
450
- })
451
- item.addEventListener('dragleave', () => {
452
- item.classList.remove('drag-over')
453
- })
454
- item.addEventListener('drop', (e) => {
455
- e.preventDefault()
456
- const dropIndex = parseInt(item.dataset.index)
457
- if (dragIndex !== null && dragIndex !== dropIndex) {
458
- const moved = apps.splice(dragIndex, 1)[0]
459
- apps.splice(dropIndex, 0, moved)
460
- render()
461
- }
462
- })
463
- })
464
- }
465
-
466
- loadCurrent()
467
- </script>
468
35
  </body>
469
36
  </html>
@@ -0,0 +1,22 @@
1
+ function e(e){let t=document.getElementById(e);if(!t)throw Error(`config.html: #${e} is missing`);return t}function t(e){return e instanceof Error?e.message:String(e)}var n=e(`discover-btn`),r=e(`save-btn`),i=e(`status`),a=e(`app-list`),o=e(`empty-state`),s=e(`dock-preview`),c=[];async function l(){try{let e=await fetch(`/plugins/signalk-app-dock/config`);if(e.ok){let t=await e.json();c=t.configuration&&t.configuration.apps||[],h()}}catch(e){console.warn(`Could not load current config`,e)}}async function u(){n.disabled=!0,n.textContent=`Discovering...`,i.textContent=``,i.className=`status`;try{let e=(await(await fetch(`/skServer/webapps`)).json()).filter(e=>e.name!==`@signalk/app-dock`&&e.name!==`@signalk/server-admin-ui`).map(e=>({name:e.name,label:e.signalk?.displayName||e.name,url:`/${e.name}/`,icon:e.signalk?.appIcon?`/${e.name}/${e.signalk.appIcon}`:null}));if(e.length===0){i.textContent=`No webapps found. Is the server fully started?`,i.className=`status error`,n.disabled=!1,n.textContent=`Discover Installed Webapps`;return}let t=new Set(c.map(e=>e.url)),r=0;for(let n of e)t.has(n.url)||(c.push({enabled:!0,url:n.url,label:n.label||``,icon:n.icon||``,color:``}),r++);i.textContent=r>0?`Found ${e.length} webapps, added ${r} new.`:`All ${e.length} webapps already in list.`,h()}catch(e){i.textContent=`Discovery failed: `+t(e),i.className=`status error`}n.disabled=!1,n.textContent=`Discover Installed Webapps`}async function d(){try{let e=await(await fetch(`/plugins/signalk-app-dock/config`)).json(),t={...e,configuration:{...e.configuration||{},apps:c}},n=await fetch(`/plugins/signalk-app-dock/config`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});n.ok?(i.textContent=`Saved! Plugin will restart with new config.`,i.className=`status`):(i.textContent=`Save failed: `+n.statusText,i.className=`status error`)}catch(e){i.textContent=`Save failed: `+t(e),i.className=`status error`}}function f(e){c.splice(e,1),h()}function p(e){c[e].enabled=!c[e].enabled,h()}function m(e){let t=e.icon||``;return t.startsWith(`/`)||t.startsWith(`http`)?`<img src="${t}" alt="${e.label}" onerror="this.parentNode.textContent='${(e.label||e.url||`??`).slice(0,2).toUpperCase()}'">`:t||(e.label||e.url||`??`).slice(0,2).toUpperCase()}function h(){c.length===0?(a.innerHTML=``,o.style.display=``,s.innerHTML=`<span style="color:#555;font-size:13px">No apps</span>`):(o.style.display=`none`,a.innerHTML=c.map((e,t)=>`
2
+ <li class="app-item" draggable="true" data-index="${t}" style="opacity:${e.enabled===!1?.4:1}">
3
+ <span class="drag-handle">≡</span>
4
+ <div class="app-icon-preview" ${e.color?`style="background:${e.color}"`:``}>
5
+ ${m(e)}
6
+ </div>
7
+ <div class="app-info">
8
+ <div class="app-name">${e.label||e.url}</div>
9
+ <div class="app-url">${e.url}</div>
10
+ </div>
11
+ <div class="app-toggle">
12
+ <input type="checkbox" ${e.enabled===!1?``:`checked`} data-action="toggle" title="Enable/disable">
13
+ </div>
14
+ <div class="app-actions">
15
+ <button class="btn btn-danger" data-action="remove">Remove</button>
16
+ </div>
17
+ </li>
18
+ `).join(``),_(),s.innerHTML=c.filter(e=>e.enabled!==!1).map(e=>`
19
+ <div class="dock-preview-icon" ${e.color?`style="background:${e.color}"`:``}>
20
+ ${m(e)}
21
+ </div>
22
+ `).join(``))}var g=null;function _(){document.querySelectorAll(`.app-item`).forEach(e=>{e.addEventListener(`dragstart`,t=>{g=parseInt(e.dataset.index||``,10),e.classList.add(`dragging`),t.dataTransfer&&(t.dataTransfer.effectAllowed=`move`)}),e.addEventListener(`dragend`,()=>{e.classList.remove(`dragging`),document.querySelectorAll(`.app-item`).forEach(e=>e.classList.remove(`drag-over`))}),e.addEventListener(`dragover`,t=>{t.preventDefault(),t.dataTransfer&&(t.dataTransfer.dropEffect=`move`),e.classList.add(`drag-over`)}),e.addEventListener(`dragleave`,()=>{e.classList.remove(`drag-over`)}),e.addEventListener(`drop`,t=>{t.preventDefault();let n=parseInt(e.dataset.index||``,10);if(g!==null&&g!==n){let e=c.splice(g,1)[0];c.splice(n,0,e),h()}})})}function v(e){let t=e.target instanceof HTMLElement?e.target.closest(`[data-action]`):null,n=t?.closest(`.app-item`);return!t||!n?null:{action:t.dataset.action||``,index:parseInt(n.dataset.index||``,10)}}a.addEventListener(`change`,e=>{let t=v(e);t?.action===`toggle`&&p(t.index)}),a.addEventListener(`click`,e=>{let t=v(e);t?.action===`remove`&&f(t.index)}),n.addEventListener(`click`,()=>void u()),r.addEventListener(`click`,()=>void d()),l();
@@ -0,0 +1 @@
1
+ var e=window.__SK_REACT__;if(!e)throw Error(`App Dock: window.__SK_REACT__ is not set. The host signalk-server admin UI must expose React on window for plugin federation. Requires signalk-server 2.27.0 or newer.`);var{Activity:t,Children:n,Component:r,Fragment:i,Profiler:a,PureComponent:o,StrictMode:s,Suspense:ee,cache:c,cacheSignal:l,cloneElement:u,createContext:te,createElement:d,createRef:f,forwardRef:p,isValidElement:m,lazy:h,memo:g,startTransition:_,use:v,useActionState:y,useCallback:b,useContext:x,useDebugValue:S,useDeferredValue:C,useEffect:w,useEffectEvent:T,useId:E,useImperativeHandle:D,useInsertionEffect:O,useLayoutEffect:k,useMemo:A,useOptimistic:j,useReducer:M,useRef:N,useState:P,useSyncExternalStore:F,useTransition:I,version:L}=e,R=(e,t)=>`<svg class="dock-svg-icon dock-svg-${e}" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${t}</svg>`,z={sun:R(`sun`,`<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>`),moon:R(`moon`,`<path d="M20.5 14A9 9 0 1 1 10 3.5a7.5 7.5 0 0 0 10.5 10.5z"/>`),fullscreen:R(`fullscreen`,`<path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"/>`),exitFullscreen:R(`exit-fullscreen`,`<path d="M9 4v5H4M20 9h-5V4M15 20v-5h5M4 15h5v5"/>`),close:R(`close`,`<path d="M6 6l12 12M18 6L6 18"/>`)},B=window.__SK_REACT_JSX_RUNTIME__;if(!B)throw Error(`App Dock: window.__SK_REACT_JSX_RUNTIME__ is not set by the host.`);var V=B,{Fragment:H,jsx:U,jsxs:W}=V;V.jsxDEV;function G(e){return e instanceof Error?e.message:String(e)}var K={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 q({icon:e,label:t,color:n,size:r}){let i=r||36,a={...K.iconBox,width:i,height:i,borderRadius:i*.22,...n?{background:n}:{}},o=e||``;return o.startsWith(`/`)||o.startsWith(`http`)?U(`div`,{style:a,children:U(`img`,{src:o,alt:t,style:{...K.iconImg,padding:i*.12},onError:e=>{let n=e.currentTarget;n.style.display=`none`,n.parentElement&&(n.parentElement.textContent=(t||`??`).slice(0,2).toUpperCase())}})}):U(`div`,{style:a,children:o||(t||`??`).slice(0,2).toUpperCase()})}function J({name:e,color:t}){return U(`span`,{style:{display:`flex`,width:`55%`,height:`55%`,color:t||`#fff`},dangerouslySetInnerHTML:{__html:z[e]}})}function Y({label:e,value:t,options:n,onChange:r,hint:i}){return W(`div`,{style:K.fieldRow,children:[U(`span`,{style:K.label,children:e}),U(`select`,{style:K.select,value:t,onChange:e=>r(e.target.value),children:n.map(e=>U(`option`,{value:e.value,children:e.label},e.value))}),i&&U(`span`,{style:K.hint,children:i})]})}function X({label:e,value:t,onChange:n,hint:r}){return W(`div`,{style:K.fieldRow,children:[U(`span`,{style:K.label,children:e}),U(`input`,{style:K.input,type:`number`,value:t,onChange:e=>n(Number(e.target.value))}),r&&U(`span`,{style:K.hint,children:r})]})}function Z({label:e,checked:t,onChange:n,hint:r}){return W(`div`,{style:K.fieldRow,children:[U(`span`,{style:K.label,children:e}),U(`input`,{type:`checkbox`,style:K.checkboxField,checked:t,onChange:e=>n(e.target.checked)}),r&&U(`span`,{style:K.hint,children:r})]})}function Q({configuration:e,save:t}){let n=e||{},[r,i]=P(()=>n.apps||[]),[a,o]=P(n.position||`bottom`),[s,ee]=P(n.iframeMode||`keep-alive`),[c,l]=P(n.iconSize||56),[u,te]=P(n.magnification!==!1),[d,f]=P(n.magnificationScale||1.7),[p,m]=P(n.showNightModeButton||!1),[h,g]=P(n.showFullscreenButton||!1),[_,v]=P(n.showExitButton||!1),y=n.tourDismissed||!1,[x,S]=P(``),[C,T]=P(!1),[E,D]=P(!1),[O,k]=P(!1),[A,j]=P(null),[M,F]=P(null),I=N(r);w(()=>{I.current=r},[r]);let L=N(null);w(()=>()=>{L.current&&clearTimeout(L.current)},[]);let R=b(e=>({position:a,iframeMode:s,iconSize:c,magnification:u,magnificationScale:d,showNightModeButton:p,showFullscreenButton:h,showExitButton:_,tourDismissed:y,apps:e}),[a,s,c,u,d,p,h,_,y]),z=b(async()=>{L.current&&clearTimeout(L.current),k(!0),S(`Saving…`),T(!1);try{await t(R(r)),S(`Configuration saved — plugin will restart`),T(!1),L.current=setTimeout(()=>S(``),5e3)}catch(e){S(`Save failed: `+G(e)),T(!0)}finally{k(!1)}},[r,R,t]),B=async()=>{D(!0),S(``),T(!1);try{let e=(await(await fetch(`/skServer/webapps`)).json()).filter(e=>e.name!==`@signalk/app-dock`).map(e=>e.name===`@signalk/server-admin-ui`?{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(e.length===0){S(`No webapps found. Is the server fully started?`),T(!0),D(!1);return}let t=I.current,n=new Set(t.map(e=>e.url)),r=0,a=[...t];for(let t of e)n.has(t.url)||(a.push({enabled:!0,autostart:!1,url:t.url,label:t.label||``,icon:t.icon||``,color:t.isAdmin?`#78788c`:``}),r++);i(a),S(r>0?`Found ${e.length} webapps, added ${r} new.`:`All ${e.length} webapps already in list.`),T(!1)}catch(e){S(`Discovery failed: `+G(e)),T(!0)}D(!1)},V=e=>i(r.map((t,n)=>n===e?{...t,enabled:!t.enabled}:t)),Q=e=>i(r.map((t,n)=>n===e?{...t,autostart:!t.autostart}:{...t,autostart:!1})),ne=e=>i(r.filter((t,n)=>n!==e)),re=e=>j(e),ie=(e,t)=>{e.preventDefault(),F(t)},ae=()=>F(null),oe=(e,t)=>{if(e.preventDefault(),F(null),A!==null&&A!==t){let e=[...r],[n]=e.splice(A,1);e.splice(t,0,n),i(e)}j(null)},se=()=>{j(null),F(null)},$=r.filter(e=>e.enabled!==!1);return W(`div`,{style:K.root,children:[U(`div`,{style:K.sectionTitle,children:`Dock Settings`}),U(Y,{label:`Dock position`,value:a,onChange:o,options:[{value:`bottom`,label:`Bottom`},{value:`top`,label:`Top`},{value:`left`,label:`Left`},{value:`right`,label:`Right`}]}),U(Y,{label:`iFrame lifecycle`,value:s,onChange:ee,options:[{value:`keep-alive`,label:`Keep-alive (faster switching, more RAM)`},{value:`destroy`,label:`Destroy (reload each time, less RAM)`}]}),U(X,{label:`Icon size`,value:c,onChange:l,hint:`px`}),U(Z,{label:`Magnification effect`,checked:u,onChange:te}),u&&U(X,{label:`Magnification scale`,value:d,onChange:f,hint:`1.0 - 2.5`}),U(`div`,{style:K.sectionTitle,children:`Utility Buttons`}),U(Z,{label:`Night/Day mode toggle`,checked:p,onChange:m,hint:`Sun/moon button to toggle environment.mode`}),U(Z,{label:`Fullscreen toggle`,checked:h,onChange:g,hint:`Toggles browser fullscreen. Hidden on devices without Fullscreen API support (e.g. iPadOS Safari).`}),U(Z,{label:`Exit button`,checked:_,onChange:v,hint:`X button to return to Signal K admin UI`}),U(`div`,{style:K.sectionTitle,children:`Webapps`}),U(`button`,{style:{...K.btn,...K.btnPrimary,...E?{opacity:.6}:{}},onClick:()=>void B(),disabled:E,children:E?`Discovering…`:`Discover Installed Webapps`}),x&&U(`div`,{style:{...K.status,color:C?`#ef4444`:`#10b981`},children:x}),U(`div`,{style:K.sectionTitle,children:`Dock Apps (drag to reorder)`}),r.length===0?U(`div`,{style:K.empty,children:`No apps yet. Click Discover above.`}):U(`div`,{children:r.map((e,t)=>W(`div`,{draggable:!0,onDragStart:()=>re(t),onDragOver:e=>ie(e,t),onDragLeave:ae,onDrop:e=>oe(e,t),onDragEnd:se,style:{...K.item,...e.enabled===!1?K.itemDisabled:{},...M===t?K.dragOver:{},...A===t?{opacity:.4}:{}},children:[U(`span`,{style:K.handle,children:`≡`}),U(q,{icon:e.icon,label:e.label||e.url,color:e.color}),W(`div`,{style:K.info,children:[U(`div`,{style:K.name,children:e.label||e.url}),U(`div`,{style:K.url,children:e.url})]}),U(`div`,{style:K.toggle,title:`Enabled`,children:U(`input`,{type:`checkbox`,checked:e.enabled!==!1,onChange:()=>V(t),style:K.checkbox})}),U(`div`,{style:{...K.toggle,cursor:`pointer`,fontSize:16,opacity:e.autostart?1:.25},onClick:()=>Q(t),title:e.autostart?`Autostart: ON (click to disable)`:`Click to set as autostart app`,children:`▶`}),U(`button`,{style:{...K.btn,...K.btnDanger},onClick:()=>ne(t),children:`Remove`})]},e.url+t))}),($.length>0||p||h||_)&&W(H,{children:[U(`div`,{style:K.sectionTitle,children:`Preview`}),W(`div`,{style:K.preview,children:[p&&U(`div`,{style:{...K.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},children:U(J,{name:`sun`,color:`#fbbf24`})}),h&&U(`div`,{style:{...K.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`},children:U(J,{name:`fullscreen`})}),(p||h)&&U(`div`,{style:{width:1,height:24,background:`rgba(255,255,255,0.15)`,alignSelf:`center`}}),$.map((e,t)=>U(q,{icon:e.icon,label:e.label||e.url,color:e.color,size:40},t)),_&&W(H,{children:[U(`div`,{style:{width:1,height:24,background:`rgba(255,255,255,0.15)`,alignSelf:`center`}}),U(`div`,{style:{...K.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`},children:U(J,{name:`close`})})]})]})]}),W(`div`,{style:K.actions,children:[U(`button`,{style:{...K.btn,...K.btnSave,...O?{opacity:.6}:{}},onClick:()=>void z(),disabled:O,children:O?`Saving…`:`Save Configuration`}),x&&U(`div`,{style:{...K.status,color:C?`#ef4444`:`#10b981`},children:x})]})]})}export{Q as default};
@@ -0,0 +1 @@
1
+ import"./dist-D1jGdgWH.js";var e={},t=[];export{t as usedRemotes,e as usedShared};