dsh-capyreporter 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.
package/lib/client.js ADDED
@@ -0,0 +1,567 @@
1
+ // dsh-capyreporter — client half (browser). Registered with the DSH module loader.
2
+ // Talks to the host half over webServer routes using plain fetch.
3
+
4
+ function makeFactory() {
5
+ return (require) => {
6
+ const React = require('react')
7
+ const { useState, useEffect } = React
8
+
9
+ const name = 'capyreporter'
10
+ const inject = ['slots']
11
+ const ROUTE = '/dsh-capyreporter'
12
+
13
+ const SETTINGS_CSS = [
14
+ '.dsh-pet-section { display:flex; flex-direction:column; gap:16px; font-size:13px; }',
15
+ '.dsh-pet-row { display:flex; align-items:center; justify-content:space-between; gap:14px; }',
16
+ '.dsh-pet-row-main { display:flex; flex-direction:column; gap:3px; min-width:0; }',
17
+ '.dsh-pet-title { font-weight:600; }',
18
+ '.dsh-pet-hint { opacity:.65; font-size:12px; line-height:1.5; }',
19
+ '.dsh-pet-toggle { position:relative; flex:0 0 auto; width:42px; height:23px; border-radius:12px; background:#88888855; border:0; cursor:pointer; padding:0; transition:background .15s; }',
20
+ '.dsh-pet-toggle[data-on="1"] { background:#4f8cff; }',
21
+ '.dsh-pet-toggle::after { content:""; position:absolute; top:3px; left:3px; width:17px; height:17px; border-radius:50%; background:#fff; transition:left .15s; box-shadow:0 1px 2px #0004; }',
22
+ '.dsh-pet-toggle[data-on="1"]::after { left:22px; }',
23
+ '.dsh-pet-preview { width:110px; height:110px; object-fit:contain; border-radius:10px; background:#88888822; border:1px solid #88888844; flex:0 0 auto; }',
24
+ '.dsh-pet-btn { border:1px solid #88888866; background:transparent; color:inherit; border-radius:8px; padding:5px 12px; cursor:pointer; font-size:12.5px; }',
25
+ '.dsh-pet-btn:hover { background:#88888833; }',
26
+ '.dsh-pet-btn-danger { border-color:#e5484d88; color:#e5484d; }',
27
+ '.dsh-pet-btn-danger:hover { background:#e5484d22; }',
28
+ '.dsh-pet-msg { font-size:12px; opacity:.8; line-height:1.5; }',
29
+ ].join('\n')
30
+
31
+ const PET_CSS = [
32
+ '.dsh-pet-float { position:fixed; z-index:2147483000; pointer-events:auto; user-select:none; -webkit-user-select:none; }',
33
+ '.dsh-pet-img { display:block; width:132px; height:auto; pointer-events:auto; cursor:grab; filter:drop-shadow(0 6px 16px #0006); transform-origin:center bottom; transition:transform .12s ease-out; }',
34
+ '.dsh-pet-img.dragging { cursor:grabbing; }',
35
+ '.dsh-pet-img.bounce { transform:scale(1.12); }',
36
+ '.dsh-pet-bubble { position:absolute; bottom:100%; left:50%; transform:translateX(-50%); margin-bottom:8px; background:#fdfdf7; color:#22242c; border-radius:14px; padding:8px 12px; font-size:12.5px; line-height:1.45; box-shadow:0 4px 14px #0007; max-width:900px; min-width:90px; cursor:pointer; z-index:5; flex-direction:column; gap:3px; display:flex; box-sizing:border-box; }',
37
+ '.dsh-pet-bubble::after { content:""; position:absolute; bottom:-8px; left:50%; transform:translateX(-50%); border:8px solid transparent; border-top-color:#fdfdf7; border-bottom:0; }',
38
+ '.dsh-pet-bubble-head { font-weight:600; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:break-word; }',
39
+ '.dsh-pet-bubble-detail { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:break-word; opacity:.85; }',
40
+ '.dsh-pet-bubble .dsh-pet-hint { white-space:nowrap; }',
41
+ '.dsh-pet-bubble-log { max-height:150px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; font-size:12px; line-height:1.4; scrollbar-width:thin; }',
42
+ '.dsh-pet-bubble-log::-webkit-scrollbar { width:6px; }',
43
+ '.dsh-pet-bubble-log::-webkit-scrollbar-thumb { background:#00000044; border-radius:3px; }',
44
+ '.dsh-pet-bubble-line { overflow-wrap:break-word; white-space:normal; }',
45
+ '.dsh-pet-hint { font-size:10.5px; opacity:.55; margin-top:3px; }',
46
+ '.dsh-pet-menu { position:absolute; bottom:0; left:100%; margin-left:8px; flex-direction:column; background:#23262f; color:#e8eaf0; border:1px solid #ffffff22; border-radius:10px; overflow:hidden; box-shadow:0 8px 24px #0009; z-index:9; }',
47
+ '.dsh-pet-menu-item { appearance:none; border:0; background:transparent; color:#e8eaf0; padding:8px 14px; font-size:12px; text-align:left; cursor:pointer; font-family:inherit; white-space:nowrap; }',
48
+ '.dsh-pet-menu-item:hover { background:#ffffff14; }',
49
+ ].join('\n')
50
+
51
+ function ensureCss() {
52
+ try {
53
+ if (typeof document === 'undefined') return
54
+ if (document.querySelector('style[data-plugin-css="dsh-capyreporter"]')) return
55
+ const tag = document.createElement('style')
56
+ tag.dataset.pluginCss = 'dsh-capyreporter'
57
+ tag.textContent = SETTINGS_CSS + '\n' + PET_CSS
58
+ document.head.appendChild(tag)
59
+ } catch (e) {}
60
+ }
61
+
62
+ async function getJSON(path, opts) {
63
+ const r = await fetch(ROUTE + path, opts)
64
+ return r.json()
65
+ }
66
+ async function postJSON(path, body) {
67
+ return getJSON(path, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: JSON.stringify(body || {}),
71
+ })
72
+ }
73
+
74
+ function bubbleModel(act) {
75
+ if (!act) return null
76
+ const done = act.completed
77
+ if (done) return { mode: 'done', at: done.at, head: '\u2705 Task complete \u2014 ' + (done.title || 'Task'), lines: [], detail: null, hint: 'click to expand \u00B7 double-click to dismiss' }
78
+ const n = act.runningCount | 0
79
+ if (n > 0) {
80
+ const first = (act.running && act.running[0]) || {}
81
+ const head = n > 1 ? '\u23F3 ' + n + ' tasks running \u2014 ' + (first.title || 'Session') : '\u23F3 Working: ' + (first.title || 'Session')
82
+ return { mode: 'working', at: null, head: head, lines: first.lines || [], detail: (first.preview || '').slice(0, 260) || null, hint: 'click to expand \u00B7 double-click to hide' }
83
+ }
84
+ return null
85
+ }
86
+
87
+ // Content signature of the activity feed: the bubble auto-(re)appears whenever
88
+ // the signature changes (new preview, task count change, completion) and stays
89
+ // hidden after a click until then.
90
+ function activitySig(act) {
91
+ if (!act) return 'none'
92
+ if (act.completed) return 'done:' + act.completed.at
93
+ const n = act.runningCount | 0
94
+ if (n > 0) {
95
+ const first = (act.running && act.running[0]) || {}
96
+ return 'work:' + n + ':' + (first.title || '') + ':' + (first.preview || '')
97
+ }
98
+ return 'none'
99
+ }
100
+
101
+ function notifyDone(comp, lastAtRef) {
102
+ try {
103
+ if (lastAtRef.at === comp.at) return
104
+ if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return
105
+ lastAtRef.at = comp.at
106
+ new Notification('CapyReporter', { body: '\u2705 Task complete \u2014 ' + (comp.title || 'Task'), tag: 'dsh-capyreporter-complete' })
107
+ } catch (e) {}
108
+ }
109
+
110
+ function PetOverlay() {
111
+ const activity = React.useState(null)
112
+ const enabled = React.useState(true)
113
+ const hasCustom = React.useState(false)
114
+ const desktop = React.useState(false)
115
+ const menu = React.useState(false)
116
+ const bounce = React.useState(false)
117
+ const pos = React.useState({
118
+ left: 24,
119
+ top: typeof window !== 'undefined' ? Math.max(120, window.innerHeight - 420) : 220,
120
+ })
121
+ const drag = React.useState({ s: null })
122
+ const imgToken = React.useState(0)
123
+ const setActivity = activity[1]
124
+ const setEnabled = enabled[1]
125
+ const setHasCustom = hasCustom[1]
126
+ const setDesktop = desktop[1]
127
+ const setMenu = menu[1]
128
+ const setBounce = bounce[1]
129
+ const setPos = pos[1]
130
+ const dragState = drag[0]
131
+ const setImgToken = imgToken[1]
132
+ const lastNotified = React.useState({ at: null }) // plain object ref; stable across renders
133
+ const customRef = React.useState({ v: null }) // stable object ref for hasCustom change detection
134
+ const suppressed = React.useState({ sig: null }) // stable ref: bubble signature hidden by a double-click
135
+ const expanded = React.useState(false) // single-click expands the step log
136
+ const setExpanded = expanded[1]
137
+ const scale = React.useState(1) // pet size multiplier, owned by the host config
138
+ const setScale = scale[1]
139
+ const scaleRef = React.useRef(1)
140
+ scaleRef.current = scale[0]
141
+ const wheelBound = React.useRef(false)
142
+ const bindImg = React.useCallback((node) => {
143
+ if (!node || wheelBound.current) return
144
+ wheelBound.current = true
145
+ // Ctrl + wheel over the pet resizes her. Non-passive listener so the
146
+ // browser zoom gesture can be suppressed; the size is stored host-side
147
+ // and shared with the desktop window and the Settings slider.
148
+ node.addEventListener('wheel', (e) => {
149
+ if (!e.ctrlKey && !e.metaKey) return
150
+ e.preventDefault()
151
+ const next = Math.min(2.5, Math.max(0.5, Math.round((scaleRef.current + (e.deltaY < 0 ? 0.1 : -0.1)) * 10) / 10))
152
+ if (next === scaleRef.current) return
153
+ scaleRef.current = next
154
+ setScale(next)
155
+ postJSON('/scale', { scale: next }).catch(() => {})
156
+ }, { passive: false })
157
+ }, [])
158
+
159
+ React.useEffect(() => {
160
+ let alive = true
161
+ const poll = async () => {
162
+ try {
163
+ const a = await getJSON('/activity')
164
+ if (!alive) return
165
+ setActivity(a)
166
+ if (a && a.completed) notifyDone(a.completed, lastNotified[0])
167
+ } catch (e) {}
168
+ try {
169
+ const s = await getJSON('/state')
170
+ if (!alive) return
171
+ setEnabled(s.enabled)
172
+ setHasCustom(s.hasCustom)
173
+ setDesktop(!!s.desktop)
174
+ if (typeof s.scale === 'number') setScale(s.scale)
175
+ if (customRef[0].v !== s.hasCustom) {
176
+ customRef[0].v = s.hasCustom
177
+ setImgToken((x) => x + 1)
178
+ }
179
+ } catch (e) {}
180
+ }
181
+ poll()
182
+ const iv = setInterval(poll, 1200)
183
+ return () => {
184
+ alive = false
185
+ clearInterval(iv)
186
+ }
187
+ }, [])
188
+
189
+ // Hide the in-page overlay while the always-on-top desktop window is running,
190
+ // so there's only one pet. Falls back to the in-page pet if the desktop window isn't up.
191
+ if (!enabled[0] || desktop[0]) return null
192
+ const sig = activitySig(activity[0])
193
+ const raw = bubbleModel(activity[0])
194
+ const bubble = raw && sig !== suppressed[0].sig ? raw : null
195
+
196
+ const dismissDone = () => {
197
+ const at = activity[0] && activity[0].completed ? activity[0].completed.at : null
198
+ if (at) {
199
+ try {
200
+ postJSON('/dismiss', { at })
201
+ } catch (e) {}
202
+ setActivity(Object.assign({}, activity[0], { completed: null }))
203
+ }
204
+ }
205
+ const onLeftClick = () => {
206
+ if (activity[0] && activity[0].completed) {
207
+ dismissDone()
208
+ return
209
+ }
210
+ setBounce(true)
211
+ setTimeout(() => setBounce(false), 220)
212
+ }
213
+ const turnOff = async () => {
214
+ try {
215
+ await postJSON('/enabled', { enabled: false })
216
+ } catch (e) {}
217
+ setEnabled(false)
218
+ }
219
+ const onPointerDown = (e) => {
220
+ if (e.button !== 0) return
221
+ dragState.s = { sx: e.clientX, sy: e.clientY, bx: pos[0].left, by: pos[0].top }
222
+ }
223
+ const onPointerMove = (e) => {
224
+ const d = dragState.s
225
+ if (!d) return
226
+ const w = typeof window !== 'undefined' ? window.innerWidth : 800
227
+ const h = typeof window !== 'undefined' ? window.innerHeight : 600
228
+ const clamp = (v, max) => Math.max(0, Math.min(v, max))
229
+ const m = scaleRef.current
230
+ setPos({ left: clamp(d.bx + (e.clientX - d.sx), w - Math.round(140 * m)), top: clamp(d.by + (e.clientY - d.sy), h - Math.round(160 * m)) })
231
+ }
232
+ const onPointerUp = () => {
233
+ dragState.s = null
234
+ }
235
+
236
+ const menuItems = []
237
+ if (suppressed[0].sig !== null) menuItems.push(['Restore bubble', () => { suppressed[0].sig = null; setExpanded(false) }])
238
+ if (bubble && bubble.mode === 'done') menuItems.push(['Dismiss notification', dismissDone])
239
+ menuItems.push(['Hide pet', turnOff])
240
+
241
+ return React.createElement(
242
+ 'div',
243
+ { className: 'dsh-pet-float', style: { left: pos[0].left, top: pos[0].top } },
244
+ bubble
245
+ ? React.createElement(
246
+ 'div',
247
+ { className: 'dsh-pet-bubble', onClick: () => setExpanded(!expanded[0]), onDoubleClick: () => {
248
+ suppressed[0].sig = sig
249
+ if (raw.mode === 'done') dismissDone()
250
+ setExpanded(false)
251
+ } },
252
+ React.createElement('div', { className: 'dsh-pet-bubble-head' }, bubble.head),
253
+ expanded[0] && bubble.lines.length
254
+ ? React.createElement('div', { className: 'dsh-pet-bubble-log' },
255
+ bubble.lines.map((l, i) => React.createElement('div', { className: 'dsh-pet-bubble-line', key: i }, l)),
256
+ )
257
+ : bubble.detail
258
+ ? React.createElement('div', { className: 'dsh-pet-bubble-detail' }, bubble.detail)
259
+ : null,
260
+ bubble.hint ? React.createElement('div', { className: 'dsh-pet-hint' }, bubble.hint) : null,
261
+ )
262
+ : null,
263
+ menu[0]
264
+ ? React.createElement(
265
+ 'div',
266
+ { className: 'dsh-pet-menu' },
267
+ menuItems.map((it, i) =>
268
+ React.createElement(
269
+ 'button',
270
+ {
271
+ className: 'dsh-pet-menu-item',
272
+ key: i,
273
+ onClick: () => {
274
+ setMenu(false)
275
+ it[1]()
276
+ },
277
+ },
278
+ it[0],
279
+ ),
280
+ ),
281
+ )
282
+ : null,
283
+ React.createElement('img', {
284
+ className: 'dsh-pet-img' + (bounce[0] ? ' bounce' : ''),
285
+ style: { width: Math.round(132 * scale[0]) + 'px' },
286
+ ref: bindImg,
287
+ src: ROUTE + '/image?v=' + imgToken[0],
288
+ draggable: false,
289
+ onPointerDown: onPointerDown,
290
+ onPointerMove: onPointerMove,
291
+ onPointerUp: onPointerUp,
292
+ onClick: onLeftClick,
293
+ onContextMenu: (e) => {
294
+ e.preventDefault()
295
+ setMenu(!menu[0])
296
+ },
297
+ }),
298
+ )
299
+ }
300
+
301
+ function PetSettings() {
302
+ const tick = React.useState(0)
303
+ const confirming = React.useState(false)
304
+ const msg = React.useState(null)
305
+ const enabled = React.useState(true)
306
+ const hasCustom = React.useState(false)
307
+ const desktopOn = React.useState(false)
308
+ const petScale = React.useState(1)
309
+ const setTick = tick[1]
310
+ const setConfirming = confirming[1]
311
+ const setMsg = msg[1]
312
+ const setEnabled = enabled[1]
313
+ const setHasCustom = hasCustom[1]
314
+ const setDesktopOn = desktopOn[1]
315
+ const setPetScale = petScale[1]
316
+
317
+ React.useEffect(() => {
318
+ let alive = true
319
+ const refresh = async () => {
320
+ try {
321
+ const s = await getJSON('/state')
322
+ if (!alive) return
323
+ setEnabled(s.enabled)
324
+ setHasCustom(s.hasCustom)
325
+ setDesktopOn(!!s.desktopEnabled)
326
+ if (typeof s.scale === 'number') setPetScale(s.scale)
327
+ setTick((x) => x + 1)
328
+ } catch (e) {}
329
+ }
330
+ refresh()
331
+ const iv = setInterval(refresh, 2500)
332
+ return () => {
333
+ alive = false
334
+ clearInterval(iv)
335
+ }
336
+ }, [])
337
+
338
+ const onToggle = async () => {
339
+ const next = !enabled[0]
340
+ setEnabled(next)
341
+ setTick((x) => x + 1)
342
+ try {
343
+ await postJSON('/enabled', { enabled: next })
344
+ } catch (e) {}
345
+ if (next) {
346
+ setMsg(null)
347
+ try {
348
+ if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission()
349
+ } catch (e) {}
350
+ }
351
+ }
352
+
353
+ const onDesktopToggle = async () => {
354
+ const next = !desktopOn[0]
355
+ setDesktopOn(next)
356
+ try {
357
+ await postJSON(next ? '/desktop/show' : '/desktop/hide', {})
358
+ } catch (e) {}
359
+ }
360
+
361
+ const onSizeSlider = (ev) => {
362
+ const next = Math.min(2.5, Math.max(0.5, Number(ev.target.value) / 100 || 1))
363
+ setPetScale(next)
364
+ postJSON('/scale', { scale: next }).catch(() => {})
365
+ }
366
+
367
+ const onUpload = (ev) => {
368
+ const f = ev.target.files && ev.target.files[0]
369
+ ev.target.value = ''
370
+ if (!f) return
371
+ const reader = new FileReader()
372
+ reader.onload = async () => {
373
+ try {
374
+ const r = await postJSON('/image', { dataUrl: String(reader.result) })
375
+ if (r && r.ok) {
376
+ setHasCustom(true)
377
+ setMsg(null)
378
+ } else {
379
+ setMsg((r && r.error) || 'Upload failed.')
380
+ }
381
+ setTick((x) => x + 1)
382
+ } catch (e) {
383
+ setMsg('Upload failed: ' + ((e && e.message) || e))
384
+ }
385
+ }
386
+ reader.readAsDataURL(f)
387
+ }
388
+
389
+ const onReset = async () => {
390
+ try {
391
+ await postJSON('/reset', {})
392
+ } catch (e) {}
393
+ setHasCustom(false)
394
+ setMsg(null)
395
+ setTick((x) => x + 1)
396
+ }
397
+
398
+ const onUninstall = async () => {
399
+ if (!confirming[0]) {
400
+ setConfirming(true)
401
+ return
402
+ }
403
+ setConfirming(false)
404
+ try {
405
+ await postJSON('/enabled', { enabled: false })
406
+ await postJSON('/reset', {})
407
+ } catch (e) {}
408
+ setEnabled(false)
409
+ setHasCustom(false)
410
+ setMsg('Pet hidden and its data cleared. To remove the plugin itself, ask the assistant to remove the \u201Cdesktop pet\u201D plugin.')
411
+ }
412
+
413
+ const children = []
414
+ children.push(
415
+ React.createElement(
416
+ 'div',
417
+ { className: 'dsh-pet-row', key: 'toggle' },
418
+ React.createElement(
419
+ 'div',
420
+ { className: 'dsh-pet-row-main' },
421
+ React.createElement('div', { className: 'dsh-pet-title' }, 'Floating desktop pet'),
422
+ React.createElement(
423
+ 'div',
424
+ { className: 'dsh-pet-hint' },
425
+ 'CapyReporter floats on this page over the interface. Drag her where you like; a speech bubble reports running work \u2014 click it to hide it until the next update or completion. \u201CTask complete\u201D stays until you click it. When you are away she sends a desktop notification when a task finishes.',
426
+ ),
427
+ ),
428
+ React.createElement('button', {
429
+ 'data-on': enabled[0] ? '1' : '0',
430
+ className: 'dsh-pet-toggle',
431
+ 'aria-label': enabled[0] ? 'Hide floating pet' : 'Show floating pet',
432
+ onClick: onToggle,
433
+ }),
434
+ ),
435
+ )
436
+
437
+ children.push(
438
+ React.createElement(
439
+ 'div',
440
+ { className: 'dsh-pet-row', key: 'desktop' },
441
+ React.createElement(
442
+ 'div',
443
+ { className: 'dsh-pet-row-main' },
444
+ React.createElement('div', { className: 'dsh-pet-title' }, 'Always-on-top desktop window'),
445
+ React.createElement(
446
+ 'div',
447
+ { className: 'dsh-pet-hint' },
448
+ 'Shows CapyReporter in a small transparent window that stays above other apps \u2014 even when this page or another app has focus. Turn off to keep her only on this page.',
449
+ ),
450
+ ),
451
+ React.createElement('button', {
452
+ 'data-on': desktopOn[0] ? '1' : '0',
453
+ className: 'dsh-pet-toggle',
454
+ 'aria-label': desktopOn[0] ? 'Hide desktop window' : 'Show desktop window',
455
+ onClick: onDesktopToggle,
456
+ }),
457
+ ),
458
+ )
459
+
460
+ children.push(
461
+ React.createElement(
462
+ 'div',
463
+ { className: 'dsh-pet-row', key: 'size' },
464
+ React.createElement(
465
+ 'div',
466
+ { className: 'dsh-pet-row-main' },
467
+ React.createElement('div', { className: 'dsh-pet-title' }, 'Pet size'),
468
+ React.createElement(
469
+ 'div',
470
+ { className: 'dsh-pet-hint' },
471
+ 'Drag the slider, or hold Ctrl and scroll over the pet (plain scroll works over the desktop window). The size applies to the on-page pet and the desktop window and survives restarts.',
472
+ ),
473
+ ),
474
+ React.createElement(
475
+ 'div',
476
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
477
+ React.createElement('input', {
478
+ type: 'range',
479
+ min: 50,
480
+ max: 250,
481
+ step: 5,
482
+ value: Math.round(petScale[0] * 100),
483
+ onChange: onSizeSlider,
484
+ style: { width: '150px' },
485
+ 'aria-label': 'Pet size percent',
486
+ }),
487
+ React.createElement('span', { style: { fontSize: '12px', opacity: 0.75, minWidth: '38px', textAlign: 'right' } }, Math.round(petScale[0] * 100) + '%'),
488
+ ),
489
+ ),
490
+ )
491
+
492
+ children.push(
493
+ React.createElement(
494
+ 'div',
495
+ { className: 'dsh-pet-row', key: 'image' },
496
+ React.createElement('img', { className: 'dsh-pet-preview', src: ROUTE + '/image?v=' + tick[0], alt: 'Current pet appearance' }),
497
+ React.createElement(
498
+ 'div',
499
+ { className: 'dsh-pet-row-main' },
500
+ React.createElement('div', { className: 'dsh-pet-title' }, 'Pet appearance'),
501
+ React.createElement('div', { className: 'dsh-pet-hint' }, 'Use a transparent PNG so she floats cleanly (under ~4 MB).'),
502
+ React.createElement(
503
+ 'div',
504
+ { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
505
+ React.createElement(
506
+ 'label',
507
+ { className: 'dsh-pet-btn', style: { display: 'inline-flex', alignItems: 'center' } },
508
+ 'Replace image\u2026',
509
+ React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: onUpload }),
510
+ ),
511
+ hasCustom[0] ? React.createElement('button', { className: 'dsh-pet-btn', onClick: onReset }, 'Reset to CapyReporter') : null,
512
+ ),
513
+ ),
514
+ ),
515
+ )
516
+
517
+ children.push(
518
+ React.createElement(
519
+ 'div',
520
+ { className: 'dsh-pet-row', key: 'uninstall', style: { alignItems: 'flex-start' } },
521
+ React.createElement(
522
+ 'div',
523
+ { className: 'dsh-pet-row-main' },
524
+ React.createElement('div', { className: 'dsh-pet-title' }, 'Uninstall'),
525
+ React.createElement(
526
+ 'div',
527
+ { className: 'dsh-pet-hint' },
528
+ confirming[0] ? 'Hide the pet and clear its custom image? Click again to confirm.' : 'Hides the pet and clears its data.',
529
+ ),
530
+ ),
531
+ React.createElement(
532
+ 'button',
533
+ { className: 'dsh-pet-btn dsh-pet-btn-danger', onClick: onUninstall },
534
+ confirming[0] ? 'Confirm uninstall' : 'Uninstall pet',
535
+ ),
536
+ ),
537
+ )
538
+
539
+ if (msg[0]) children.push(React.createElement('div', { className: 'dsh-pet-msg', key: 'msg' }, msg[0]))
540
+
541
+ return React.createElement('div', { className: 'dsh-pet-section' }, children)
542
+ }
543
+
544
+ function apply(ctx) {
545
+ ensureCss()
546
+ ctx.slots.inject('shell.overlay', function* () {
547
+ yield ctx.slots.register(
548
+ { name: 'shell.overlay', id: 'dsh-pet-overlay', order: 500 },
549
+ () => React.createElement(PetOverlay),
550
+ )
551
+ })
552
+ ctx.slots.inject('settings.section', function* () {
553
+ yield ctx.slots.register(
554
+ { name: 'settings.section', id: 'dsh-capyreporter', order: 90, label: '\uD83D\uDC3E CapyReporter' },
555
+ () => React.createElement(PetSettings),
556
+ )
557
+ })
558
+ }
559
+
560
+ return { name, inject, apply }
561
+ }
562
+ }
563
+
564
+ window.__ModuleLoader__.load({
565
+ id: 'dsh-capyreporter',
566
+ factory: makeFactory(),
567
+ })