@signalk/app-dock 1.2.1 → 1.3.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 +30 -0
- package/package.json +6 -1
- package/public/dock.css +62 -0
- package/public/dock.js +170 -17
- package/public/index.html +4 -0
- package/.gitattributes +0 -5
- package/.github/release.yml +0 -37
- package/.github/workflows/ci.yml +0 -11
- package/.github/workflows/label-by-title.yml +0 -67
- package/.github/workflows/release-please.yml +0 -109
- package/.github/workflows/release_on_tag.yml +0 -82
- package/.github/workflows/stale.yml +0 -42
- package/.prettierignore +0 -6
- package/.prettierrc +0 -6
- package/.release-please-manifest.json +0 -3
- package/AGENTS.md +0 -134
- package/CLAUDE.md +0 -1
- package/eslint.config.js +0 -59
- package/release-please-config.json +0 -11
- package/src/configpanel/PluginConfigurationPanel.js +0 -549
- package/src/configpanel/index.js +0 -1
- package/test/plugin.test.js +0 -696
- package/webpack.config.js +0 -39
|
@@ -1,549 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
const S = {
|
|
4
|
-
root: {
|
|
5
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
6
|
-
color: '#333',
|
|
7
|
-
padding: '16px 0'
|
|
8
|
-
},
|
|
9
|
-
sectionTitle: {
|
|
10
|
-
fontSize: 13,
|
|
11
|
-
fontWeight: 600,
|
|
12
|
-
color: '#888',
|
|
13
|
-
textTransform: 'uppercase',
|
|
14
|
-
letterSpacing: '0.05em',
|
|
15
|
-
marginBottom: 10,
|
|
16
|
-
marginTop: 24
|
|
17
|
-
},
|
|
18
|
-
btn: {
|
|
19
|
-
display: 'inline-flex',
|
|
20
|
-
alignItems: 'center',
|
|
21
|
-
gap: 8,
|
|
22
|
-
padding: '8px 16px',
|
|
23
|
-
border: 'none',
|
|
24
|
-
borderRadius: 6,
|
|
25
|
-
fontSize: 13,
|
|
26
|
-
fontWeight: 600,
|
|
27
|
-
cursor: 'pointer'
|
|
28
|
-
},
|
|
29
|
-
btnPrimary: { background: '#3b82f6', color: '#fff' },
|
|
30
|
-
btnSave: { background: '#10b981', color: '#fff' },
|
|
31
|
-
btnDanger: { background: '#ef4444', color: '#fff', padding: '4px 10px', fontSize: 11 },
|
|
32
|
-
status: { marginTop: 8, fontSize: 12, minHeight: 18 },
|
|
33
|
-
item: {
|
|
34
|
-
display: 'flex',
|
|
35
|
-
alignItems: 'center',
|
|
36
|
-
gap: 10,
|
|
37
|
-
padding: '8px 12px',
|
|
38
|
-
background: '#f8f9fa',
|
|
39
|
-
border: '1px solid #e0e0e0',
|
|
40
|
-
borderRadius: 8,
|
|
41
|
-
marginBottom: 6,
|
|
42
|
-
cursor: 'grab'
|
|
43
|
-
},
|
|
44
|
-
itemDisabled: { opacity: 0.4 },
|
|
45
|
-
dragOver: { borderColor: '#3b82f6', background: '#eef4ff' },
|
|
46
|
-
handle: {
|
|
47
|
-
color: '#bbb',
|
|
48
|
-
fontSize: 16,
|
|
49
|
-
cursor: 'grab',
|
|
50
|
-
userSelect: 'none',
|
|
51
|
-
flexShrink: 0,
|
|
52
|
-
width: 18,
|
|
53
|
-
textAlign: 'center'
|
|
54
|
-
},
|
|
55
|
-
iconBox: {
|
|
56
|
-
width: 36,
|
|
57
|
-
height: 36,
|
|
58
|
-
borderRadius: 8,
|
|
59
|
-
display: 'flex',
|
|
60
|
-
alignItems: 'center',
|
|
61
|
-
justifyContent: 'center',
|
|
62
|
-
background: '#e2e8f0',
|
|
63
|
-
overflow: 'hidden',
|
|
64
|
-
flexShrink: 0,
|
|
65
|
-
fontSize: 18
|
|
66
|
-
},
|
|
67
|
-
iconImg: { width: '100%', height: '100%', objectFit: 'contain', padding: 5 },
|
|
68
|
-
info: { flex: 1, minWidth: 0 },
|
|
69
|
-
name: { fontSize: 13, fontWeight: 600, color: '#333' },
|
|
70
|
-
url: { fontSize: 10, color: '#999', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
71
|
-
toggle: { flexShrink: 0 },
|
|
72
|
-
checkbox: { width: 16, height: 16, cursor: 'pointer', accentColor: '#3b82f6' },
|
|
73
|
-
preview: {
|
|
74
|
-
display: 'flex',
|
|
75
|
-
alignItems: 'flex-end',
|
|
76
|
-
justifyContent: 'center',
|
|
77
|
-
gap: 6,
|
|
78
|
-
padding: '12px 18px',
|
|
79
|
-
background: 'rgba(30,30,32,0.85)',
|
|
80
|
-
border: '1px solid rgba(255,255,255,0.18)',
|
|
81
|
-
borderRadius: 18,
|
|
82
|
-
margin: '16px auto',
|
|
83
|
-
width: 'fit-content'
|
|
84
|
-
},
|
|
85
|
-
actions: { display: 'flex', gap: 10, alignItems: 'center', marginTop: 16 },
|
|
86
|
-
empty: { textAlign: 'center', padding: '30px 16px', color: '#999', fontSize: 13 },
|
|
87
|
-
fieldRow: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 },
|
|
88
|
-
label: { fontSize: 13, fontWeight: 500, color: '#555', width: 160, flexShrink: 0 },
|
|
89
|
-
select: {
|
|
90
|
-
padding: '6px 10px',
|
|
91
|
-
borderRadius: 6,
|
|
92
|
-
border: '1px solid #ccc',
|
|
93
|
-
fontSize: 13,
|
|
94
|
-
background: '#fff',
|
|
95
|
-
color: '#333'
|
|
96
|
-
},
|
|
97
|
-
input: {
|
|
98
|
-
padding: '6px 10px',
|
|
99
|
-
borderRadius: 6,
|
|
100
|
-
border: '1px solid #ccc',
|
|
101
|
-
fontSize: 13,
|
|
102
|
-
width: 80,
|
|
103
|
-
background: '#fff',
|
|
104
|
-
color: '#333'
|
|
105
|
-
},
|
|
106
|
-
checkboxField: { width: 16, height: 16, accentColor: '#3b82f6' },
|
|
107
|
-
hint: { fontSize: 11, color: '#aaa', marginLeft: 8 }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function IconPreview({ icon, label, color, size }) {
|
|
111
|
-
const sz = size || 36
|
|
112
|
-
const style = {
|
|
113
|
-
...S.iconBox,
|
|
114
|
-
width: sz,
|
|
115
|
-
height: sz,
|
|
116
|
-
borderRadius: sz * 0.22,
|
|
117
|
-
...(color ? { background: color } : {})
|
|
118
|
-
}
|
|
119
|
-
const iconVal = icon || ''
|
|
120
|
-
if (iconVal.startsWith('/') || iconVal.startsWith('http')) {
|
|
121
|
-
return (
|
|
122
|
-
<div style={style}>
|
|
123
|
-
<img
|
|
124
|
-
src={iconVal}
|
|
125
|
-
alt={label}
|
|
126
|
-
style={{ ...S.iconImg, padding: sz * 0.12 }}
|
|
127
|
-
onError={(e) => {
|
|
128
|
-
e.target.style.display = 'none'
|
|
129
|
-
e.target.parentNode.textContent = (label || '??').slice(0, 2).toUpperCase()
|
|
130
|
-
}}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
return <div style={style}>{iconVal || (label || '??').slice(0, 2).toUpperCase()}</div>
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function SelectField({ label, value, options, onChange, hint }) {
|
|
139
|
-
return (
|
|
140
|
-
<div style={S.fieldRow}>
|
|
141
|
-
<span style={S.label}>{label}</span>
|
|
142
|
-
<select style={S.select} value={value} onChange={(e) => onChange(e.target.value)}>
|
|
143
|
-
{options.map((o) => (
|
|
144
|
-
<option key={o.value} value={o.value}>
|
|
145
|
-
{o.label}
|
|
146
|
-
</option>
|
|
147
|
-
))}
|
|
148
|
-
</select>
|
|
149
|
-
{hint && <span style={S.hint}>{hint}</span>}
|
|
150
|
-
</div>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function NumberField({ label, value, onChange, hint }) {
|
|
155
|
-
return (
|
|
156
|
-
<div style={S.fieldRow}>
|
|
157
|
-
<span style={S.label}>{label}</span>
|
|
158
|
-
<input style={S.input} type="number" value={value} onChange={(e) => onChange(Number(e.target.value))} />
|
|
159
|
-
{hint && <span style={S.hint}>{hint}</span>}
|
|
160
|
-
</div>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function CheckboxField({ label, checked, onChange, hint }) {
|
|
165
|
-
return (
|
|
166
|
-
<div style={S.fieldRow}>
|
|
167
|
-
<span style={S.label}>{label}</span>
|
|
168
|
-
<input type="checkbox" style={S.checkboxField} checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
|
169
|
-
{hint && <span style={S.hint}>{hint}</span>}
|
|
170
|
-
</div>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export default function PluginConfigurationPanel({ configuration, save }) {
|
|
175
|
-
const cfg = configuration || {}
|
|
176
|
-
const [apps, setApps] = useState(() => cfg.apps || [])
|
|
177
|
-
const [position, setPosition] = useState(cfg.position || 'bottom')
|
|
178
|
-
const [iframeMode, setIframeMode] = useState(cfg.iframeMode || 'keep-alive')
|
|
179
|
-
const [iconSize, setIconSize] = useState(cfg.iconSize || 56)
|
|
180
|
-
const [magnification, setMagnification] = useState(cfg.magnification !== false)
|
|
181
|
-
const [magnificationScale, setMagnificationScale] = useState(cfg.magnificationScale || 1.7)
|
|
182
|
-
const [showNightModeButton, setShowNightModeButton] = useState(cfg.showNightModeButton || false)
|
|
183
|
-
const [showFullscreenButton, setShowFullscreenButton] = useState(cfg.showFullscreenButton || false)
|
|
184
|
-
const [showExitButton, setShowExitButton] = useState(cfg.showExitButton || false)
|
|
185
|
-
const [tourDismissed, setTourDismissed] = useState(cfg.tourDismissed || false)
|
|
186
|
-
|
|
187
|
-
const [status, setStatus] = useState('')
|
|
188
|
-
const [statusError, setStatusError] = useState(false)
|
|
189
|
-
const [discovering, setDiscovering] = useState(false)
|
|
190
|
-
const [saving, setSaving] = useState(false)
|
|
191
|
-
const [dragIdx, setDragIdx] = useState(null)
|
|
192
|
-
const [overIdx, setOverIdx] = useState(null)
|
|
193
|
-
|
|
194
|
-
const appsRef = useRef(apps)
|
|
195
|
-
appsRef.current = apps
|
|
196
|
-
|
|
197
|
-
const statusTimeoutRef = useRef(null)
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
return () => {
|
|
200
|
-
if (statusTimeoutRef.current) clearTimeout(statusTimeoutRef.current)
|
|
201
|
-
}
|
|
202
|
-
}, [])
|
|
203
|
-
|
|
204
|
-
const buildConfig = useCallback(
|
|
205
|
-
(appsList) => ({
|
|
206
|
-
position,
|
|
207
|
-
iframeMode,
|
|
208
|
-
iconSize,
|
|
209
|
-
magnification,
|
|
210
|
-
magnificationScale,
|
|
211
|
-
showNightModeButton,
|
|
212
|
-
showFullscreenButton,
|
|
213
|
-
showExitButton,
|
|
214
|
-
tourDismissed,
|
|
215
|
-
apps: appsList
|
|
216
|
-
}),
|
|
217
|
-
[
|
|
218
|
-
position,
|
|
219
|
-
iframeMode,
|
|
220
|
-
iconSize,
|
|
221
|
-
magnification,
|
|
222
|
-
magnificationScale,
|
|
223
|
-
showNightModeButton,
|
|
224
|
-
showFullscreenButton,
|
|
225
|
-
showExitButton,
|
|
226
|
-
tourDismissed
|
|
227
|
-
]
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
const doSave = useCallback(async () => {
|
|
231
|
-
if (statusTimeoutRef.current) clearTimeout(statusTimeoutRef.current)
|
|
232
|
-
setSaving(true)
|
|
233
|
-
setStatus('Saving\u2026')
|
|
234
|
-
setStatusError(false)
|
|
235
|
-
try {
|
|
236
|
-
const result = save(buildConfig(apps))
|
|
237
|
-
if (result && typeof result.then === 'function') {
|
|
238
|
-
await result
|
|
239
|
-
}
|
|
240
|
-
setStatus('Configuration saved \u2014 plugin will restart')
|
|
241
|
-
setStatusError(false)
|
|
242
|
-
statusTimeoutRef.current = setTimeout(() => setStatus(''), 5000)
|
|
243
|
-
} catch (e) {
|
|
244
|
-
setStatus('Save failed: ' + (e.message || e))
|
|
245
|
-
setStatusError(true)
|
|
246
|
-
} finally {
|
|
247
|
-
setSaving(false)
|
|
248
|
-
}
|
|
249
|
-
}, [apps, buildConfig, save])
|
|
250
|
-
|
|
251
|
-
const discover = async () => {
|
|
252
|
-
setDiscovering(true)
|
|
253
|
-
setStatus('')
|
|
254
|
-
setStatusError(false)
|
|
255
|
-
try {
|
|
256
|
-
const res = await fetch('/skServer/webapps')
|
|
257
|
-
const allWebapps = await res.json()
|
|
258
|
-
const webapps = allWebapps
|
|
259
|
-
.filter((w) => w.name !== '@signalk/app-dock')
|
|
260
|
-
.map((w) => {
|
|
261
|
-
if (w.name === '@signalk/server-admin-ui') {
|
|
262
|
-
return {
|
|
263
|
-
name: w.name,
|
|
264
|
-
label: 'Settings',
|
|
265
|
-
url: '/admin/',
|
|
266
|
-
icon: '/@signalk/app-dock/icon-settings.svg',
|
|
267
|
-
isAdmin: true
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
name: w.name,
|
|
272
|
-
label: w.signalk?.displayName || w.name,
|
|
273
|
-
url: `/${w.name}/`,
|
|
274
|
-
icon: w.signalk?.appIcon ? `/${w.name}/${w.signalk.appIcon}` : null
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
if (webapps.length === 0) {
|
|
279
|
-
setStatus('No webapps found. Is the server fully started?')
|
|
280
|
-
setStatusError(true)
|
|
281
|
-
setDiscovering(false)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const current = appsRef.current
|
|
286
|
-
const existingUrls = new Set(current.map((a) => a.url))
|
|
287
|
-
let added = 0
|
|
288
|
-
const merged = [...current]
|
|
289
|
-
for (const w of webapps) {
|
|
290
|
-
if (!existingUrls.has(w.url)) {
|
|
291
|
-
merged.push({
|
|
292
|
-
enabled: true,
|
|
293
|
-
autostart: false,
|
|
294
|
-
url: w.url,
|
|
295
|
-
label: w.label || '',
|
|
296
|
-
icon: w.icon || '',
|
|
297
|
-
color: w.isAdmin ? '#78788c' : ''
|
|
298
|
-
})
|
|
299
|
-
added++
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
setApps(merged)
|
|
304
|
-
setStatus(
|
|
305
|
-
added > 0
|
|
306
|
-
? `Found ${webapps.length} webapps, added ${added} new.`
|
|
307
|
-
: `All ${webapps.length} webapps already in list.`
|
|
308
|
-
)
|
|
309
|
-
setStatusError(false)
|
|
310
|
-
} catch (e) {
|
|
311
|
-
setStatus('Discovery failed: ' + e.message)
|
|
312
|
-
setStatusError(true)
|
|
313
|
-
}
|
|
314
|
-
setDiscovering(false)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const toggleApp = (i) => setApps(apps.map((a, j) => (j === i ? { ...a, enabled: !a.enabled } : a)))
|
|
318
|
-
const toggleAutostart = (i) =>
|
|
319
|
-
setApps(apps.map((a, j) => (j === i ? { ...a, autostart: !a.autostart } : { ...a, autostart: false })))
|
|
320
|
-
const removeApp = (i) => setApps(apps.filter((_, j) => j !== i))
|
|
321
|
-
|
|
322
|
-
const onDragStart = (i) => setDragIdx(i)
|
|
323
|
-
const onDragOver = (e, i) => {
|
|
324
|
-
e.preventDefault()
|
|
325
|
-
setOverIdx(i)
|
|
326
|
-
}
|
|
327
|
-
const onDragLeave = () => setOverIdx(null)
|
|
328
|
-
const onDrop = (e, dropIdx) => {
|
|
329
|
-
e.preventDefault()
|
|
330
|
-
setOverIdx(null)
|
|
331
|
-
if (dragIdx !== null && dragIdx !== dropIdx) {
|
|
332
|
-
const next = [...apps]
|
|
333
|
-
const [moved] = next.splice(dragIdx, 1)
|
|
334
|
-
next.splice(dropIdx, 0, moved)
|
|
335
|
-
setApps(next)
|
|
336
|
-
}
|
|
337
|
-
setDragIdx(null)
|
|
338
|
-
}
|
|
339
|
-
const onDragEnd = () => {
|
|
340
|
-
setDragIdx(null)
|
|
341
|
-
setOverIdx(null)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const enabledApps = apps.filter((a) => a.enabled !== false)
|
|
345
|
-
|
|
346
|
-
return (
|
|
347
|
-
<div style={S.root}>
|
|
348
|
-
<div style={S.sectionTitle}>Dock Settings</div>
|
|
349
|
-
|
|
350
|
-
<SelectField
|
|
351
|
-
label="Dock position"
|
|
352
|
-
value={position}
|
|
353
|
-
onChange={setPosition}
|
|
354
|
-
options={[
|
|
355
|
-
{ value: 'bottom', label: 'Bottom' },
|
|
356
|
-
{ value: 'top', label: 'Top' },
|
|
357
|
-
{ value: 'left', label: 'Left' },
|
|
358
|
-
{ value: 'right', label: 'Right' }
|
|
359
|
-
]}
|
|
360
|
-
/>
|
|
361
|
-
|
|
362
|
-
<SelectField
|
|
363
|
-
label="iFrame lifecycle"
|
|
364
|
-
value={iframeMode}
|
|
365
|
-
onChange={setIframeMode}
|
|
366
|
-
options={[
|
|
367
|
-
{ value: 'keep-alive', label: 'Keep-alive (faster switching, more RAM)' },
|
|
368
|
-
{ value: 'destroy', label: 'Destroy (reload each time, less RAM)' }
|
|
369
|
-
]}
|
|
370
|
-
/>
|
|
371
|
-
|
|
372
|
-
<NumberField label="Icon size" value={iconSize} onChange={setIconSize} hint="px" />
|
|
373
|
-
|
|
374
|
-
<CheckboxField label="Magnification effect" checked={magnification} onChange={setMagnification} />
|
|
375
|
-
|
|
376
|
-
{magnification && (
|
|
377
|
-
<NumberField
|
|
378
|
-
label="Magnification scale"
|
|
379
|
-
value={magnificationScale}
|
|
380
|
-
onChange={setMagnificationScale}
|
|
381
|
-
hint="1.0 - 2.5"
|
|
382
|
-
/>
|
|
383
|
-
)}
|
|
384
|
-
|
|
385
|
-
<div style={S.sectionTitle}>Utility Buttons</div>
|
|
386
|
-
|
|
387
|
-
<CheckboxField
|
|
388
|
-
label="Night/Day mode toggle"
|
|
389
|
-
checked={showNightModeButton}
|
|
390
|
-
onChange={setShowNightModeButton}
|
|
391
|
-
hint="Sun/moon button to toggle environment.mode"
|
|
392
|
-
/>
|
|
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
|
-
|
|
401
|
-
<CheckboxField
|
|
402
|
-
label="Exit button"
|
|
403
|
-
checked={showExitButton}
|
|
404
|
-
onChange={setShowExitButton}
|
|
405
|
-
hint="X button to return to Signal K admin UI"
|
|
406
|
-
/>
|
|
407
|
-
|
|
408
|
-
<div style={S.sectionTitle}>Webapps</div>
|
|
409
|
-
<button
|
|
410
|
-
style={{ ...S.btn, ...S.btnPrimary, ...(discovering ? { opacity: 0.6 } : {}) }}
|
|
411
|
-
onClick={discover}
|
|
412
|
-
disabled={discovering}
|
|
413
|
-
>
|
|
414
|
-
{discovering ? 'Discovering\u2026' : 'Discover Installed Webapps'}
|
|
415
|
-
</button>
|
|
416
|
-
{status && <div style={{ ...S.status, color: statusError ? '#ef4444' : '#10b981' }}>{status}</div>}
|
|
417
|
-
|
|
418
|
-
<div style={S.sectionTitle}>Dock Apps (drag to reorder)</div>
|
|
419
|
-
{apps.length === 0 ? (
|
|
420
|
-
<div style={S.empty}>No apps yet. Click Discover above.</div>
|
|
421
|
-
) : (
|
|
422
|
-
<div>
|
|
423
|
-
{apps.map((app, i) => (
|
|
424
|
-
<div
|
|
425
|
-
key={app.url + i}
|
|
426
|
-
draggable
|
|
427
|
-
onDragStart={() => onDragStart(i)}
|
|
428
|
-
onDragOver={(e) => onDragOver(e, i)}
|
|
429
|
-
onDragLeave={onDragLeave}
|
|
430
|
-
onDrop={(e) => onDrop(e, i)}
|
|
431
|
-
onDragEnd={onDragEnd}
|
|
432
|
-
style={{
|
|
433
|
-
...S.item,
|
|
434
|
-
...(app.enabled === false ? S.itemDisabled : {}),
|
|
435
|
-
...(overIdx === i ? S.dragOver : {}),
|
|
436
|
-
...(dragIdx === i ? { opacity: 0.4 } : {})
|
|
437
|
-
}}
|
|
438
|
-
>
|
|
439
|
-
<span style={S.handle}>{'\u2261'}</span>
|
|
440
|
-
<IconPreview icon={app.icon} label={app.label || app.url} color={app.color} />
|
|
441
|
-
<div style={S.info}>
|
|
442
|
-
<div style={S.name}>{app.label || app.url}</div>
|
|
443
|
-
<div style={S.url}>{app.url}</div>
|
|
444
|
-
</div>
|
|
445
|
-
<div style={S.toggle} title="Enabled">
|
|
446
|
-
<input
|
|
447
|
-
type="checkbox"
|
|
448
|
-
checked={app.enabled !== false}
|
|
449
|
-
onChange={() => toggleApp(i)}
|
|
450
|
-
style={S.checkbox}
|
|
451
|
-
/>
|
|
452
|
-
</div>
|
|
453
|
-
<div
|
|
454
|
-
style={{
|
|
455
|
-
...S.toggle,
|
|
456
|
-
cursor: 'pointer',
|
|
457
|
-
fontSize: 16,
|
|
458
|
-
opacity: app.autostart ? 1 : 0.25
|
|
459
|
-
}}
|
|
460
|
-
onClick={() => toggleAutostart(i)}
|
|
461
|
-
title={app.autostart ? 'Autostart: ON (click to disable)' : 'Click to set as autostart app'}
|
|
462
|
-
>
|
|
463
|
-
{'\u25b6'}
|
|
464
|
-
</div>
|
|
465
|
-
<button style={{ ...S.btn, ...S.btnDanger }} onClick={() => removeApp(i)}>
|
|
466
|
-
Remove
|
|
467
|
-
</button>
|
|
468
|
-
</div>
|
|
469
|
-
))}
|
|
470
|
-
</div>
|
|
471
|
-
)}
|
|
472
|
-
|
|
473
|
-
{(enabledApps.length > 0 || showNightModeButton || showFullscreenButton || showExitButton) && (
|
|
474
|
-
<>
|
|
475
|
-
<div style={S.sectionTitle}>Preview</div>
|
|
476
|
-
<div style={S.preview}>
|
|
477
|
-
{showNightModeButton && (
|
|
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' }} />
|
|
510
|
-
)}
|
|
511
|
-
{enabledApps.map((app, i) => (
|
|
512
|
-
<IconPreview key={i} icon={app.icon} label={app.label || app.url} color={app.color} size={40} />
|
|
513
|
-
))}
|
|
514
|
-
{showExitButton && (
|
|
515
|
-
<>
|
|
516
|
-
<div style={{ width: 1, height: 24, background: 'rgba(255,255,255,0.15)', alignSelf: 'center' }} />
|
|
517
|
-
<div
|
|
518
|
-
style={{
|
|
519
|
-
...S.iconBox,
|
|
520
|
-
width: 40,
|
|
521
|
-
height: 40,
|
|
522
|
-
background: 'rgba(255,255,255,0.08)',
|
|
523
|
-
border: '1px solid rgba(255,255,255,0.12)',
|
|
524
|
-
fontSize: 16,
|
|
525
|
-
borderRadius: 10,
|
|
526
|
-
color: '#fff'
|
|
527
|
-
}}
|
|
528
|
-
>
|
|
529
|
-
{'\u2715'}
|
|
530
|
-
</div>
|
|
531
|
-
</>
|
|
532
|
-
)}
|
|
533
|
-
</div>
|
|
534
|
-
</>
|
|
535
|
-
)}
|
|
536
|
-
|
|
537
|
-
<div style={S.actions}>
|
|
538
|
-
<button
|
|
539
|
-
style={{ ...S.btn, ...S.btnSave, ...(saving ? { opacity: 0.6 } : {}) }}
|
|
540
|
-
onClick={doSave}
|
|
541
|
-
disabled={saving}
|
|
542
|
-
>
|
|
543
|
-
{saving ? 'Saving\u2026' : 'Save Configuration'}
|
|
544
|
-
</button>
|
|
545
|
-
{status && <div style={{ ...S.status, color: statusError ? '#ef4444' : '#10b981' }}>{status}</div>}
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
548
|
-
)
|
|
549
|
-
}
|
package/src/configpanel/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// Vite entry point — the actual component is loaded via Module Federation
|