@nextclaw/ui 0.27.4 → 0.27.5-beta.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/.vite/manifest.json +95 -95
  3. package/dist/assets/{account-panel-Cfx3ocPY.js → account-panel-C83GDFuy.js} +1 -1
  4. package/dist/assets/agents-CJA15zzv.js +1 -0
  5. package/dist/assets/{agents-BdeFtIOV.js → agents-Dr2KkEp2.js} +1 -1
  6. package/dist/assets/{app-presenter-provider-BB3Xl8b0.js → app-presenter-provider-Cx9MwMtb.js} +17 -16
  7. package/dist/assets/{appearance-settings-page-DiSRSDN3.js → appearance-settings-page-3IfIzjic.js} +1 -1
  8. package/dist/assets/apps-CvIlb9Rn.js +1 -0
  9. package/dist/assets/{confirm-dialog-jz2cQKId.js → confirm-dialog-D30HJBXN.js} +1 -1
  10. package/dist/assets/{context-menu-C0qywo73.js → context-menu-C6m-WNpl.js} +65 -65
  11. package/dist/assets/cron-BWZWu80t.js +1 -0
  12. package/dist/assets/{dialog-DMKYZDFu.js → dialog-DB_l9LK0.js} +1 -1
  13. package/dist/assets/doc-browser-DQDsmRlr.js +1 -0
  14. package/dist/assets/inbox-CIsD65lW.js +1 -0
  15. package/dist/assets/{keyboard-shortcuts-settings-page-BFBXVf20.js → keyboard-shortcuts-settings-page-L4kLGuB8.js} +1 -1
  16. package/dist/assets/{main-Bp1iozNB.js → main-BNWRrWc_.js} +2 -2
  17. package/dist/assets/{marketplace-BTDWnHP7.js → marketplace-BH1Sq8dL.js} +1 -1
  18. package/dist/assets/mcp-marketplace-page-BA-InAbJ.js +1 -0
  19. package/dist/assets/{model-config-page-CeVvZ29o.js → model-config-page-zqVWyIT0.js} +1 -1
  20. package/dist/assets/panel-apps-CMX4iWwQ.js +1 -0
  21. package/dist/assets/{panelStandalone-DomappXm.js → panelStandalone-C38o5B4Q.js} +1 -1
  22. package/dist/assets/{projects-fPTOn2Wz.js → projects-DNe43T2d.js} +1 -1
  23. package/dist/assets/{provider-models-D8WOdsdu.js → provider-models-zGsDUBHG.js} +1 -1
  24. package/dist/assets/{provider-scoped-model-input-BOPkUOtD.js → provider-scoped-model-input-DpEyP1h8.js} +1 -1
  25. package/dist/assets/{providers-config-page-BilPL0kF.js → providers-config-page-J-1auWPu.js} +1 -1
  26. package/dist/assets/{pwa-Cp7pPvsZ.js → pwa-jmuWbrqH.js} +1 -1
  27. package/dist/assets/remote-C7H2o46-.js +1 -0
  28. package/dist/assets/{runtime-config-page-P5RUq3YK.js → runtime-config-page-CJhqeDcu.js} +1 -1
  29. package/dist/assets/{service-apps-panel-BReYr_ni.js → service-apps-panel-Dj9okUgp.js} +1 -1
  30. package/dist/assets/{theme-provider-BsJ6HxeX.js → theme-provider-D4rd-tCP.js} +1 -1
  31. package/dist/index.html +10 -10
  32. package/dist/panel-standalone.html +4 -4
  33. package/package.json +7 -7
  34. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +16 -0
  35. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +17 -0
  36. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +3 -3
  37. package/src/features/chat/features/message/utils/chat-message.utils.ts +11 -4
  38. package/src/features/chat/features/message/utils/chat-user-question-timeline.utils.ts +42 -0
  39. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +2 -0
  40. package/src/features/panel-apps/components/panel-app-runtime-surface.tsx +3 -1
  41. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +25 -10
  42. package/src/features/right-panel-resources/components/__tests__/system-object-resource.test.tsx +25 -3
  43. package/src/features/right-panel-resources/components/native-object-resource.tsx +3 -1
  44. package/src/features/right-panel-resources/components/system-object-resource.tsx +15 -1
  45. package/src/features/right-panel-resources/pages/resource-page.tsx +29 -2
  46. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -13
  47. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +3 -0
  48. package/src/shared/components/doc-browser/doc-browser.tsx +7 -1
  49. package/src/shared/components/ui/context-menu/context-menu-items.tsx +1 -0
  50. package/src/shared/components/ui/context-menu/context-menu.tsx +9 -0
  51. package/dist/assets/agents-p_wcGhC3.js +0 -1
  52. package/dist/assets/apps-CKFrWb00.js +0 -1
  53. package/dist/assets/cron-UqsFT41c.js +0 -1
  54. package/dist/assets/doc-browser-2VjSkax1.js +0 -1
  55. package/dist/assets/inbox-D62apR2j.js +0 -1
  56. package/dist/assets/mcp-marketplace-page-oHCbBkS-.js +0 -1
  57. package/dist/assets/panel-apps-CniMi7QW.js +0 -1
  58. package/dist/assets/remote-CsJMF8OR.js +0 -1
  59. package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +0 -108
  60. package/src/features/panel-apps/components/panel-app-toolbar.tsx +0 -89
