@nextop-os/browser-node 0.0.15 → 0.0.17

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,852 @@
1
+ import {
2
+ resolveBrowserSessionPartition
3
+ } from "./chunk-OTK5YBCK.js";
4
+
5
+ // src/react/BrowserNode.tsx
6
+ import {
7
+ ArrowLeftIcon,
8
+ ArrowRightIcon,
9
+ Button,
10
+ LaunchIcon,
11
+ LoadingIcon,
12
+ RefreshIcon,
13
+ cn
14
+ } from "@nextop-os/ui-system";
15
+
16
+ // src/react/useBrowserNodeController.ts
17
+ import { useEffect, useMemo } from "react";
18
+ import { useExternalStoreSnapshot } from "@nextop-os/ui-react-hooks";
19
+
20
+ // src/core/nodeController.ts
21
+ var controllerRegistry = /* @__PURE__ */ new Map();
22
+ function acquireBrowserNodeController(input) {
23
+ const existing = controllerRegistry.get(input.nodeId);
24
+ const entry = existing ?? createBrowserNodeControllerEntry({
25
+ defaultUrl: input.defaultUrl,
26
+ feature: input.feature,
27
+ nodeId: input.nodeId,
28
+ profileId: input.profileId ?? null,
29
+ sessionMode: input.sessionMode ?? "shared"
30
+ });
31
+ entry.context = {
32
+ defaultUrl: input.defaultUrl,
33
+ feature: input.feature,
34
+ nodeId: input.nodeId,
35
+ profileId: input.profileId ?? null,
36
+ sessionMode: input.sessionMode ?? "shared"
37
+ };
38
+ if (!existing) {
39
+ controllerRegistry.set(input.nodeId, entry);
40
+ }
41
+ reconcileBrowserNodeControllerState(entry, {
42
+ allowAutoActivate: false,
43
+ notifyListeners: false
44
+ });
45
+ return entry.controller;
46
+ }
47
+ function createBrowserNodeControllerEntry(context) {
48
+ const runtime = context.feature.runtimeStore.getNodeState(context.nodeId);
49
+ const displayUrl = resolveBrowserNodeDisplayUrl(runtime, context.defaultUrl);
50
+ const entry = {
51
+ connectedRelease: null,
52
+ controller: null,
53
+ context,
54
+ lastColdActivationUrl: null,
55
+ listeners: /* @__PURE__ */ new Set(),
56
+ pendingColdActivationUrl: null,
57
+ refCount: 0,
58
+ runtimeUnsubscribe: null,
59
+ state: {
60
+ displayUrl,
61
+ draftUrl: displayUrl,
62
+ runtime
63
+ }
64
+ };
65
+ entry.controller = {
66
+ getState() {
67
+ return entry.state;
68
+ },
69
+ goBack() {
70
+ return entry.context.feature.hostApi.goBack({
71
+ nodeId: entry.context.nodeId
72
+ });
73
+ },
74
+ goForward() {
75
+ return entry.context.feature.hostApi.goForward({
76
+ nodeId: entry.context.nodeId
77
+ });
78
+ },
79
+ reload() {
80
+ return entry.context.feature.hostApi.reload({
81
+ nodeId: entry.context.nodeId
82
+ });
83
+ },
84
+ release() {
85
+ entry.refCount = Math.max(0, entry.refCount - 1);
86
+ if (entry.refCount > 0) {
87
+ return;
88
+ }
89
+ entry.connectedRelease?.();
90
+ entry.connectedRelease = null;
91
+ entry.runtimeUnsubscribe?.();
92
+ entry.runtimeUnsubscribe = null;
93
+ controllerRegistry.delete(entry.context.nodeId);
94
+ },
95
+ retain() {
96
+ entry.refCount += 1;
97
+ if (entry.refCount > 1) {
98
+ return;
99
+ }
100
+ if (!controllerRegistry.has(entry.context.nodeId)) {
101
+ controllerRegistry.set(entry.context.nodeId, entry);
102
+ }
103
+ entry.connectedRelease = entry.context.feature.connect();
104
+ entry.runtimeUnsubscribe = entry.context.feature.runtimeStore.subscribe(
105
+ () => {
106
+ reconcileBrowserNodeControllerState(entry, {
107
+ allowAutoActivate: true,
108
+ notifyListeners: true
109
+ });
110
+ }
111
+ );
112
+ reconcileBrowserNodeControllerState(entry, {
113
+ allowAutoActivate: true,
114
+ notifyListeners: true
115
+ });
116
+ },
117
+ setDraftUrl(nextUrl) {
118
+ if (entry.state.draftUrl === nextUrl) {
119
+ return;
120
+ }
121
+ entry.state = {
122
+ ...entry.state,
123
+ draftUrl: nextUrl
124
+ };
125
+ notifyBrowserNodeControllerListeners(entry);
126
+ },
127
+ subscribe(listener) {
128
+ entry.listeners.add(listener);
129
+ return () => {
130
+ entry.listeners.delete(listener);
131
+ };
132
+ },
133
+ async submitDraftUrl() {
134
+ const resolved = entry.context.feature.resolveAddressInput(
135
+ entry.state.draftUrl
136
+ );
137
+ if (!resolved.url) {
138
+ return;
139
+ }
140
+ if (entry.state.draftUrl !== resolved.url) {
141
+ entry.state = {
142
+ ...entry.state,
143
+ draftUrl: resolved.url
144
+ };
145
+ notifyBrowserNodeControllerListeners(entry);
146
+ }
147
+ await entry.context.feature.hostApi.navigate({
148
+ nodeId: entry.context.nodeId,
149
+ url: resolved.url
150
+ });
151
+ }
152
+ };
153
+ return entry;
154
+ }
155
+ function notifyBrowserNodeControllerListeners(entry) {
156
+ for (const listener of entry.listeners) {
157
+ listener();
158
+ }
159
+ }
160
+ function resolveBrowserNodeDisplayUrl(runtime, defaultUrl) {
161
+ const resolvedRuntimeUrl = runtime.url?.trim() ?? "";
162
+ return resolvedRuntimeUrl.length > 0 ? resolvedRuntimeUrl : defaultUrl;
163
+ }
164
+ function reconcileBrowserNodeControllerState(entry, options) {
165
+ const runtime = entry.context.feature.runtimeStore.getNodeState(
166
+ entry.context.nodeId
167
+ );
168
+ const displayUrl = resolveBrowserNodeDisplayUrl(
169
+ runtime,
170
+ entry.context.defaultUrl
171
+ );
172
+ const nextDraftUrl = displayUrl !== entry.state.displayUrl ? displayUrl : entry.state.draftUrl;
173
+ const changed = entry.state.runtime !== runtime || entry.state.displayUrl !== displayUrl || entry.state.draftUrl !== nextDraftUrl;
174
+ if (changed) {
175
+ entry.state = {
176
+ displayUrl,
177
+ draftUrl: nextDraftUrl,
178
+ runtime
179
+ };
180
+ if (options.notifyListeners) {
181
+ notifyBrowserNodeControllerListeners(entry);
182
+ }
183
+ }
184
+ if (options.allowAutoActivate) {
185
+ void maybeActivateColdBrowserNode(entry).catch(() => void 0);
186
+ }
187
+ }
188
+ async function maybeActivateColdBrowserNode(entry) {
189
+ const { defaultUrl, feature, nodeId, profileId, sessionMode } = entry.context;
190
+ const trimmedUrl = defaultUrl.trim();
191
+ if (trimmedUrl.length === 0 || entry.state.runtime.lifecycle !== "cold" || entry.state.runtime.isLoading || entry.state.runtime.error !== null || entry.pendingColdActivationUrl === trimmedUrl || entry.lastColdActivationUrl === trimmedUrl) {
192
+ return;
193
+ }
194
+ entry.pendingColdActivationUrl = trimmedUrl;
195
+ try {
196
+ await feature.hostApi.activate({
197
+ nodeId,
198
+ profileId,
199
+ sessionMode,
200
+ url: trimmedUrl
201
+ });
202
+ entry.lastColdActivationUrl = trimmedUrl;
203
+ } finally {
204
+ if (entry.pendingColdActivationUrl === trimmedUrl) {
205
+ entry.pendingColdActivationUrl = null;
206
+ }
207
+ }
208
+ }
209
+
210
+ // src/react/useBrowserNodeController.ts
211
+ function useBrowserNodeController(input) {
212
+ const controller = useMemo(
213
+ () => acquireBrowserNodeController({
214
+ defaultUrl: input.defaultUrl,
215
+ feature: input.feature,
216
+ nodeId: input.nodeId,
217
+ profileId: input.profileId ?? null,
218
+ sessionMode: input.sessionMode ?? "shared"
219
+ }),
220
+ [
221
+ input.defaultUrl,
222
+ input.feature,
223
+ input.nodeId,
224
+ input.profileId,
225
+ input.sessionMode
226
+ ]
227
+ );
228
+ useEffect(() => {
229
+ controller.retain();
230
+ return () => {
231
+ controller.release();
232
+ };
233
+ }, [controller]);
234
+ const state = useExternalStoreSnapshot({
235
+ getSnapshot() {
236
+ return controller.getState();
237
+ },
238
+ subscribe(listener) {
239
+ return controller.subscribe(listener);
240
+ }
241
+ });
242
+ return {
243
+ controller,
244
+ state
245
+ };
246
+ }
247
+
248
+ // src/react/useBrowserNodeWebview.ts
249
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2 } from "react";
250
+ import { useExternalStoreSnapshot as useExternalStoreSnapshot2 } from "@nextop-os/ui-react-hooks";
251
+
252
+ // src/core/webviewController.ts
253
+ var browserGuestUnregisterGraceMs = 250;
254
+ var webviewControllerRegistry = /* @__PURE__ */ new Map();
255
+ var pendingGuestIdsByNodeId = /* @__PURE__ */ new Map();
256
+ var pendingUnregisterTimersByNodeId = /* @__PURE__ */ new Map();
257
+ function acquireBrowserNodeWebviewController(input) {
258
+ const existing = webviewControllerRegistry.get(input.nodeId);
259
+ const entry = existing ?? createBrowserNodeWebviewControllerEntry({
260
+ feature: input.feature,
261
+ initialUrl: input.initialUrl,
262
+ lifecycle: input.lifecycle,
263
+ nodeId: input.nodeId,
264
+ onGuestInteraction: input.onGuestInteraction,
265
+ profileId: input.profileId,
266
+ sessionMode: input.sessionMode
267
+ });
268
+ entry.context = {
269
+ feature: input.feature,
270
+ initialUrl: input.initialUrl,
271
+ lifecycle: input.lifecycle,
272
+ nodeId: input.nodeId,
273
+ onGuestInteraction: input.onGuestInteraction,
274
+ profileId: input.profileId,
275
+ sessionMode: input.sessionMode
276
+ };
277
+ if (!existing) {
278
+ webviewControllerRegistry.set(input.nodeId, entry);
279
+ }
280
+ return entry.controller;
281
+ }
282
+ function createBrowserNodeWebviewControllerEntry(context) {
283
+ const state = resolveBrowserNodeWebviewControllerState(context);
284
+ const entry = {
285
+ attachedListeners: [],
286
+ context,
287
+ controller: null,
288
+ listeners: /* @__PURE__ */ new Set(),
289
+ refCount: 0,
290
+ registeredGuestId: null,
291
+ registeringGuestId: null,
292
+ state,
293
+ webview: null
294
+ };
295
+ entry.controller = {
296
+ getState() {
297
+ return entry.state;
298
+ },
299
+ release() {
300
+ entry.refCount = Math.max(0, entry.refCount - 1);
301
+ if (entry.refCount > 0) {
302
+ return;
303
+ }
304
+ scheduleBrowserNodeGuestUnregister(entry);
305
+ detachBrowserNodeWebview(entry);
306
+ webviewControllerRegistry.delete(entry.context.nodeId);
307
+ },
308
+ retain() {
309
+ entry.refCount += 1;
310
+ if (entry.refCount > 1) {
311
+ return;
312
+ }
313
+ reconcileBrowserNodeWebviewControllerState(entry, {
314
+ allowHostEffects: true,
315
+ notifyListeners: true,
316
+ rebindWebview: true
317
+ });
318
+ },
319
+ setWebview(element) {
320
+ if (entry.webview === element) {
321
+ return;
322
+ }
323
+ detachBrowserNodeWebview(entry);
324
+ entry.webview = element;
325
+ attachBrowserNodeWebview(entry);
326
+ },
327
+ sync() {
328
+ reconcileBrowserNodeWebviewControllerState(entry, {
329
+ allowHostEffects: true,
330
+ notifyListeners: true,
331
+ rebindWebview: true
332
+ });
333
+ },
334
+ subscribe(listener) {
335
+ entry.listeners.add(listener);
336
+ return () => {
337
+ entry.listeners.delete(listener);
338
+ };
339
+ }
340
+ };
341
+ return entry;
342
+ }
343
+ function resolveBrowserNodeWebviewControllerState(context) {
344
+ const webviewPartition = resolveBrowserSessionPartition({
345
+ profileId: context.profileId,
346
+ sessionMode: context.sessionMode
347
+ });
348
+ return {
349
+ shouldRenderWebview: context.lifecycle !== "cold",
350
+ webviewKey: `${context.nodeId}:${webviewPartition}`,
351
+ webviewPartition,
352
+ webviewSrc: resolveBrowserWebviewSrc(context.initialUrl)
353
+ };
354
+ }
355
+ function reconcileBrowserNodeWebviewControllerState(entry, options) {
356
+ const nextState = resolveBrowserNodeWebviewControllerState(entry.context);
357
+ const changed = entry.state.shouldRenderWebview !== nextState.shouldRenderWebview || entry.state.webviewKey !== nextState.webviewKey || entry.state.webviewPartition !== nextState.webviewPartition || entry.state.webviewSrc !== nextState.webviewSrc;
358
+ if (options.allowHostEffects) {
359
+ if (entry.context.lifecycle === "cold") {
360
+ scheduleBrowserNodeGuestUnregister(entry);
361
+ } else {
362
+ clearPendingBrowserNodeGuestUnregister(entry.context.nodeId);
363
+ void entry.context.feature.hostApi.prepareSession({
364
+ nodeId: entry.context.nodeId,
365
+ profileId: entry.context.profileId,
366
+ sessionMode: entry.context.sessionMode
367
+ }).catch(() => void 0);
368
+ }
369
+ }
370
+ if (!changed && !options.rebindWebview) {
371
+ return;
372
+ }
373
+ if (changed) {
374
+ entry.state = nextState;
375
+ }
376
+ detachBrowserNodeWebview(entry);
377
+ attachBrowserNodeWebview(entry);
378
+ if (changed && options.notifyListeners) {
379
+ notifyBrowserNodeWebviewControllerListeners(entry);
380
+ }
381
+ }
382
+ function resolveBrowserWebviewSrc(url) {
383
+ const trimmed = url.trim();
384
+ return trimmed.length > 0 ? trimmed : "about:blank";
385
+ }
386
+ function notifyBrowserNodeWebviewControllerListeners(entry) {
387
+ for (const listener of entry.listeners) {
388
+ listener();
389
+ }
390
+ }
391
+ function clearPendingBrowserNodeGuestUnregister(nodeId) {
392
+ const timerId = pendingUnregisterTimersByNodeId.get(nodeId);
393
+ if (timerId !== void 0) {
394
+ globalThis.clearTimeout(timerId);
395
+ pendingUnregisterTimersByNodeId.delete(nodeId);
396
+ }
397
+ pendingGuestIdsByNodeId.delete(nodeId);
398
+ }
399
+ function scheduleBrowserNodeGuestUnregister(entry) {
400
+ const guestId = entry.registeredGuestId;
401
+ const nodeId = entry.context.nodeId;
402
+ entry.registeringGuestId = null;
403
+ if (guestId === null) {
404
+ clearPendingBrowserNodeGuestUnregister(nodeId);
405
+ return;
406
+ }
407
+ entry.registeredGuestId = null;
408
+ clearPendingBrowserNodeGuestUnregister(nodeId);
409
+ pendingGuestIdsByNodeId.set(nodeId, guestId);
410
+ const timerId = globalThis.setTimeout(() => {
411
+ pendingUnregisterTimersByNodeId.delete(nodeId);
412
+ const pendingGuestId = pendingGuestIdsByNodeId.get(nodeId);
413
+ pendingGuestIdsByNodeId.delete(nodeId);
414
+ if (typeof pendingGuestId !== "number" || !Number.isFinite(pendingGuestId)) {
415
+ return;
416
+ }
417
+ void entry.context.feature.hostApi.unregisterGuest({
418
+ nodeId: entry.context.nodeId,
419
+ webContentsId: pendingGuestId
420
+ }).catch(() => void 0);
421
+ }, browserGuestUnregisterGraceMs);
422
+ pendingUnregisterTimersByNodeId.set(nodeId, timerId);
423
+ }
424
+ function detachBrowserNodeWebview(entry) {
425
+ if (!entry.webview) {
426
+ entry.attachedListeners = [];
427
+ return;
428
+ }
429
+ for (const record of entry.attachedListeners) {
430
+ entry.webview.removeEventListener(record.event, record.listener);
431
+ }
432
+ entry.attachedListeners = [];
433
+ }
434
+ function attachBrowserNodeWebview(entry) {
435
+ const webview = entry.webview;
436
+ if (!webview || !entry.state.shouldRenderWebview) {
437
+ return;
438
+ }
439
+ const registerGuest = async () => {
440
+ const guestId = webview.getWebContentsId?.();
441
+ if (typeof guestId !== "number" || !Number.isFinite(guestId) || guestId <= 0 || entry.registeredGuestId === guestId || entry.registeringGuestId === guestId) {
442
+ return;
443
+ }
444
+ clearPendingBrowserNodeGuestUnregister(entry.context.nodeId);
445
+ entry.registeringGuestId = guestId;
446
+ try {
447
+ await entry.context.feature.hostApi.registerGuest({
448
+ nodeId: entry.context.nodeId,
449
+ profileId: entry.context.profileId,
450
+ sessionMode: entry.context.sessionMode,
451
+ webContentsId: guestId
452
+ });
453
+ entry.registeredGuestId = guestId;
454
+ } finally {
455
+ if (entry.registeringGuestId === guestId) {
456
+ entry.registeringGuestId = null;
457
+ }
458
+ }
459
+ };
460
+ const handleDidAttach = () => {
461
+ void registerGuest().catch(() => void 0);
462
+ };
463
+ const handleDomReady = () => {
464
+ void registerGuest().catch(() => void 0);
465
+ };
466
+ const handleGuestInteraction = () => {
467
+ entry.context.onGuestInteraction?.();
468
+ };
469
+ const records = [
470
+ { event: "did-attach", listener: handleDidAttach },
471
+ { event: "dom-ready", listener: handleDomReady },
472
+ { event: "focus", listener: handleGuestInteraction },
473
+ { event: "ipc-message", listener: handleGuestInteraction }
474
+ ];
475
+ for (const record of records) {
476
+ webview.addEventListener(record.event, record.listener);
477
+ }
478
+ entry.attachedListeners = records;
479
+ }
480
+
481
+ // src/react/useBrowserNodeWebview.ts
482
+ function useBrowserNodeWebview({
483
+ feature,
484
+ initialUrl,
485
+ lifecycle,
486
+ nodeId,
487
+ onGuestInteraction,
488
+ profileId,
489
+ sessionMode
490
+ }) {
491
+ const controller = useMemo2(
492
+ () => acquireBrowserNodeWebviewController({
493
+ feature,
494
+ initialUrl,
495
+ lifecycle,
496
+ nodeId,
497
+ onGuestInteraction,
498
+ profileId,
499
+ sessionMode
500
+ }),
501
+ [
502
+ feature,
503
+ initialUrl,
504
+ lifecycle,
505
+ nodeId,
506
+ onGuestInteraction,
507
+ profileId,
508
+ sessionMode
509
+ ]
510
+ );
511
+ useEffect2(() => {
512
+ controller.retain();
513
+ return () => {
514
+ controller.release();
515
+ };
516
+ }, [controller]);
517
+ useEffect2(() => {
518
+ controller.sync();
519
+ }, [
520
+ controller,
521
+ initialUrl,
522
+ lifecycle,
523
+ nodeId,
524
+ onGuestInteraction,
525
+ profileId,
526
+ sessionMode
527
+ ]);
528
+ const state = useExternalStoreSnapshot2({
529
+ getSnapshot() {
530
+ return controller.getState();
531
+ },
532
+ subscribe(listener) {
533
+ return controller.subscribe(listener);
534
+ }
535
+ });
536
+ const setWebviewRef = useCallback(
537
+ (element) => {
538
+ controller.setWebview(element);
539
+ },
540
+ [controller]
541
+ );
542
+ return {
543
+ shouldRenderWebview: state.shouldRenderWebview,
544
+ setWebviewRef,
545
+ webviewKey: state.webviewKey,
546
+ webviewPartition: state.webviewPartition,
547
+ webviewSrc: state.webviewSrc
548
+ };
549
+ }
550
+
551
+ // src/react/BrowserNode.tsx
552
+ import { jsx, jsxs } from "react/jsx-runtime";
553
+ function BrowserNode({
554
+ defaultUrl,
555
+ feature,
556
+ nodeId,
557
+ onFocusRequest,
558
+ profileId = null,
559
+ sessionMode = "shared",
560
+ showHeader = true
561
+ }) {
562
+ const { controller, state } = useBrowserNodeController({
563
+ defaultUrl,
564
+ feature,
565
+ nodeId,
566
+ profileId,
567
+ sessionMode
568
+ });
569
+ const runtime = state.runtime;
570
+ const errorMessage = runtime.error ? formatBrowserNodeErrorMessage(feature, runtime.error) : null;
571
+ const {
572
+ shouldRenderWebview,
573
+ setWebviewRef,
574
+ webviewKey,
575
+ webviewPartition,
576
+ webviewSrc
577
+ } = useBrowserNodeWebview({
578
+ feature,
579
+ initialUrl: state.displayUrl,
580
+ lifecycle: runtime.lifecycle,
581
+ nodeId,
582
+ onGuestInteraction: onFocusRequest,
583
+ profileId,
584
+ sessionMode
585
+ });
586
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col overflow-hidden bg-background", children: [
587
+ showHeader ? /* @__PURE__ */ jsx(
588
+ BrowserNodeHeader,
589
+ {
590
+ canGoBack: runtime.canGoBack,
591
+ canGoForward: runtime.canGoForward,
592
+ draftUrl: state.draftUrl,
593
+ feature,
594
+ isCold: runtime.lifecycle === "cold",
595
+ isLoading: runtime.isLoading,
596
+ onDraftUrlChange: (nextUrl) => controller.setDraftUrl(nextUrl),
597
+ onFocusRequest,
598
+ onSubmitUrl: () => {
599
+ void controller.submitDraftUrl().catch(() => void 0);
600
+ },
601
+ onGoBack: () => {
602
+ void controller.goBack().catch(() => void 0);
603
+ },
604
+ onGoForward: () => {
605
+ void controller.goForward().catch(() => void 0);
606
+ },
607
+ onReload: () => {
608
+ void controller.reload().catch(() => void 0);
609
+ }
610
+ }
611
+ ) : null,
612
+ /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden bg-background", children: [
613
+ shouldRenderWebview ? /* @__PURE__ */ jsx(
614
+ "webview",
615
+ {
616
+ ref: setWebviewRef,
617
+ className: "absolute inset-0 h-full w-full border-0 bg-background",
618
+ "data-browser-node-webview": "true",
619
+ partition: webviewPartition,
620
+ src: webviewSrc
621
+ },
622
+ webviewKey
623
+ ) : null,
624
+ errorMessage ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-end p-3 text-center", children: /* @__PURE__ */ jsxs(
625
+ "div",
626
+ {
627
+ className: "max-w-[min(320px,100%)] rounded-md border border-border bg-card/95 px-3 py-2 text-sm text-card-foreground shadow-panel",
628
+ role: "status",
629
+ "aria-live": "polite",
630
+ children: [
631
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: feature.i18n.t("loadFailed") }),
632
+ /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: errorMessage })
633
+ ]
634
+ }
635
+ ) }) : null
636
+ ] })
637
+ ] });
638
+ }
639
+ function BrowserNodeWorkbenchHeader({
640
+ className,
641
+ defaultActions,
642
+ defaultUrl,
643
+ dragHandleProps,
644
+ feature,
645
+ nodeId,
646
+ onCloseRequest,
647
+ onFocusRequest
648
+ }) {
649
+ const { controller, state } = useBrowserNodeController({
650
+ defaultUrl,
651
+ feature,
652
+ nodeId
653
+ });
654
+ const runtime = state.runtime;
655
+ return /* @__PURE__ */ jsx(
656
+ BrowserNodeHeader,
657
+ {
658
+ canGoBack: runtime.canGoBack,
659
+ canGoForward: runtime.canGoForward,
660
+ className,
661
+ defaultActions,
662
+ draftUrl: state.draftUrl,
663
+ dragHandleProps,
664
+ feature,
665
+ isCold: runtime.lifecycle === "cold",
666
+ isLoading: runtime.isLoading,
667
+ onCloseRequest,
668
+ onDraftUrlChange: (nextUrl) => controller.setDraftUrl(nextUrl),
669
+ onFocusRequest,
670
+ onSubmitUrl: () => {
671
+ void controller.submitDraftUrl().catch(() => void 0);
672
+ },
673
+ onGoBack: () => {
674
+ void controller.goBack().catch(() => void 0);
675
+ },
676
+ onGoForward: () => {
677
+ void controller.goForward().catch(() => void 0);
678
+ },
679
+ onReload: () => {
680
+ void controller.reload().catch(() => void 0);
681
+ },
682
+ withBorder: false
683
+ }
684
+ );
685
+ }
686
+ function BrowserNodeHeader({
687
+ canGoBack,
688
+ canGoForward,
689
+ className,
690
+ defaultActions,
691
+ draftUrl,
692
+ dragHandleProps,
693
+ feature,
694
+ isCold = false,
695
+ isLoading,
696
+ onCloseRequest,
697
+ onDraftUrlChange,
698
+ onFocusRequest,
699
+ onGoBack,
700
+ onGoForward,
701
+ onReload,
702
+ onSubmitUrl,
703
+ withBorder = true
704
+ }) {
705
+ return /* @__PURE__ */ jsxs(
706
+ "div",
707
+ {
708
+ className: cn(
709
+ "flex h-[var(--workbench-header-height,38px)] min-h-[var(--workbench-header-height,38px)] items-center gap-2 bg-[var(--workbench-window-header-bg)] px-2 pl-3",
710
+ withBorder ? "border-b border-border" : null,
711
+ className
712
+ ),
713
+ "data-browser-node-header": "true",
714
+ onDoubleClick: (event) => {
715
+ if (event.target instanceof Element && event.target.closest(".nodrag")) {
716
+ return;
717
+ }
718
+ event.stopPropagation();
719
+ dragHandleProps?.onDoubleClick?.(event);
720
+ },
721
+ children: [
722
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1", children: [
723
+ /* @__PURE__ */ jsx(
724
+ BrowserNodeHeaderButton,
725
+ {
726
+ disabled: !canGoBack,
727
+ label: feature.i18n.t("actions.back"),
728
+ onClick: onGoBack,
729
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-4" })
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx(
733
+ BrowserNodeHeaderButton,
734
+ {
735
+ disabled: !canGoForward,
736
+ label: feature.i18n.t("actions.forward"),
737
+ onClick: onGoForward,
738
+ children: /* @__PURE__ */ jsx(ArrowRightIcon, { className: "size-4" })
739
+ }
740
+ ),
741
+ /* @__PURE__ */ jsx(
742
+ BrowserNodeHeaderButton,
743
+ {
744
+ label: feature.i18n.t("actions.reload"),
745
+ onClick: onReload,
746
+ children: /* @__PURE__ */ jsx(RefreshIcon, { className: "size-4" })
747
+ }
748
+ )
749
+ ] }),
750
+ /* @__PURE__ */ jsx(
751
+ "div",
752
+ {
753
+ ...dragHandleProps,
754
+ className: "h-full w-8 shrink-0 cursor-grab active:cursor-grabbing",
755
+ "data-browser-node-drag-gutter": "true",
756
+ "data-node-drag-handle": "true",
757
+ "aria-hidden": "true"
758
+ }
759
+ ),
760
+ /* @__PURE__ */ jsxs(
761
+ "form",
762
+ {
763
+ className: "nodrag flex h-8 min-h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-border bg-[var(--workbench-field-bg)] px-2 focus-within:ring-2 focus-within:ring-ring/60",
764
+ onSubmit: (event) => {
765
+ event.preventDefault();
766
+ event.stopPropagation();
767
+ onSubmitUrl();
768
+ },
769
+ children: [
770
+ /* @__PURE__ */ jsx(LaunchIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
771
+ /* @__PURE__ */ jsx(
772
+ "input",
773
+ {
774
+ "aria-label": feature.i18n.t("addressLabel"),
775
+ className: "h-full min-w-0 flex-1 border-0 bg-transparent text-[13px] leading-none text-foreground outline-none placeholder:text-muted-foreground",
776
+ placeholder: feature.i18n.t("addressPlaceholder"),
777
+ value: draftUrl,
778
+ onChange: (event) => onDraftUrlChange(event.target.value),
779
+ onFocus: onFocusRequest
780
+ }
781
+ ),
782
+ isLoading ? /* @__PURE__ */ jsx(LoadingIcon, { className: "size-4 shrink-0 animate-spin text-muted-foreground" }) : null
783
+ ]
784
+ }
785
+ ),
786
+ defaultActions ? /* @__PURE__ */ jsxs("div", { className: "nodrag flex shrink-0 items-center gap-1.5", children: [
787
+ isCold ? /* @__PURE__ */ jsx(
788
+ "span",
789
+ {
790
+ className: "inline-flex h-[26px] min-w-7 items-center justify-center rounded-md bg-muted/80 px-2 text-[10px] font-semibold lowercase tracking-[0.08em] text-muted-foreground",
791
+ "aria-label": feature.i18n.t("coldStatus"),
792
+ children: feature.i18n.t("coldStatus")
793
+ }
794
+ ) : null,
795
+ /* @__PURE__ */ jsx(
796
+ "span",
797
+ {
798
+ className: "contents",
799
+ onClickCapture: (event) => {
800
+ if (!onCloseRequest || !(event.target instanceof Element) || !event.target.closest('[data-workbench-action="close"]')) {
801
+ return;
802
+ }
803
+ onCloseRequest();
804
+ },
805
+ children: defaultActions
806
+ }
807
+ )
808
+ ] }) : null
809
+ ]
810
+ }
811
+ );
812
+ }
813
+ function formatBrowserNodeErrorMessage(feature, error) {
814
+ switch (error.code) {
815
+ case "invalid-url":
816
+ return feature.i18n.t("errors.invalidUrl", error.params);
817
+ case "navigation-failed":
818
+ return feature.i18n.t("errors.navigationFailed", error.params);
819
+ case "unsupported-protocol":
820
+ return feature.i18n.t("errors.unsupportedProtocol", error.params);
821
+ case "unsupported-url":
822
+ return feature.i18n.t("errors.unsupportedUrl", error.params);
823
+ }
824
+ }
825
+ function BrowserNodeHeaderButton({
826
+ children,
827
+ disabled,
828
+ label,
829
+ onClick
830
+ }) {
831
+ return /* @__PURE__ */ jsx(
832
+ Button,
833
+ {
834
+ "aria-label": label,
835
+ className: "rounded-md",
836
+ disabled,
837
+ size: "icon",
838
+ title: label,
839
+ type: "button",
840
+ variant: "chrome",
841
+ onClick,
842
+ children
843
+ }
844
+ );
845
+ }
846
+
847
+ export {
848
+ BrowserNode,
849
+ BrowserNodeWorkbenchHeader,
850
+ BrowserNodeHeader
851
+ };
852
+ //# sourceMappingURL=chunk-XCKUTY7D.js.map