@restregular/dsh-visual-browser 0.0.0-stage → 0.1.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/client.js ADDED
@@ -0,0 +1,835 @@
1
+ /**
2
+ * Client half of dsh-visual-browser.
3
+ *
4
+ * One sidebar entry + one main panel: the live page frame, a click-through
5
+ * surface, browser controls, and the replayable action timeline the Host keeps.
6
+ * The whole file is an IIFE because every client bundle shares one global
7
+ * lexical scope inside the page.
8
+ */
9
+ (function () {
10
+ 'use strict';
11
+
12
+ var MODULE_ID = '@restregular/dsh-visual-browser';
13
+ var API = '/visual-browser/api';
14
+ var NS = 'dsh-visual-browser';
15
+
16
+ var DICT = {
17
+ zh: {
18
+ 'panel.title': '浏览器',
19
+ 'state.running': '运行中',
20
+ 'state.starting': '启动中',
21
+ 'state.stopped': '未启动',
22
+ 'state.offline': '宿主未连接',
23
+ 'action.start': '启动',
24
+ 'action.stop': '关闭',
25
+ 'action.back': '后退',
26
+ 'action.forward': '前进',
27
+ 'action.reload': '刷新页面',
28
+ 'action.capture': '截取画面',
29
+ 'action.live': '实时画面',
30
+ 'action.open': '打开',
31
+ 'url.placeholder': '输入网址,例如 example.com',
32
+ 'hint.interactive': '点击画面可直接操作浏览器,滚轮可滚动页面',
33
+ 'hint.headless': '当前是无头浏览器:用户看不到窗口,画面只在这里',
34
+ 'empty.title': '浏览器还没启动',
35
+ 'empty.desc': '在上方输入网址打开,或者让 Agent 用 browser_open 打开一个页面;之后每一次操作都会在这里留下画面。',
36
+ 'timeline.title': '操作记录',
37
+ 'timeline.empty': '还没有操作。Agent 每次点击、输入之后都会在这里留一帧画面。',
38
+ 'timeline.latest': '回到最新',
39
+ 'timeline.replay': '回放这一帧',
40
+ 'error.offline': '连不上宿主路由(/visual-browser/api),请确认插件已启用且宿主仍在运行。',
41
+ 'kind.start': '启动',
42
+ 'kind.close': '关闭',
43
+ 'kind.open': '打开',
44
+ 'kind.click': '点击',
45
+ 'kind.type': '输入',
46
+ 'kind.key': '按键',
47
+ 'kind.hover': '悬停',
48
+ 'kind.scroll': '滚动',
49
+ 'kind.select': '选择',
50
+ 'kind.back': '后退',
51
+ 'kind.forward': '前进',
52
+ 'kind.reload': '刷新',
53
+ 'kind.wait': '等待',
54
+ 'kind.snapshot': '读取',
55
+ 'kind.screenshot': '截图',
56
+ 'kind.dialog': '对话框',
57
+ 'kind.user-click': '你点击',
58
+ 'kind.user-scroll': '你滚动',
59
+ 'kind.user-key': '你按键',
60
+ 'busy': '处理中…',
61
+ },
62
+ en: {
63
+ 'panel.title': 'Browser',
64
+ 'state.running': 'Running',
65
+ 'state.starting': 'Starting',
66
+ 'state.stopped': 'Stopped',
67
+ 'state.offline': 'Host offline',
68
+ 'action.start': 'Start',
69
+ 'action.stop': 'Stop',
70
+ 'action.back': 'Back',
71
+ 'action.forward': 'Forward',
72
+ 'action.reload': 'Reload page',
73
+ 'action.capture': 'Refresh frame',
74
+ 'action.live': 'Live view',
75
+ 'action.open': 'Open',
76
+ 'url.placeholder': 'Enter a URL, for example example.com',
77
+ 'hint.interactive': 'Click the picture to control the browser; the wheel scrolls the page',
78
+ 'hint.headless': 'Headless browser: there is no window to watch, the picture here is the browser',
79
+ 'empty.title': 'The browser is not running yet',
80
+ 'empty.desc': 'Open a URL above, or let the Agent call browser_open. Every action it takes leaves a frame here.',
81
+ 'timeline.title': 'Actions',
82
+ 'timeline.empty': 'No actions yet. Every click and keystroke the Agent makes leaves a frame here.',
83
+ 'timeline.latest': 'Back to latest',
84
+ 'timeline.replay': 'Replay this frame',
85
+ 'error.offline': 'Cannot reach the host route (/visual-browser/api). Check that the plugin is enabled and the host is running.',
86
+ 'kind.start': 'start',
87
+ 'kind.close': 'close',
88
+ 'kind.open': 'open',
89
+ 'kind.click': 'click',
90
+ 'kind.type': 'type',
91
+ 'kind.key': 'key',
92
+ 'kind.hover': 'hover',
93
+ 'kind.scroll': 'scroll',
94
+ 'kind.select': 'select',
95
+ 'kind.back': 'back',
96
+ 'kind.forward': 'forward',
97
+ 'kind.reload': 'reload',
98
+ 'kind.wait': 'wait',
99
+ 'kind.snapshot': 'read',
100
+ 'kind.screenshot': 'shot',
101
+ 'kind.dialog': 'dialog',
102
+ 'kind.user-click': 'you clicked',
103
+ 'kind.user-scroll': 'you scrolled',
104
+ 'kind.user-key': 'you pressed',
105
+ 'busy': 'Working…',
106
+ },
107
+ };
108
+
109
+ var CSS = [
110
+ '.dvb-page{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}',
111
+ '.dvb-head{flex:none;border-bottom:1px solid var(--dsw-alias-border-l1);padding:10px 14px 10px;display:flex;flex-direction:column;gap:8px}',
112
+ '.dvb-head-row{display:flex;align-items:center;gap:8px;min-width:0}',
113
+ '.dvb-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary)}',
114
+ '.dvb-dot[data-on="true"]{background:var(--dsw-alias-state-success-primary)}',
115
+ '.dvb-dot[data-error="true"]{background:var(--dsw-alias-state-error-primary)}',
116
+ '.dvb-title{font-weight:600;font-size:14px;white-space:nowrap}',
117
+ '.dvb-status{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
118
+ '.dvb-spacer{flex:1 1 auto;min-width:8px}',
119
+ '.dvb-tools{display:flex;align-items:center;gap:4px;flex:none}',
120
+ '.dvb-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:28px;min-width:28px;padding:0 8px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:12px}',
121
+ '.dvb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
122
+ '.dvb-btn:disabled{opacity:.45;cursor:default}',
123
+ '.dvb-btn[data-on="true"]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}',
124
+ '.dvb-btn[data-variant="primary"]{background:var(--dsw-alias-brand-primary);border-color:transparent;color:#fff}',
125
+ '.dvb-btn[data-variant="primary"]:hover:not(:disabled){background:var(--dsw-alias-brand-primary);filter:brightness(1.06);color:#fff}',
126
+ '.dvb-url{display:flex;align-items:center;gap:8px;min-width:0}',
127
+ '.dvb-input{flex:1 1 auto;min-width:0;height:28px;padding:0 10px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}',
128
+ '.dvb-input::placeholder{color:var(--dsw-alias-label-secondary)}',
129
+ '.dvb-input:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}',
130
+ '.dvb-stage{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px}',
131
+ '.dvb-frame{display:block;width:100%;height:auto;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);box-shadow:0 6px 20px rgba(0,0,0,.08)}',
132
+ '.dvb-stage[data-interactive="true"] .dvb-frame{cursor:crosshair}',
133
+ '.dvb-hint{padding:6px 2px 0;font-size:11px;color:var(--dsw-alias-label-secondary);display:flex;gap:10px;flex-wrap:wrap}',
134
+ '.dvb-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;height:100%;min-height:220px;text-align:center;padding:24px}',
135
+ '.dvb-empty-title{font-size:14px;font-weight:600}',
136
+ '.dvb-empty-desc{max-width:360px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
137
+ '.dvb-timeline{flex:none;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;min-height:0;max-height:38%}',
138
+ '.dvb-timeline-head{display:flex;align-items:center;gap:8px;padding:8px 14px 6px;flex:none}',
139
+ '.dvb-timeline-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
140
+ '.dvb-rows{overflow:auto;padding:0 8px 10px;display:flex;flex-direction:column;gap:2px;min-height:0}',
141
+ '.dvb-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:5px 6px;border-radius:6px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}',
142
+ '.dvb-row:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}',
143
+ '.dvb-row[data-selected="true"]{background:var(--dsw-alias-bg-layer-2);box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary)}',
144
+ '.dvb-row:disabled{cursor:default;opacity:.6}',
145
+ '.dvb-row-time{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:none;width:56px}',
146
+ '.dvb-row-kind{flex:none;min-width:52px;padding:0 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11px;text-align:center}',
147
+ '.dvb-row-kind[data-kind^="user"]{background:var(--dsw-alias-brand-primary);color:#fff}',
148
+ '.dvb-row-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
149
+ '.dvb-row-text[data-error="true"]{color:var(--dsw-alias-state-error-primary)}',
150
+ '.dvb-empty-timeline{padding:2px 14px 12px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
151
+ '.dvb-error{margin:0 14px 10px;padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:12px}',
152
+ '.dvb-busy{font-size:11px;color:var(--dsw-alias-label-secondary)}',
153
+ ].join('\n');
154
+
155
+ window.__ModuleLoader__.load({
156
+ id: MODULE_ID,
157
+ factory(require) {
158
+ var React = require('react');
159
+ var h = React.createElement;
160
+ var useState = React.useState;
161
+ var useEffect = React.useEffect;
162
+ var useRef = React.useRef;
163
+ var useCallback = React.useCallback;
164
+ var useMemo = React.useMemo;
165
+
166
+ var translateCurrent = function (key) {
167
+ var table = DICT.zh;
168
+ return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : key;
169
+ };
170
+
171
+ function bindLocale(ctx) {
172
+ var locale = ctx.get('locale');
173
+ if (!locale || typeof locale.register !== 'function') return;
174
+ try {
175
+ var dispose = locale.register(NS, DICT);
176
+ if (typeof locale.bind === 'function') {
177
+ translateCurrent = locale.bind(NS);
178
+ } else {
179
+ translateCurrent = function (key) {
180
+ var table = DICT.zh;
181
+ return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : key;
182
+ };
183
+ }
184
+ ctx.effect(
185
+ function () {
186
+ return function () {
187
+ translateCurrent = function (key) {
188
+ return key;
189
+ };
190
+ try {
191
+ if (typeof dispose === 'function') dispose();
192
+ } catch {
193
+ /* removing a dictionary must never break unload */
194
+ }
195
+ };
196
+ },
197
+ 'dsh-visual-browser: dictionaries',
198
+ );
199
+ } catch (error) {
200
+ console.warn('[dsh-visual-browser] locale registration failed:', error && error.message ? error.message : error);
201
+ }
202
+ }
203
+
204
+ function installStyles(ctx) {
205
+ ctx.effect(function () {
206
+ var style = document.createElement('style');
207
+ style.setAttribute('data-plugin', MODULE_ID);
208
+ style.textContent = CSS;
209
+ document.head.appendChild(style);
210
+ return function () {
211
+ try {
212
+ style.remove();
213
+ } catch {
214
+ /* already gone */
215
+ }
216
+ };
217
+ }, 'dsh-visual-browser: styles');
218
+ }
219
+
220
+ // ---------------------------------------------------------------------
221
+ // Host API
222
+ // ---------------------------------------------------------------------
223
+
224
+ async function requestJson(path, options) {
225
+ var init = { method: (options && options.method) || 'GET', headers: {} };
226
+ if (options && options.body !== undefined) {
227
+ init.headers['content-type'] = 'application/json';
228
+ init.body = JSON.stringify(options.body);
229
+ }
230
+ var response = await fetch(API + path, init);
231
+ var text = await response.text();
232
+ var data = null;
233
+ if (text) {
234
+ try {
235
+ data = JSON.parse(text);
236
+ } catch {
237
+ data = null;
238
+ }
239
+ }
240
+ if (!response.ok) {
241
+ var message = data && data.error && data.error.message ? data.error.message : 'HTTP ' + response.status;
242
+ throw new Error(message);
243
+ }
244
+ return data || {};
245
+ }
246
+
247
+ function pad(value) {
248
+ return value < 10 ? '0' + value : String(value);
249
+ }
250
+
251
+ function clock(ts) {
252
+ var date = new Date(ts || Date.now());
253
+ return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
254
+ }
255
+
256
+ function shortUrl(value) {
257
+ var text = String(value || '');
258
+ return text.replace(/^https?:\/\//, '').replace(/\/$/, '');
259
+ }
260
+
261
+ // ---------------------------------------------------------------------
262
+ // Icons (inline SVG, currentColor)
263
+ // ---------------------------------------------------------------------
264
+
265
+ function icon(name, size) {
266
+ var common = {
267
+ width: size || 16,
268
+ height: size || 16,
269
+ viewBox: '0 0 24 24',
270
+ fill: 'none',
271
+ stroke: 'currentColor',
272
+ strokeWidth: 1.9,
273
+ strokeLinecap: 'round',
274
+ strokeLinejoin: 'round',
275
+ 'aria-hidden': true,
276
+ focusable: false,
277
+ };
278
+ var paths;
279
+ switch (name) {
280
+ case 'play':
281
+ paths = [h('path', { key: 'a', d: 'M8 5.5v13l11-6.5-11-6.5Z', fill: 'currentColor', stroke: 'none' })];
282
+ break;
283
+ case 'stop':
284
+ paths = [h('rect', { key: 'a', x: 6, y: 6, width: 12, height: 12, rx: 2.5, fill: 'currentColor', stroke: 'none' })];
285
+ break;
286
+ case 'back':
287
+ paths = [h('path', { key: 'a', d: 'M14.5 6.5 8 12l6.5 5.5' })];
288
+ break;
289
+ case 'forward':
290
+ paths = [h('path', { key: 'a', d: 'M9.5 6.5 16 12l-6.5 5.5' })];
291
+ break;
292
+ case 'reload':
293
+ paths = [
294
+ h('path', { key: 'a', d: 'M19 12a7 7 0 1 1-2.1-5' }),
295
+ h('path', { key: 'b', d: 'M19 4.5V9h-4.5' }),
296
+ ];
297
+ break;
298
+ case 'camera':
299
+ paths = [
300
+ h('path', { key: 'a', d: 'M4.5 8.5h3l1.5-2h6l1.5 2h3v9.5h-15Z' }),
301
+ h('circle', { key: 'b', cx: 12, cy: 13, r: 3.2 }),
302
+ ];
303
+ break;
304
+ case 'live':
305
+ paths = [
306
+ h('circle', { key: 'a', cx: 12, cy: 12, r: 3 }),
307
+ h('path', { key: 'b', d: 'M6.5 6.5a7.8 7.8 0 0 0 0 11M17.5 6.5a7.8 7.8 0 0 1 0 11' }),
308
+ ];
309
+ break;
310
+ default:
311
+ paths = [];
312
+ }
313
+ return h('svg', common, paths);
314
+ }
315
+
316
+ function PanelIcon(props) {
317
+ var size = (props && props.size) || 16;
318
+ var active = props && props.active;
319
+ return h(
320
+ 'svg',
321
+ {
322
+ width: size,
323
+ height: size,
324
+ viewBox: '0 0 24 24',
325
+ fill: 'none',
326
+ stroke: 'currentColor',
327
+ strokeWidth: 1.8,
328
+ strokeLinecap: 'round',
329
+ strokeLinejoin: 'round',
330
+ 'aria-hidden': true,
331
+ focusable: false,
332
+ style: { opacity: active ? 1 : 0.86 },
333
+ },
334
+ h('rect', { key: 'a', x: 3, y: 4.5, width: 18, height: 15, rx: 2.6 }),
335
+ h('path', { key: 'b', d: 'M3 8.5h18' }),
336
+ h('circle', { key: 'c', cx: 12, cy: 13.6, r: 2.2, fill: 'currentColor', stroke: 'none' }),
337
+ );
338
+ }
339
+
340
+ // ---------------------------------------------------------------------
341
+ // Panel
342
+ // ---------------------------------------------------------------------
343
+
344
+ var EMPTY_STATE = {
345
+ running: false,
346
+ starting: false,
347
+ url: '',
348
+ title: '',
349
+ viewport: { width: 1280, height: 800 },
350
+ frameSeq: 0,
351
+ actionSeq: 0,
352
+ frameSeqs: [],
353
+ actions: [],
354
+ allowUserControl: true,
355
+ lastError: '',
356
+ };
357
+
358
+ function BrowserPage(props) {
359
+ var ctx = props.ctx;
360
+ var t = translateCurrent;
361
+ var [tick, setTick] = useState(0);
362
+ var [state, setState] = useState(EMPTY_STATE);
363
+ var [reachable, setReachable] = useState(true);
364
+ var [busy, setBusy] = useState('');
365
+ var [error, setError] = useState('');
366
+ var [live, setLive] = useState(false);
367
+ var [liveTick, setLiveTick] = useState(0);
368
+ var [previewSeq, setPreviewSeq] = useState(0);
369
+ var [draft, setDraft] = useState('');
370
+ var [dirty, setDirty] = useState(false);
371
+ var stageRef = useRef(null);
372
+ var wheelAcc = useRef({ deltaY: 0, timer: 0 });
373
+
374
+ useEffect(function () {
375
+ var locale = ctx.get('locale');
376
+ if (!locale || typeof locale.subscribe !== 'function') return undefined;
377
+ return locale.subscribe(function () {
378
+ setTick(function (value) {
379
+ return value + 1;
380
+ });
381
+ });
382
+ }, [ctx]);
383
+
384
+ useEffect(
385
+ function () {
386
+ var cancelled = false;
387
+ var timer = null;
388
+ var poll = async function () {
389
+ try {
390
+ var data = await requestJson('/state');
391
+ if (cancelled) return;
392
+ setReachable(true);
393
+ if (data.state) setState(data.state);
394
+ } catch {
395
+ if (!cancelled) setReachable(false);
396
+ }
397
+ if (!cancelled) {
398
+ timer = window.setTimeout(poll, document.visibilityState === 'hidden' ? 5000 : 1500);
399
+ }
400
+ };
401
+ poll();
402
+ return function () {
403
+ cancelled = true;
404
+ if (timer) window.clearTimeout(timer);
405
+ };
406
+ },
407
+ [],
408
+ );
409
+
410
+ useEffect(
411
+ function () {
412
+ if (!dirty) setDraft(state.url && state.url !== 'about:blank' ? state.url : '');
413
+ },
414
+ [state.url, dirty],
415
+ );
416
+
417
+ useEffect(
418
+ function () {
419
+ if (!live || !state.running) return undefined;
420
+ var id = window.setInterval(function () {
421
+ setLiveTick(function (value) {
422
+ return value + 1;
423
+ });
424
+ }, 1100);
425
+ return function () {
426
+ window.clearInterval(id);
427
+ };
428
+ },
429
+ [live, state.running],
430
+ );
431
+
432
+ var run = useCallback(async function (label, path, body) {
433
+ setBusy(label);
434
+ setError('');
435
+ try {
436
+ var data = await requestJson(path, { method: 'POST', body: body || {} });
437
+ if (data && data.state) {
438
+ setState(data.state);
439
+ setReachable(true);
440
+ }
441
+ } catch (failure) {
442
+ setError(failure && failure.message ? failure.message : String(failure));
443
+ } finally {
444
+ setBusy('');
445
+ }
446
+ }, []);
447
+
448
+ var sendWheel = useCallback(
449
+ function (deltaY) {
450
+ if (!state.running || !state.allowUserControl) return;
451
+ void run('wheel', '/wheel', { deltaY: Math.round(deltaY) });
452
+ },
453
+ [run, state.running, state.allowUserControl],
454
+ );
455
+
456
+ useEffect(
457
+ function () {
458
+ var stage = stageRef.current;
459
+ if (!stage) return undefined;
460
+ var onWheel = function (event) {
461
+ if (!state.running || !state.allowUserControl) return;
462
+ event.preventDefault();
463
+ var acc = wheelAcc.current;
464
+ acc.deltaY += event.deltaY;
465
+ if (acc.timer) window.clearTimeout(acc.timer);
466
+ acc.timer = window.setTimeout(function () {
467
+ var deltaY = acc.deltaY;
468
+ acc.deltaY = 0;
469
+ acc.timer = 0;
470
+ if (Math.abs(deltaY) >= 1) sendWheel(deltaY);
471
+ }, 160);
472
+ };
473
+ stage.addEventListener('wheel', onWheel, { passive: false });
474
+ return function () {
475
+ stage.removeEventListener('wheel', onWheel);
476
+ var acc = wheelAcc.current;
477
+ if (acc.timer) window.clearTimeout(acc.timer);
478
+ };
479
+ },
480
+ [sendWheel, state.running, state.allowUserControl],
481
+ );
482
+
483
+ var onFrameClick = function (event) {
484
+ if (!state.running || !state.allowUserControl) return;
485
+ var image = event.currentTarget;
486
+ var rect = image.getBoundingClientRect();
487
+ if (!rect || rect.width < 2 || rect.height < 2) return;
488
+ var x = (event.clientX - rect.left) / rect.width;
489
+ var y = (event.clientY - rect.top) / rect.height;
490
+ if (x < 0 || x > 1 || y < 0 || y > 1) return;
491
+ void run('click', '/click', { x: x, y: y });
492
+ };
493
+
494
+ var frameSrc = useMemo(
495
+ function () {
496
+ if (previewSeq > 0) return API + '/frame?seq=' + previewSeq;
497
+ if (live && state.running) return API + '/live?t=' + liveTick;
498
+ return API + '/frame?seq=' + (state.frameSeq || 0);
499
+ },
500
+ [previewSeq, live, liveTick, state.frameSeq, state.running],
501
+ );
502
+
503
+ var hasFrame = previewSeq > 0 || (state.running && state.frameSeq > 0);
504
+ var kindLabel = function (kind) {
505
+ var key = 'kind.' + kind;
506
+ var text = t(key);
507
+ return text === key ? kind : text;
508
+ };
509
+
510
+ var openOrStart = function () {
511
+ void run('open', '/start', { url: draft.trim() });
512
+ setDirty(false);
513
+ };
514
+
515
+ var statusText = !reachable
516
+ ? t('state.offline')
517
+ : state.starting
518
+ ? t('state.starting')
519
+ : state.running
520
+ ? t('state.running')
521
+ : t('state.stopped');
522
+
523
+ return h(
524
+ 'div',
525
+ { className: 'dvb-page' },
526
+ h(
527
+ 'div',
528
+ { className: 'dvb-head' },
529
+ h(
530
+ 'div',
531
+ { className: 'dvb-head-row' },
532
+ h('span', {
533
+ className: 'dvb-dot',
534
+ 'data-on': String(state.running),
535
+ 'data-error': String(!reachable),
536
+ title: statusText + (state.headless ? ' · headless' : ''),
537
+ }),
538
+ h('span', { className: 'dvb-title' }, t('panel.title')),
539
+ h('span', { className: 'dvb-status', title: state.title || state.url || '' }, state.title || shortUrl(state.url) || statusText),
540
+ h('span', { className: 'dvb-spacer' }),
541
+ busy ? h('span', { className: 'dvb-busy' }, t('busy')) : null,
542
+ h(
543
+ 'div',
544
+ { className: 'dvb-tools' },
545
+ h(
546
+ 'button',
547
+ {
548
+ type: 'button',
549
+ className: 'dvb-btn',
550
+ title: t('action.back'),
551
+ 'aria-label': t('action.back'),
552
+ disabled: !state.running || Boolean(busy),
553
+ onClick: function () {
554
+ void run('back', '/nav', { action: 'back' });
555
+ },
556
+ },
557
+ icon('back'),
558
+ ),
559
+ h(
560
+ 'button',
561
+ {
562
+ type: 'button',
563
+ className: 'dvb-btn',
564
+ title: t('action.forward'),
565
+ 'aria-label': t('action.forward'),
566
+ disabled: !state.running || Boolean(busy),
567
+ onClick: function () {
568
+ void run('forward', '/nav', { action: 'forward' });
569
+ },
570
+ },
571
+ icon('forward'),
572
+ ),
573
+ h(
574
+ 'button',
575
+ {
576
+ type: 'button',
577
+ className: 'dvb-btn',
578
+ title: t('action.reload'),
579
+ 'aria-label': t('action.reload'),
580
+ disabled: !state.running || Boolean(busy),
581
+ onClick: function () {
582
+ void run('reload', '/nav', { action: 'reload' });
583
+ },
584
+ },
585
+ icon('reload'),
586
+ ),
587
+ h(
588
+ 'button',
589
+ {
590
+ type: 'button',
591
+ className: 'dvb-btn',
592
+ title: t('action.live'),
593
+ 'aria-label': t('action.live'),
594
+ 'data-on': String(live),
595
+ disabled: !state.running || Boolean(busy),
596
+ onClick: function () {
597
+ setLive(function (value) {
598
+ return !value;
599
+ });
600
+ setLiveTick(function (value) {
601
+ return value + 1;
602
+ });
603
+ },
604
+ },
605
+ icon('live'),
606
+ ),
607
+ h(
608
+ 'button',
609
+ {
610
+ type: 'button',
611
+ className: 'dvb-btn',
612
+ title: t('action.capture'),
613
+ 'aria-label': t('action.capture'),
614
+ disabled: !state.running || Boolean(busy),
615
+ onClick: function () {
616
+ void run('capture', '/capture', {});
617
+ },
618
+ },
619
+ icon('camera'),
620
+ ),
621
+ state.running
622
+ ? h(
623
+ 'button',
624
+ {
625
+ type: 'button',
626
+ className: 'dvb-btn',
627
+ title: t('action.stop'),
628
+ 'aria-label': t('action.stop'),
629
+ disabled: Boolean(busy),
630
+ onClick: function () {
631
+ void run('stop', '/stop', {});
632
+ },
633
+ },
634
+ icon('stop'),
635
+ )
636
+ : h(
637
+ 'button',
638
+ {
639
+ type: 'button',
640
+ className: 'dvb-btn',
641
+ 'data-variant': 'primary',
642
+ disabled: Boolean(busy),
643
+ onClick: openOrStart,
644
+ },
645
+ icon('play'),
646
+ t('action.start'),
647
+ ),
648
+ ),
649
+ ),
650
+ h(
651
+ 'div',
652
+ { className: 'dvb-url' },
653
+ h('input', {
654
+ className: 'dvb-input',
655
+ type: 'text',
656
+ spellCheck: false,
657
+ placeholder: t('url.placeholder'),
658
+ value: draft,
659
+ onChange: function (event) {
660
+ setDirty(true);
661
+ setDraft(event.target.value);
662
+ },
663
+ onKeyDown: function (event) {
664
+ if (event.key === 'Enter') {
665
+ event.preventDefault();
666
+ openOrStart();
667
+ }
668
+ },
669
+ }),
670
+ h(
671
+ 'button',
672
+ {
673
+ type: 'button',
674
+ className: 'dvb-btn',
675
+ disabled: Boolean(busy) || draft.trim() === '',
676
+ onClick: openOrStart,
677
+ },
678
+ t('action.open'),
679
+ ),
680
+ ),
681
+ ),
682
+ error ? h('div', { className: 'dvb-error' }, error) : null,
683
+ !reachable ? h('div', { className: 'dvb-error' }, t('error.offline')) : null,
684
+ h(
685
+ 'div',
686
+ { className: 'dvb-stage', ref: stageRef, 'data-interactive': String(state.running && state.allowUserControl) },
687
+ hasFrame
688
+ ? h(
689
+ 'div',
690
+ null,
691
+ h('img', {
692
+ className: 'dvb-frame',
693
+ src: frameSrc,
694
+ alt: state.title || state.url || t('panel.title'),
695
+ draggable: false,
696
+ onClick: onFrameClick,
697
+ }),
698
+ h(
699
+ 'div',
700
+ { className: 'dvb-hint' },
701
+ state.running ? h('span', null, state.allowUserControl ? t('hint.interactive') : '—') : null,
702
+ state.running && state.headless ? h('span', null, t('hint.headless')) : null,
703
+ ),
704
+ )
705
+ : h(
706
+ 'div',
707
+ { className: 'dvb-empty' },
708
+ h('svg', {
709
+ width: 56,
710
+ height: 56,
711
+ viewBox: '0 0 24 24',
712
+ fill: 'none',
713
+ stroke: 'var(--dsw-alias-label-secondary)',
714
+ strokeWidth: 1.2,
715
+ 'aria-hidden': true,
716
+ },
717
+ h('rect', { x: 2.5, y: 4, width: 19, height: 16, rx: 2.5 }),
718
+ h('path', { d: 'M2.5 8h19' }),
719
+ h('circle', { cx: 12, cy: 13.5, r: 2.6 }),
720
+ ),
721
+ h('div', { className: 'dvb-empty-title' }, t('empty.title')),
722
+ h('div', { className: 'dvb-empty-desc' }, t('empty.desc')),
723
+ state.running
724
+ ? h('button', {
725
+ type: 'button',
726
+ className: 'dvb-btn',
727
+ disabled: Boolean(busy),
728
+ onClick: function () {
729
+ void run('capture', '/capture', {});
730
+ },
731
+ }, t('action.capture'))
732
+ : null,
733
+ ),
734
+ ),
735
+ h(
736
+ 'div',
737
+ { className: 'dvb-timeline' },
738
+ h(
739
+ 'div',
740
+ { className: 'dvb-timeline-head' },
741
+ h('span', { className: 'dvb-timeline-title' }, t('timeline.title')),
742
+ h('span', { className: 'dvb-status' }, state.actions.length ? String(state.actions.length) : ''),
743
+ h('span', { className: 'dvb-spacer' }),
744
+ previewSeq > 0
745
+ ? h(
746
+ 'button',
747
+ {
748
+ type: 'button',
749
+ className: 'dvb-btn',
750
+ onClick: function () {
751
+ setPreviewSeq(0);
752
+ },
753
+ },
754
+ t('timeline.latest'),
755
+ )
756
+ : null,
757
+ ),
758
+ state.actions.length === 0
759
+ ? h('div', { className: 'dvb-empty-timeline' }, t('timeline.empty'))
760
+ : h(
761
+ 'div',
762
+ { className: 'dvb-rows' },
763
+ state.actions.map(function (action) {
764
+ var selected = previewSeq > 0 && previewSeq === action.frameSeq;
765
+ var label = action.detail || action.target || kindLabel(action.kind);
766
+ return h(
767
+ 'button',
768
+ {
769
+ key: action.seq,
770
+ type: 'button',
771
+ className: 'dvb-row',
772
+ 'data-selected': String(selected),
773
+ disabled: !action.frameSeq,
774
+ title: label,
775
+ onClick: function () {
776
+ if (action.frameSeq) setPreviewSeq(action.frameSeq);
777
+ },
778
+ },
779
+ h('span', { className: 'dvb-row-time' }, clock(action.ts)),
780
+ h('span', { className: 'dvb-row-kind', 'data-kind': action.kind }, kindLabel(action.kind)),
781
+ h('span', { className: 'dvb-row-text', 'data-error': String(action.ok === false) }, label),
782
+ );
783
+ }),
784
+ ),
785
+ ),
786
+ );
787
+ }
788
+
789
+ // ---------------------------------------------------------------------
790
+ // Module
791
+ // ---------------------------------------------------------------------
792
+
793
+ return {
794
+ inject: ['slots', 'locale'],
795
+ apply: function (ctx) {
796
+ bindLocale(ctx);
797
+ installStyles(ctx);
798
+ var slots = ctx.get('slots');
799
+ if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
800
+ console.warn('[dsh-visual-browser] slots service missing; panel disabled');
801
+ return;
802
+ }
803
+ var register = function (slot, options, component, label) {
804
+ try {
805
+ ctx.effect(function () {
806
+ return slots.inject(slot, function () {
807
+ return slots.register({ name: slot, ...options }, component);
808
+ });
809
+ }, label);
810
+ } catch (error) {
811
+ console.warn('[dsh-visual-browser] could not register ' + slot + ':', error && error.message ? error.message : error);
812
+ }
813
+ };
814
+ register(
815
+ 'sidebar.panellist',
816
+ {
817
+ id: 'visual-browser',
818
+ order: 40,
819
+ label: function () {
820
+ return translateCurrent('panel.title');
821
+ },
822
+ },
823
+ function (props) {
824
+ return h(PanelIcon, { size: props && props.size ? props.size : 16, active: Boolean(props && props.active) });
825
+ },
826
+ 'dsh-visual-browser: sidebar entry',
827
+ );
828
+ register('main', { key: 'visual-browser' }, function () {
829
+ return h(BrowserPage, { ctx: ctx });
830
+ }, 'dsh-visual-browser: main page');
831
+ },
832
+ };
833
+ },
834
+ });
835
+ })();