@signalk/app-dock 0.1.0

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.
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="72" height="72">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#1e3a5f"/>
5
+ <stop offset="100%" stop-color="#0a1628"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <!-- Background -->
9
+ <rect width="72" height="72" rx="16" fill="url(#bg)"/>
10
+ <!-- Dock pill -->
11
+ <rect x="8" y="48" width="56" height="16" rx="8" fill="#ffffff" fill-opacity="0.1" stroke="#ffffff" stroke-opacity="0.2" stroke-width="0.5"/>
12
+ <!-- Dock icons - 3 squares in the pill -->
13
+ <rect x="14" y="51" width="10" height="10" rx="2.5" fill="#3b82f6" opacity="0.9"/>
14
+ <rect x="31" y="51" width="10" height="10" rx="2.5" fill="#10b981" opacity="0.9"/>
15
+ <rect x="48" y="51" width="10" height="10" rx="2.5" fill="#f59e0b" opacity="0.9"/>
16
+ <!-- Middle icon magnified (popping up) -->
17
+ <rect x="28" y="34" width="16" height="16" rx="3.5" fill="#10b981" opacity="0.95"/>
18
+ <!-- Active dot under middle icon -->
19
+ <circle cx="36" cy="66" r="1.5" fill="#ffffff" fill-opacity="0.8"/>
20
+ <!-- Compass rose hint -->
21
+ <circle cx="36" cy="22" r="10" fill="none" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1"/>
22
+ <line x1="36" y1="14" x2="36" y2="18" stroke="#ffffff" stroke-opacity="0.3" stroke-width="1" stroke-linecap="round"/>
23
+ <line x1="36" y1="26" x2="36" y2="30" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
24
+ <line x1="28" y1="22" x2="32" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
25
+ <line x1="40" y1="22" x2="44" y2="22" stroke="#ffffff" stroke-opacity="0.15" stroke-width="1" stroke-linecap="round"/>
26
+ <!-- Compass needle -->
27
+ <polygon points="36,15 34,22 36,20 38,22" fill="#ef4444" fill-opacity="0.7"/>
28
+ <polygon points="36,29 34,22 36,24 38,22" fill="#ffffff" fill-opacity="0.25"/>
29
+ </svg>
@@ -0,0 +1,469 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
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>
233
+ </head>
234
+ <body>
235
+ <h1>App Dock Configuration</h1>
236
+ <p class="subtitle">Discover installed webapps, arrange their order, and customize the dock.</p>
237
+
238
+ <div class="section">
239
+ <div class="section-title">Webapps</div>
240
+ <button class="btn btn-primary" id="discover-btn" onclick="discover()">Discover Installed Webapps</button>
241
+ <div class="status" id="status"></div>
242
+ </div>
243
+
244
+ <div class="section">
245
+ <div class="section-title">Dock Apps (drag to reorder)</div>
246
+ <ul class="app-list" id="app-list"></ul>
247
+ <div class="empty-state" id="empty-state" style="display: none">No apps yet. Click Discover above.</div>
248
+ </div>
249
+
250
+ <div class="section">
251
+ <div class="section-title">Preview</div>
252
+ <div class="dock-preview" id="dock-preview"></div>
253
+ </div>
254
+
255
+ <div class="actions-bar">
256
+ <button class="btn btn-save" onclick="save()">Save Configuration</button>
257
+ </div>
258
+
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
+ </body>
469
+ </html>