@@ -14,6 +14,16 @@ import { pageResourceTab } from "@/features/right-panel-resources/managers/page-
14
14
  import { pageResourceFromTarget } from "@/features/right-panel-resources/utils/page-resource-identity.utils";
15
15
  import type { PageResource } from "@/features/right-panel-resources/types/page-resource.types";
16
16
  import { t } from "@/shared/lib/i18n";
17
+ import { parseSystemObjectReferenceUri } from "@nextclaw/shared";
18
+ import { RefreshCw } from "lucide-react";
19
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
20
+ import { usePanelApps } from "@/features/panel-apps/hooks/use-panel-apps";
21
+
22
+ function PanelAppResourceTitle({ appId, fallback }: { appId: string; fallback: string }) {
23
+ const panelApps = usePanelApps();
24
+ const title = panelApps.data?.entries.find((entry) => entry.appId === appId)?.title ?? fallback;
25
+ return <span className="min-w-0 flex-1 truncate text-sm">{title}</span>;
26
+ }
17
27
 
18
28
  function ResourcePageContent({ page }: { page: PageResource }) {
19
29
  const app = useAppPresenter();
@@ -22,6 +32,10 @@ function ResourcePageContent({ page }: { page: PageResource }) {
22
32
  const [revision, setRevision] = useState(0);
23
33
  const tab = useMemo(() => pageResourceTab(page), [page]);
24
34
  const renderer = PAGE_RESOURCE_RENDERERS[tab.kind];
35
+ const object = tab.kind === "system-object"
36
+ ? parseSystemObjectReferenceUri(page.uri)
37
+ : null;
38
+ const panelAppId = object?.objectType === "panel-app" ? object.objectId : null;
25
39
  const iframeInstanceId = `main:${page.uri}:${revision}`;
26
40
  const restore = useDocBrowserScrollRestoration({
27
41
  currentTab: tab,
@@ -55,6 +69,7 @@ function ResourcePageContent({ page }: { page: PageResource }) {
55
69
  tab,
56
70
  currentUrl: tab.currentUrl,
57
71
  refreshIframe: () => setRevision((value) => value + 1),
72
+ refreshVersion: revision,
58
73
  open: (uri?: string) => {
59
74
  const next = uri ? app.pageResourceManager.resolve(uri) : null;
60
75
  if (next) app.pageResourceManager.open(next, "main", navigate);
@@ -73,10 +88,22 @@ function ResourcePageContent({ page }: { page: PageResource }) {
73
88
  >
74
89
  <header className="flex min-h-10 shrink-0 items-center gap-2 border-b border-border/60 px-3">
75
90
  <PageResourceIcon uri={page.uri} icon={page.target.dockIcon} />
76
- <span className="min-w-0 flex-1 truncate text-sm">{page.title}</span>
91
+ {panelAppId ? (
92
+ <PanelAppResourceTitle appId={panelAppId} fallback={page.title} />
93
+ ) : (
94
+ <span className="min-w-0 flex-1 truncate text-sm">{page.title}</span>
95
+ )}
96
+ {panelAppId ? (
97
+ <IconActionButton
98
+ icon={<RefreshCw className="h-3.5 w-3.5" />}
99
+ label={t("panelAppsRefreshCurrent")}
100
+ onClick={params.refreshIframe}
101
+ size="sm"
102
+ />
103
+ ) : null}
77
104
  <PageResourceActionsMenu page={page} />
78
105
  </header>
79
- {renderer?.renderToolbar?.(params)}
106
+ {!panelAppId ? renderer?.renderToolbar?.(params) : null}
80
107
  <DocBrowserFrameContent
81
108
  currentTab={tab}
82
109
  currentUrl={tab.currentUrl}
@@ -18,6 +18,7 @@ import type {
18
18
  DocBrowserTab,
19
19
  } from "@/shared/components/doc-browser/doc-browser-context";
20
20
  import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
21
+ import { pageResourceFromTab, usePageResourceActions } from "@/features/right-panel-resources";
21
22
  import { PANEL_APP_SCROLL_RESTORATION_CONTRACT } from "@nextclaw/shared";
22
23
 
23
24
  const { navigateMock } = vi.hoisted(() => ({
@@ -28,6 +29,12 @@ const panelAppHooks = vi.hoisted(() => ({
28
29
  mutate: vi.fn(),
29
30
  }));
30
31
 
32
+ function DocBrowserWithPageActions() {
33
+ const actions = usePageResourceActions();
34
+ return <DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS}
35
+ getTabMenuGroups={(tab) => actions(pageResourceFromTab(tab), 'global')} />;
36
+ }
37
+
31
38
  vi.mock("react-router-dom", async () => ({
32
39
  ...((await vi.importActual("react-router-dom")) as object),
33
40
  useNavigate: () => navigateMock,
@@ -536,21 +543,12 @@ describe("DocBrowser panel app navigation", () => {
536
543
  docBrowserState.activeHistoryIndex = 1;
537
544
  docBrowserState.currentTab = panelAppTab;
538
545
 
539
- render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
546
+ render(<DocBrowserWithPageActions />);
540
547
 
541
548
  expect(screen.getAllByRole("button", { name: "Back" })).toHaveLength(1);
542
549
  expect(screen.queryByRole("button", { name: "Apps" })).toBeNull();
543
550
  await user.click(screen.getByRole("button", { name: "Back" }));
544
551
  expect(docBrowserState.goBack).toHaveBeenCalledTimes(1);
545
-
546
- expect(screen.queryByRole("button", { name: "Pin to left sidebar" })).toBeNull();
547
- await user.click(screen.getByRole("button", { name: "More panel app actions" }));
548
- await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
549
- await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
550
- expect(panelAppHooks.mutate).toHaveBeenCalledWith({
551
- id: "piano",
552
- preferences: { mainSidebar: true },
553
- });
554
552
  });
555
553
 
556
554
  it("keeps placement actions when a restored shortcut only has a stable current URL", async () => {
@@ -569,9 +567,9 @@ describe("DocBrowser panel app navigation", () => {
569
567
  docBrowserState.activeTabId = panelAppTab.id;
570
568
  docBrowserState.currentTab = panelAppTab;
571
569
 
572
- render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
570
+ render(<DocBrowserWithPageActions />);
573
571
 
574
- await user.click(screen.getByRole("button", { name: "More panel app actions" }));
572
+ await user.click(screen.getByRole("button", { name: "More tab actions" }));
575
573
  await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
576
574
  await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
577
575
  expect(panelAppHooks.mutate).toHaveBeenCalledWith({
@@ -623,7 +621,12 @@ describe("DocBrowser scroll restoration", () => {
623
621
  source: initialWindow,
624
622
  }));
625
623
 
626
- fireEvent.click(screen.getByRole("button", { name: "Refresh current panel app" }));
624
+ expect(screen.queryByRole("button", { name: "Refresh current panel app" })).toBeNull();
625
+ fireEvent.click(screen.getByRole("button", { name: "More tab actions" }));
626
+ const standalone = screen.getByRole("menuitem", { name: "Open in New Tab" });
627
+ expect(standalone.getAttribute("href")).toBe("/apps/panel/piano/standalone");
628
+ expect(standalone.getAttribute("target")).toBe("_blank");
629
+ fireEvent.click(screen.getByRole("menuitem", { name: "Refresh current panel app" }));
627
630
 
628
631
  const refreshedIframe = container.querySelector('iframe[title="Piano"]') as HTMLIFrameElement;
629
632
  const refreshedPostMessage = vi.fn();
@@ -1,11 +1,13 @@
1
1
  import type { PointerEvent, ReactNode } from 'react';
2
2
  import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-context';
3
+ import type { ContextMenuGroup } from '@/shared/components/ui/context-menu/context-menu';
3
4
 
4
5
  export type DocBrowserCustomTabRenderParams = {
5
6
  currentUrl: string;
6
7
  open: DocBrowserContextValue['open'];
7
8
  openTarget: DocBrowserContextValue['openTarget'];
8
9
  refreshIframe: () => void;
10
+ refreshVersion: number;
9
11
  tab: DocBrowserTab;
10
12
  };
11
13
 
@@ -17,6 +19,7 @@ export type DocBrowserIframeMessageParams = {
17
19
  };
18
20
 
19
21
  export type DocBrowserCustomTabRenderer = {
22
+ getTabMenuGroups?: (params: DocBrowserCustomTabRenderParams) => readonly ContextMenuGroup[];
20
23
  getIframeSandbox?: (tab: DocBrowserTab) => string | undefined;
21
24
  getTitle?: (tab: DocBrowserTab) => string;
22
25
  onIframeMessage?: (params: DocBrowserIframeMessageParams) => void;
@@ -265,6 +265,7 @@ export function DocBrowser({
265
265
  open,
266
266
  openTarget,
267
267
  refreshIframe,
268
+ refreshVersion: iframeReloadVersion,
268
269
  tab: currentTab,
269
270
  } : undefined;
270
271
  const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
@@ -325,7 +326,12 @@ export function DocBrowser({
325
326
  onToggleDock={handleToggleDock}
326
327
  onSetActiveTab={setActiveTab}
327
328
  onCloseTab={closeTab}
328
- getTabMenuGroups={getTabMenuGroups}
329
+ getTabMenuGroups={(tab) => [
330
+ ...(getTabMenuGroups?.(tab) ?? []),
331
+ ...(tab.id === activeTabId && customRenderParams
332
+ ? customRenderer?.getTabMenuGroups?.(customRenderParams) ?? []
333
+ : []),
334
+ ]}
329
335
  />
330
336
  }>
331
337
  {panelContent}
@@ -164,6 +164,7 @@ export function ContextMenuItems({ groups, onClose }: {
164
164
  onClose={onClose} className={className}>{content}</ContextSubmenu>;
165
165
  const select = () => { item.onSelect?.(); onClose(item.href ? false : item.restoreFocus !== false); };
166
166
  return item.href ? <a key={item.key} role="menuitem" className={className} href={item.href} download={item.download}
167
+ target={item.target} rel={item.rel} referrerPolicy={item.referrerPolicy}
167
168
  onPointerEnter={() => setOpenKey(null)} onClick={(event) => { event.stopPropagation(); select(); }}>{content}</a>
168
169
  : <button key={item.key} type="button" role="menuitem" disabled={item.disabled} aria-pressed={item.pressed}
169
170
  data-menu-key={item.key} className={className} onPointerEnter={() => setOpenKey(null)}
@@ -30,18 +30,27 @@ export type ContextMenuItem = ContextMenuItemBase &
30
30
  children?: never;
31
31
  download?: string;
32
32
  href: string;
33
+ target?: '_blank';
34
+ rel?: string;
35
+ referrerPolicy?: React.HTMLAttributeReferrerPolicy;
33
36
  onSelect?: () => void;
34
37
  }
35
38
  | {
36
39
  children?: never;
37
40
  download?: never;
38
41
  href?: never;
42
+ target?: never;
43
+ rel?: never;
44
+ referrerPolicy?: never;
39
45
  onSelect: () => void;
40
46
  }
41
47
  | {
42
48
  children: readonly ContextMenuGroup[];
43
49
  download?: never;
44
50
  href?: never;
51
+ target?: never;
52
+ rel?: never;
53
+ referrerPolicy?: never;
45
54
  onSelect?: never;
46
55
  }
47
56
  );
@@ -1 +0,0 @@
1
- import{t as e}from"./agents-BdeFtIOV.js";export{e as AgentsPage};
@@ -1 +0,0 @@
1
- import{ot as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as AppsPanel};
@@ -1 +0,0 @@
1
- import{K as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as CronJobResource};
@@ -1 +0,0 @@
1
- import{St as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as DocBrowser};
@@ -1 +0,0 @@
1
- import{z as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as InboxPage};
@@ -1 +0,0 @@
1
- import{st as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as McpMarketplacePage};
@@ -1 +0,0 @@
1
- import{s as e,v as t}from"./theme-provider-BsJ6HxeX.js";import"./app-presenter-provider-BB3Xl8b0.js";export{t as PanelAppHostProvider,e as PanelAppRuntimeSurface};
@@ -1 +0,0 @@
1
- import{F as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as RemoteAccessPage};
@@ -1,108 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
5
-
6
- const mainSidebarMutation = vi.hoisted(() => ({
7
- isPending: false,
8
- pinned: false,
9
- mutate: vi.fn(),
10
- }));
11
-
12
- vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
13
- usePanelApps: () => ({ data:{ entries:[{ ...entry, mainSidebar: mainSidebarMutation.pinned }] } }),
14
- useUpdatePanelAppPreferences: () => mainSidebarMutation,
15
- }));
16
-
17
- const entry = {
18
- appId: 'ink-assistant',
19
- clientDeclared: false,
20
- clientGranted: false,
21
- contentPath: '/api/panel-apps/ink-assistant/content',
22
- createdAt: '2026-08-19T00:00:00.000Z',
23
- favorite: false,
24
- fileName: 'ink-assistant.panel.html',
25
- id: 'ink-assistant',
26
- kind: 'single-file' as const,
27
- mainSidebar: false,
28
- openCount: 0,
29
- sizeBytes: 10,
30
- title: '墨爪助手',
31
- updatedAt: '2026-08-19T00:00:00.000Z',
32
- };
33
-
34
- describe('PanelAppToolbar', () => {
35
- beforeEach(() => {
36
- mainSidebarMutation.isPending = false;
37
- mainSidebarMutation.pinned = false;
38
- mainSidebarMutation.mutate.mockReset();
39
- });
40
-
41
- it('keeps placement in the overflow menu without adding a second back action', async () => {
42
- const user = userEvent.setup();
43
- render(
44
- <PanelAppToolbar
45
- appTitle="墨爪助手"
46
- entry={entry}
47
- onRefresh={vi.fn()}
48
- />,
49
- );
50
-
51
- expect(screen.getByText('墨爪助手')).toBeTruthy();
52
- expect(screen.queryByRole('button', { name: 'Apps' })).toBeNull();
53
-
54
- expect(screen.queryByRole('menuitem', { name: 'Pin to left sidebar' })).toBeNull();
55
- await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
56
- await user.click(screen.getByRole('menuitem', { name: 'Layout and position' }));
57
- const addButton = screen.getByRole('menuitem', { name: 'Pin to left sidebar' });
58
- expect(addButton.getAttribute('aria-pressed')).toBe('false');
59
- await user.click(addButton);
60
-
61
- expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
62
- id: 'ink-assistant',
63
- preferences: { mainSidebar: true },
64
- });
65
- expect(screen.queryByRole('menuitem', { name: 'Pin to left sidebar' })).toBeNull();
66
- });
67
-
68
- it('uses the same menu item to remove an app already in the main sidebar', async () => {
69
- mainSidebarMutation.pinned = true;
70
- const user = userEvent.setup();
71
- render(
72
- <PanelAppToolbar
73
- appTitle="墨爪助手"
74
- entry={{ ...entry, mainSidebar: true }}
75
- onRefresh={vi.fn()}
76
- />,
77
- );
78
-
79
- await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
80
- await user.click(screen.getByRole('menuitem', { name: 'Layout and position' }));
81
- const removeButton = screen.getByRole('menuitem', { name: 'Unpin from left sidebar' });
82
- expect(removeButton.getAttribute('aria-pressed')).toBe('true');
83
- await user.click(removeButton);
84
-
85
- expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
86
- id: 'ink-assistant',
87
- preferences: { mainSidebar: false },
88
- });
89
- });
90
-
91
- it('offers the standalone app link from the toolbar menu', async () => {
92
- const user = userEvent.setup();
93
- render(
94
- <PanelAppToolbar
95
- appTitle="墨爪助手"
96
- entry={entry}
97
- onRefresh={vi.fn()}
98
- />,
99
- );
100
-
101
- await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
102
- const link = screen.getByRole('link', { name: 'Open in New Tab' });
103
- expect(link.getAttribute('href')).toBe('/apps/panel/ink-assistant/standalone');
104
- });
105
- });
106
-
107
- vi.mock("react-router-dom", async (importOriginal) => ({ ...(await importOriginal<object>()),useNavigate: () => vi.fn(), useLocation: () => ({ pathname:'/apps',search:'' }) }));
108
- vi.mock("@/app/components/app-presenter-provider", () => ({ useAppPresenter: () => ({ pageResourceManager:{ open:vi.fn() } }) }));
@@ -1,89 +0,0 @@
1
- import { useMemo, useState } from 'react';
2
- import { MoreVertical, RefreshCw } from 'lucide-react';
3
- import { PanelAppMainSidebarMenuItem } from '@/features/panel-apps/components/panel-app-main-sidebar-menu-item';
4
- import { PanelAppOpenStandaloneMenuItem } from '@/features/panel-apps/components/panel-app-open-standalone-menu-item';
5
- import { usePanelApps } from '@/features/panel-apps/hooks/use-panel-apps';
6
- import { readPanelAppIdFromTab } from '@/features/right-panel-resources';
7
- import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
8
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
9
- import type { PanelAppEntryView } from '@/shared/lib/api';
10
- import { t } from '@/shared/lib/i18n';
11
-
12
- type PanelAppToolbarProps = {
13
- appTitle: string;
14
- entry?: PanelAppEntryView;
15
- onRefresh: () => void;
16
- };
17
-
18
- export function PanelAppToolbar({
19
- appTitle,
20
- entry,
21
- onRefresh,
22
- }: PanelAppToolbarProps) {
23
- const [isMenuOpen, setIsMenuOpen] = useState(false);
24
-
25
- return (
26
- <div className="flex items-center justify-between gap-2 border-b border-border/70 bg-card px-3.5 py-2 shrink-0">
27
- <span
28
- className="min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground"
29
- title={appTitle}
30
- >
31
- {appTitle}
32
- </span>
33
- <IconActionButton
34
- icon={<RefreshCw className="h-3.5 w-3.5" />}
35
- label={t('panelAppsRefreshCurrent')}
36
- onClick={onRefresh}
37
- size="sm"
38
- />
39
- {entry ? (
40
- <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
41
- <PopoverTrigger asChild>
42
- <IconActionButton
43
- icon={<MoreVertical className="h-3.5 w-3.5" />}
44
- label={t('panelAppsMoreActions')}
45
- size="sm"
46
- />
47
- </PopoverTrigger>
48
- <PopoverContent align="end" variant="menu">
49
- <PanelAppOpenStandaloneMenuItem
50
- entry={entry}
51
- onSelect={() => setIsMenuOpen(false)}
52
- />
53
- <PanelAppMainSidebarMenuItem
54
- entry={entry}
55
- onSelect={() => setIsMenuOpen(false)}
56
- />
57
- </PopoverContent>
58
- </Popover>
59
- ) : null}
60
- </div>
61
- );
62
- }
63
-
64
- export function PanelAppDocBrowserToolbar({
65
- appTitle,
66
- currentUrl,
67
- onRefresh,
68
- resourceUri,
69
- }: {
70
- appTitle: string;
71
- currentUrl: string;
72
- onRefresh: () => void;
73
- resourceUri?: string;
74
- }) {
75
- const panelApps = usePanelApps();
76
- const appId = readPanelAppIdFromTab({ currentUrl, resourceUri });
77
- const entry = useMemo(
78
- () => panelApps.data?.entries.find((candidate) => candidate.appId === appId),
79
- [appId, panelApps.data?.entries],
80
- );
81
-
82
- return (
83
- <PanelAppToolbar
84
- appTitle={entry?.title ?? appTitle}
85
- entry={entry}
86
- onRefresh={onRefresh}
87
- />
88
- );
89
- }