@nextclaw/ui 0.12.35-beta.1 → 0.12.35-beta.3

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 (78) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/api-D3uoaOR8.js +15 -0
  3. package/dist/assets/{book-open-825Pcd0z.js → book-open--T9dJHnp.js} +1 -1
  4. package/dist/assets/channels-list-page-DHmz9iHg.js +8 -0
  5. package/dist/assets/{chat-page-RsUTK_bY.js → chat-page-DOKLoiqI.js} +2 -2
  6. package/dist/assets/{config-split-page-BvfxFQSb.js → config-split-page-Dutq3R5o.js} +1 -1
  7. package/dist/assets/{createLucideIcon-CeNHY6BW.js → createLucideIcon-Cwr2o6Zq.js} +1 -1
  8. package/dist/assets/{desktop-update-config-B_YIv5ZJ.js → desktop-update-config-ClbDsIOs.js} +1 -1
  9. package/dist/assets/{dist-uH7tiW6E.js → dist-e5WATjIW.js} +1 -1
  10. package/dist/assets/doc-browser-ChY6hMn8.js +1 -0
  11. package/dist/assets/doc-browser-cR1vWRs1.js +1 -0
  12. package/dist/assets/{doc-browser-context-CQAx_92e.js → doc-browser-context-CLczyX2Z.js} +1 -1
  13. package/dist/assets/{es2015-c9XYpli5.js → es2015-BJw-hrex.js} +1 -1
  14. package/dist/assets/{external-link-BE1deZ_c.js → external-link-CZ9bQY0L.js} +1 -1
  15. package/dist/assets/i18n-tWKGEGxB.js +1 -0
  16. package/dist/assets/{index-BjH_p3R6.js → index-DPz9DRZN.js} +44 -44
  17. package/dist/assets/index-HvJDzfSU.css +1 -0
  18. package/dist/assets/{key-round-CHo3gqpO.js → key-round-B8tTb3ZJ.js} +1 -1
  19. package/dist/assets/{mcp-marketplace-page-0C214hGi.js → mcp-marketplace-page-BYPxLdUR.js} +2 -2
  20. package/dist/assets/mcp-marketplace-page-DXCvk7wJ.js +1 -0
  21. package/dist/assets/model-config-DcnC_fSr.js +1 -0
  22. package/dist/assets/{notice-card-DO33_LzD.js → notice-card-DUDbOvqx.js} +1 -1
  23. package/dist/assets/play-CV_kwi9X.js +1 -0
  24. package/dist/assets/plus-BqfNq7Tu.js +1 -0
  25. package/dist/assets/{provider-scoped-model-input-B0_r_4EY.js → provider-scoped-model-input-eO4XY0iY.js} +1 -1
  26. package/dist/assets/providers-list-BgvjRVmp.js +1 -0
  27. package/dist/assets/remote-BCuZ1JJe.js +1 -0
  28. package/dist/assets/{rotate-cw-CYUByuqo.js → rotate-cw-BKEhWuWp.js} +1 -1
  29. package/dist/assets/runtime-config-page-B2XPXHrk.js +1 -0
  30. package/dist/assets/{save-B6Ga8JIq.js → save-B6wbBLkU.js} +1 -1
  31. package/dist/assets/{search-5kKpgNoP.js → search-Czito3Dk.js} +1 -1
  32. package/dist/assets/search-config-BFmG4_F7.js +1 -0
  33. package/dist/assets/{secrets-config-CrXA11bo.js → secrets-config-4uY5sk-V.js} +2 -2
  34. package/dist/assets/{select-C0hGOCOK.js → select-CmmDgG8o.js} +1 -1
  35. package/dist/assets/{setting-row-xsM_0nTd.js → setting-row-BUO6FeN1.js} +1 -1
  36. package/dist/assets/{tabs-custom-Bnqfhe4p.js → tabs-custom-D3IZL9WX.js} +1 -1
  37. package/dist/assets/{tag-chip-cktJJMzh.js → tag-chip-Dm_pOtE3.js} +1 -1
  38. package/dist/assets/x-DBP3WiC9.js +1 -0
  39. package/dist/index.html +17 -17
  40. package/package.json +9 -9
  41. package/src/app/components/layout/app-layout.tsx +3 -2
  42. package/src/features/chat/components/chat-session-workspace-panel.tsx +1 -1
  43. package/src/features/chat/components/layout/chat-sidebar-read-state.test.tsx +145 -0
  44. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +16 -0
  45. package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
  46. package/src/features/panel-apps/components/panel-app-list-item.tsx +83 -0
  47. package/src/features/panel-apps/components/panel-app-toolbar.tsx +34 -0
  48. package/src/features/panel-apps/components/panel-apps-list.tsx +109 -0
  49. package/src/features/panel-apps/hooks/use-panel-apps.ts +39 -0
  50. package/src/features/panel-apps/index.ts +4 -0
  51. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +56 -0
  52. package/src/features/panel-apps/utils/panel-app-view.utils.ts +43 -0
  53. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
  54. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
  55. package/src/shared/components/doc-browser/doc-browser-context.tsx +7 -7
  56. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +201 -0
  57. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +19 -0
  58. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +67 -0
  59. package/src/shared/components/doc-browser/doc-browser.test.tsx +82 -2
  60. package/src/shared/components/doc-browser/doc-browser.tsx +205 -279
  61. package/src/shared/lib/api/types.ts +24 -0
  62. package/src/shared/lib/i18n/index.ts +2 -12
  63. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +29 -0
  64. package/dist/assets/api-De1PAch0.js +0 -15
  65. package/dist/assets/channels-list-page-CGjXDSD-.js +0 -8
  66. package/dist/assets/doc-browser-CDe9yz9n.js +0 -1
  67. package/dist/assets/doc-browser-DwT64GuF.js +0 -1
  68. package/dist/assets/i18n-DnubSHoK.js +0 -1
  69. package/dist/assets/index-6WZ6JjaU.css +0 -1
  70. package/dist/assets/mcp-marketplace-page-amWcMncu.js +0 -1
  71. package/dist/assets/model-config-B8zNrkag.js +0 -1
  72. package/dist/assets/play-CqlWbVNo.js +0 -1
  73. package/dist/assets/plus-p61tQh2-.js +0 -1
  74. package/dist/assets/providers-list-B6v3foi7.js +0 -1
  75. package/dist/assets/remote-k671rLmY.js +0 -1
  76. package/dist/assets/runtime-config-page-DgH-XJwK.js +0 -1
  77. package/dist/assets/search-config-DPZ6d9y6.js +0 -1
  78. package/dist/assets/x-DDQw-C2x.js +0 -1
@@ -30,6 +30,7 @@ import {
30
30
  ChatSidebarMobileToolbar,
31
31
  } from '@/features/chat/components/layout/chat-sidebar-toolbar';
32
32
  import { ChatSidebarUtilityMenu } from '@/features/chat/components/layout/chat-sidebar-utility-menu';
33
+ import { openPanelApps } from '@/features/panel-apps';
33
34
  import { isWindowsDesktopHost } from '@/platforms/desktop';
34
35
 
35
36
  type DateGroup = {
@@ -158,7 +159,7 @@ function useChatSessionUnreadState(
158
159
  return;
159
160
  }
160
161
  const selectedItem = items.find(({ session }) => session.key === selectedSessionKey);
161
- if (!selectedItem) {
162
+ if (!selectedItem || selectedItem.runStatus === 'running') {
162
163
  return;
163
164
  }
164
165
  const { session: selectedSession } = selectedItem;
@@ -378,7 +379,7 @@ export function ChatSidebar({
378
379
  currentLanguageLabel={currentLanguageLabel}
379
380
  languageOptions={utilityLanguageOptions}
380
381
  onSelectLanguage={handleLanguageSwitch}
381
- onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
382
+ onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })} onOpenPanelApps={() => openPanelApps(docBrowser)}
382
383
  />
383
384
  </div>
384
385
  ) : null}
@@ -0,0 +1,83 @@
1
+ import type { MouseEvent } from 'react';
2
+ import { AppWindow, Star } from 'lucide-react';
3
+ import type { PanelAppEntryView } from '@/shared/lib/api';
4
+ import { formatDateTime, t } from '@/shared/lib/i18n';
5
+
6
+ export function PanelAppListItem({
7
+ entry,
8
+ favoritePending,
9
+ onOpen,
10
+ onToggleFavorite,
11
+ }: {
12
+ entry: PanelAppEntryView;
13
+ favoritePending: boolean;
14
+ onOpen: () => void;
15
+ onToggleFavorite: () => void;
16
+ }) {
17
+ const favoriteLabel = entry.favorite ? t('panelAppsUnfavorite') : t('panelAppsFavorite');
18
+ const secondary = entry.lastOpenedAt
19
+ ? `${t('panelAppsLastOpened')} ${formatDateTime(entry.lastOpenedAt)}`
20
+ : `${t('panelAppsUpdated')} ${formatDateTime(entry.updatedAt)}`;
21
+ const handleFavorite = (event: MouseEvent<HTMLButtonElement>) => {
22
+ event.stopPropagation();
23
+ onToggleFavorite();
24
+ };
25
+
26
+ return (
27
+ <div className="group flex w-full min-w-0 items-start gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-2.5 transition-colors hover:border-amber-200 hover:bg-amber-50/50">
28
+ <button
29
+ type="button"
30
+ onClick={onOpen}
31
+ className="flex min-w-0 flex-1 items-start gap-3 text-left"
32
+ >
33
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-50 text-base text-amber-700">
34
+ <PanelAppIcon icon={entry.icon} title={entry.title} />
35
+ </span>
36
+ <span className="min-w-0 flex-1">
37
+ <span className="block truncate text-sm font-medium text-gray-900">{entry.title}</span>
38
+ {entry.description ? (
39
+ <span className="mt-0.5 block line-clamp-2 text-xs leading-5 text-gray-600">{entry.description}</span>
40
+ ) : null}
41
+ <span className="mt-1 block truncate text-xs text-gray-400">{secondary}</span>
42
+ </span>
43
+ </button>
44
+ <button
45
+ type="button"
46
+ onClick={handleFavorite}
47
+ disabled={favoritePending}
48
+ className="rounded-md p-1.5 text-gray-400 transition-colors hover:bg-white hover:text-amber-500 disabled:opacity-50"
49
+ title={favoriteLabel}
50
+ aria-label={favoriteLabel}
51
+ >
52
+ <Star className={entry.favorite ? 'h-4 w-4 fill-amber-400 text-amber-500' : 'h-4 w-4'} />
53
+ </button>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function PanelAppIcon({ icon, title }: { icon?: string; title: string }) {
59
+ if (!icon) {
60
+ return <AppWindow className="h-4 w-4" />;
61
+ }
62
+ if (isImageIcon(icon)) {
63
+ return (
64
+ <img
65
+ src={icon}
66
+ alt=""
67
+ aria-hidden="true"
68
+ className="h-5 w-5 rounded-sm object-contain"
69
+ title={title}
70
+ />
71
+ );
72
+ }
73
+ return <span className="max-w-6 truncate text-center leading-none">{icon}</span>;
74
+ }
75
+
76
+ function isImageIcon(icon: string): boolean {
77
+ return (
78
+ icon.startsWith('data:image/') ||
79
+ icon.startsWith('http://') ||
80
+ icon.startsWith('https://') ||
81
+ icon.startsWith('/')
82
+ );
83
+ }
@@ -0,0 +1,34 @@
1
+ import { ArrowLeft, RefreshCw } from 'lucide-react';
2
+ import { t } from '@/shared/lib/i18n';
3
+
4
+ type PanelAppToolbarProps = {
5
+ onOpenPanelApps: () => void;
6
+ onRefresh: () => void;
7
+ };
8
+
9
+ export function PanelAppToolbar({
10
+ onOpenPanelApps,
11
+ onRefresh,
12
+ }: PanelAppToolbarProps) {
13
+ return (
14
+ <div className="flex items-center justify-between gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
15
+ <button
16
+ type="button"
17
+ onClick={onOpenPanelApps}
18
+ className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
19
+ >
20
+ <ArrowLeft className="h-3.5 w-3.5" />
21
+ {t('panelAppsTitle')}
22
+ </button>
23
+ <button
24
+ type="button"
25
+ onClick={onRefresh}
26
+ className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800"
27
+ title={t('panelAppsRefreshCurrent')}
28
+ aria-label={t('panelAppsRefreshCurrent')}
29
+ >
30
+ <RefreshCw className="h-3.5 w-3.5" />
31
+ </button>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,109 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { HelpCircle, RefreshCw } from 'lucide-react';
3
+ import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
4
+ import { usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
5
+ import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
6
+ import type { PanelAppViewMode } from '@/features/panel-apps/utils/panel-app-view.utils';
7
+ import type { PanelAppEntryView } from '@/shared/lib/api';
8
+ import { Tabs, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
9
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shared/components/ui/tooltip';
10
+ import { t } from '@/shared/lib/i18n';
11
+
12
+ export function PanelAppsList({
13
+ onOpenPanelApp,
14
+ }: {
15
+ onOpenPanelApp: (entry: PanelAppEntryView) => void;
16
+ }) {
17
+ const panelApps = usePanelApps();
18
+ const updatePreferences = useUpdatePanelAppPreferences();
19
+ const recordOpened = useRecordPanelAppOpened();
20
+ const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
21
+ const entries = useMemo(
22
+ () => getPanelAppViewEntries(panelApps.data?.entries ?? [], viewMode),
23
+ [panelApps.data?.entries, viewMode],
24
+ );
25
+
26
+ const openPanelApp = async (entry: PanelAppEntryView) => {
27
+ try {
28
+ onOpenPanelApp(await recordOpened.mutateAsync(entry.id));
29
+ } catch {
30
+ onOpenPanelApp(entry);
31
+ }
32
+ };
33
+
34
+ const toggleFavorite = (entry: PanelAppEntryView) => {
35
+ updatePreferences.mutate({
36
+ id: entry.id,
37
+ preferences: { favorite: !entry.favorite },
38
+ });
39
+ };
40
+
41
+ if (panelApps.isLoading) {
42
+ return (
43
+ <div className="flex h-full items-center justify-center text-sm text-gray-500">{t('panelAppsLoading')}</div>
44
+ );
45
+ }
46
+
47
+ if (panelApps.isError) {
48
+ return (
49
+ <div className="p-4 text-sm text-rose-600">{panelApps.error instanceof Error ? panelApps.error.message : t('panelAppsLoadFailed')}</div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div className="flex h-full min-h-0 flex-col bg-white">
55
+ <div className="flex items-center justify-between gap-2 border-b border-gray-100 px-4 py-3">
56
+ <div className="flex min-w-0 items-center gap-1.5">
57
+ <div className="truncate text-sm font-semibold text-gray-900">{t('panelAppsTitle')}</div>
58
+ {panelApps.data?.panelsPath ? (
59
+ <TooltipProvider delayDuration={250}>
60
+ <Tooltip>
61
+ <TooltipTrigger asChild>
62
+ <button type="button" className="rounded p-0.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700" aria-label={t('panelAppsTitle')}><HelpCircle className="h-3.5 w-3.5" /></button>
63
+ </TooltipTrigger>
64
+ <TooltipContent side="bottom" className="max-w-[320px] break-all font-mono text-xs">{panelApps.data.panelsPath}</TooltipContent>
65
+ </Tooltip>
66
+ </TooltipProvider>
67
+ ) : null}
68
+ </div>
69
+ <button
70
+ type="button"
71
+ onClick={() => void panelApps.refetch()}
72
+ className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800"
73
+ title={t('panelAppsRefresh')}
74
+ aria-label={t('panelAppsRefresh')}
75
+ >
76
+ <RefreshCw className="h-3.5 w-3.5" />
77
+ </button>
78
+ </div>
79
+ <div className="border-b border-gray-100 px-3 py-2">
80
+ <Tabs value={viewMode} onValueChange={(value) => setViewMode(value as PanelAppViewMode)}>
81
+ <TabsList className="grid h-auto w-full grid-cols-5 rounded-lg bg-gray-100/70 p-0.5">
82
+ <TabsTrigger value="smart" className="px-2 py-1 text-xs">{t('panelAppsSortSmart')}</TabsTrigger>
83
+ <TabsTrigger value="favorites" className="px-2 py-1 text-xs">{t('panelAppsFavorites')}</TabsTrigger>
84
+ <TabsTrigger value="recent-open" className="px-2 py-1 text-xs">{t('panelAppsSortRecentOpen')}</TabsTrigger>
85
+ <TabsTrigger value="updated" className="px-2 py-1 text-xs">{t('panelAppsSortUpdated')}</TabsTrigger>
86
+ <TabsTrigger value="name" className="px-2 py-1 text-xs">{t('panelAppsSortName')}</TabsTrigger>
87
+ </TabsList>
88
+ </Tabs>
89
+ </div>
90
+ {entries.length === 0 ? (
91
+ <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-gray-500">{t('panelAppsEmpty')}</div>
92
+ ) : (
93
+ <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
94
+ <div className="space-y-1.5">
95
+ {entries.map((entry) => (
96
+ <PanelAppListItem
97
+ key={entry.id}
98
+ entry={entry}
99
+ favoritePending={updatePreferences.isPending}
100
+ onOpen={() => void openPanelApp(entry)}
101
+ onToggleFavorite={() => toggleFavorite(entry)}
102
+ />
103
+ ))}
104
+ </div>
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,39 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { nextclawClient } from '@/shared/lib/api';
3
+ import type { PanelAppPreferencesUpdateView } from '@/shared/lib/api';
4
+
5
+ const PANEL_APPS_QUERY_KEY = ['panel-apps'] as const;
6
+
7
+ export function usePanelApps() {
8
+ return useQuery({
9
+ queryKey: PANEL_APPS_QUERY_KEY,
10
+ queryFn: () => nextclawClient.panelApps.listPanelApps(),
11
+ staleTime: 0,
12
+ });
13
+ }
14
+
15
+ export function useUpdatePanelAppPreferences() {
16
+ const queryClient = useQueryClient();
17
+ return useMutation({
18
+ mutationFn: ({
19
+ id,
20
+ preferences,
21
+ }: {
22
+ id: string;
23
+ preferences: PanelAppPreferencesUpdateView;
24
+ }) => nextclawClient.panelApps.updatePanelAppPreferences(id, preferences),
25
+ onSuccess: () => {
26
+ void queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY });
27
+ },
28
+ });
29
+ }
30
+
31
+ export function useRecordPanelAppOpened() {
32
+ const queryClient = useQueryClient();
33
+ return useMutation({
34
+ mutationFn: (id: string) => nextclawClient.panelApps.recordPanelAppOpened(id),
35
+ onSuccess: () => {
36
+ void queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY });
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ openPanelApps,
3
+ PANEL_APPS_DOC_BROWSER_RENDERERS,
4
+ } from './utils/panel-app-doc-browser.utils';
@@ -0,0 +1,56 @@
1
+ import { AppWindow } from 'lucide-react';
2
+ import type { DocBrowserContextValue } from '@/shared/components/doc-browser/doc-browser-context';
3
+ import type { DocBrowserCustomTabRenderers } from '@/shared/components/doc-browser/doc-browser-renderer.types';
4
+ import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
5
+ import { PanelAppsList } from '@/features/panel-apps/components/panel-apps-list';
6
+ import { t } from '@/shared/lib/i18n';
7
+
8
+ export const PANEL_APPS_LIST_TAB_KIND = 'panel-apps';
9
+ export const PANEL_APP_TAB_KIND = 'panel-app';
10
+ const PANEL_APPS_LIST_URL = 'nextclaw://panel-apps';
11
+ const PANEL_APP_IFRAME_SANDBOX = [
12
+ 'allow-same-origin',
13
+ 'allow-scripts',
14
+ 'allow-forms',
15
+ 'allow-modals',
16
+ 'allow-popups',
17
+ 'allow-popups-to-escape-sandbox',
18
+ 'allow-downloads',
19
+ 'allow-pointer-lock',
20
+ 'allow-presentation',
21
+ ].join(' ');
22
+
23
+ export function openPanelApps(docBrowser: Pick<DocBrowserContextValue, 'open'>): void {
24
+ docBrowser.open(PANEL_APPS_LIST_URL, {
25
+ kind: PANEL_APPS_LIST_TAB_KIND,
26
+ title: t('panelAppsTitle'),
27
+ dedupeKey: 'panel-apps:list',
28
+ });
29
+ }
30
+
31
+ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
32
+ [PANEL_APPS_LIST_TAB_KIND]: {
33
+ getTitle: () => t('panelAppsTitle'),
34
+ renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
35
+ renderContent: ({ open }) => (
36
+ <PanelAppsList
37
+ onOpenPanelApp={(entry) => open(entry.contentPath, {
38
+ kind: PANEL_APP_TAB_KIND,
39
+ title: entry.title,
40
+ dedupeKey: `panel-app:${entry.id}`,
41
+ })}
42
+ />
43
+ ),
44
+ },
45
+ [PANEL_APP_TAB_KIND]: {
46
+ getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
47
+ getTitle: (tab) => tab.title || t('panelAppsTitle'),
48
+ renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
49
+ renderToolbar: ({ open, refreshIframe }) => (
50
+ <PanelAppToolbar
51
+ onOpenPanelApps={() => openPanelApps({ open })}
52
+ onRefresh={refreshIframe}
53
+ />
54
+ ),
55
+ },
56
+ };
@@ -0,0 +1,43 @@
1
+ import type { PanelAppEntryView } from '@/shared/lib/api';
2
+
3
+ export type PanelAppViewMode = 'smart' | 'favorites' | 'recent-open' | 'updated' | 'name';
4
+
5
+ export function getPanelAppViewEntries(entries: PanelAppEntryView[], mode: PanelAppViewMode): PanelAppEntryView[] {
6
+ if (mode === 'favorites') {
7
+ return sortPanelApps(entries.filter((entry) => entry.favorite), 'smart');
8
+ }
9
+ if (mode === 'recent-open') {
10
+ return sortPanelApps(entries.filter((entry) => entry.lastOpenedAt), mode);
11
+ }
12
+ return sortPanelApps(entries, mode);
13
+ }
14
+
15
+ function sortPanelApps(entries: PanelAppEntryView[], mode: Exclude<PanelAppViewMode, 'favorites'>): PanelAppEntryView[] {
16
+ return [...entries].sort((left, right) => comparePanelApps(left, right, mode));
17
+ }
18
+
19
+ function comparePanelApps(left: PanelAppEntryView, right: PanelAppEntryView, mode: Exclude<PanelAppViewMode, 'favorites'>): number {
20
+ if (mode === 'recent-open') {
21
+ return compareIsoDesc(left.lastOpenedAt, right.lastOpenedAt) || compareByName(left, right);
22
+ }
23
+ if (mode === 'updated') {
24
+ return compareIsoDesc(left.updatedAt, right.updatedAt) || compareByName(left, right);
25
+ }
26
+ if (mode === 'name') {
27
+ return compareByName(left, right);
28
+ }
29
+ return (
30
+ Number(right.favorite) - Number(left.favorite) ||
31
+ compareIsoDesc(left.lastOpenedAt, right.lastOpenedAt) ||
32
+ compareIsoDesc(left.updatedAt, right.updatedAt) ||
33
+ compareByName(left, right)
34
+ );
35
+ }
36
+
37
+ function compareByName(left: PanelAppEntryView, right: PanelAppEntryView): number {
38
+ return left.title.localeCompare(right.title);
39
+ }
40
+
41
+ function compareIsoDesc(left?: string, right?: string): number {
42
+ return new Date(right ?? 0).getTime() - new Date(left ?? 0).getTime();
43
+ }
@@ -8,6 +8,7 @@ import { Sidebar } from "@/app/components/layout/sidebar";
8
8
  import { DesktopWindowChrome } from "@/platforms/desktop/components/desktop-window-chrome";
9
9
  import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.utils";
10
10
  import { MobileBottomNav } from "@/platforms/mobile";
11
+ import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
11
12
  import { cn } from "@/shared/lib/utils";
12
13
 
13
14
  const DocBrowser = lazy(async () => ({
@@ -19,6 +20,7 @@ type DesktopAppShellProps = {
19
20
  isMobileLayout?: boolean;
20
21
  isDocBrowserOpen: boolean;
21
22
  docBrowserMode: "floating" | "docked";
23
+ docBrowserRenderers?: DocBrowserCustomTabRenderers;
22
24
  children: React.ReactNode;
23
25
  };
24
26
 
@@ -27,6 +29,7 @@ export function DesktopAppShell({
27
29
  isMobileLayout = false,
28
30
  isDocBrowserOpen,
29
31
  docBrowserMode,
32
+ docBrowserRenderers = {},
30
33
  children,
31
34
  }: DesktopAppShellProps) {
32
35
  const isMainRoute = isMainWorkspaceRoute(pathname);
@@ -62,7 +65,7 @@ export function DesktopAppShell({
62
65
  </div>
63
66
  {isDocBrowserOpen && docBrowserMode === "docked" ? (
64
67
  <Suspense fallback={null}>
65
- <DocBrowser />
68
+ <DocBrowser customTabRenderers={docBrowserRenderers} />
66
69
  </Suspense>
67
70
  ) : null}
68
71
  </div>
@@ -70,7 +73,7 @@ export function DesktopAppShell({
70
73
  {showMobileBottomNav ? <MobileBottomNav /> : null}
71
74
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
72
75
  <Suspense fallback={null}>
73
- <DocBrowser />
76
+ <DocBrowser customTabRenderers={docBrowserRenderers} />
74
77
  </Suspense>
75
78
  ) : null}
76
79
  </div>
@@ -5,6 +5,7 @@ import {
5
5
  } from "@/app/configs/app-navigation.config";
6
6
  import { MobileBottomNav } from "@/platforms/mobile/components/mobile-bottom-nav";
7
7
  import { MobileTopbar } from "@/platforms/mobile/components/mobile-topbar";
8
+ import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
8
9
 
9
10
  const DocBrowser = lazy(async () => ({
10
11
  default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
@@ -13,6 +14,7 @@ const DocBrowser = lazy(async () => ({
13
14
  type MobileAppShellProps = {
14
15
  pathname: string;
15
16
  isDocBrowserOpen: boolean;
17
+ docBrowserRenderers?: DocBrowserCustomTabRenderers;
16
18
  topbarLeadingInset?: string;
17
19
  children: React.ReactNode;
18
20
  };
@@ -20,6 +22,7 @@ type MobileAppShellProps = {
20
22
  export function MobileAppShell({
21
23
  pathname,
22
24
  isDocBrowserOpen,
25
+ docBrowserRenderers = {},
23
26
  topbarLeadingInset,
24
27
  children,
25
28
  }: MobileAppShellProps) {
@@ -42,7 +45,7 @@ export function MobileAppShell({
42
45
  {showBottomNav ? <MobileBottomNav /> : null}
43
46
  {isDocBrowserOpen ? (
44
47
  <Suspense fallback={null}>
45
- <DocBrowser displayMode="fullscreen" />
48
+ <DocBrowser customTabRenderers={docBrowserRenderers} displayMode="fullscreen" />
46
49
  </Suspense>
47
50
  ) : null}
48
51
  </div>
@@ -14,7 +14,7 @@ export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
14
14
  const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
15
15
 
16
16
  export type DocBrowserMode = 'floating' | 'docked';
17
- export type DocBrowserTabKind = 'docs' | 'content';
17
+ export type DocBrowserTabKind = 'docs' | 'content' | (string & {});
18
18
 
19
19
  export type DocBrowserTab = {
20
20
  id: string;
@@ -55,7 +55,7 @@ interface DocBrowserActions {
55
55
  setActiveTab: (tabId: string) => void;
56
56
  }
57
57
 
58
- type DocBrowserContextValue = DocBrowserState & DocBrowserActions & {
58
+ export type DocBrowserContextValue = DocBrowserState & DocBrowserActions & {
59
59
  currentTab?: DocBrowserTab;
60
60
  };
61
61
 
@@ -224,15 +224,15 @@ function resolveOpenTargetUrl(params: {
224
224
  kind: DocBrowserTabKind;
225
225
  activeTab?: DocBrowserTab;
226
226
  }): string {
227
- if (params.url && params.url.trim().length > 0) {
228
- return normalizeUrlByKind(params.url, params.kind);
227
+ const { activeTab, kind, url } = params;
228
+ if (url && url.trim().length > 0) {
229
+ return normalizeUrlByKind(url, kind);
229
230
  }
230
231
 
231
- if (params.kind === 'docs') {
232
+ if (kind === 'docs') {
232
233
  return getDefaultDocsUrl();
233
234
  }
234
-
235
- return params.activeTab?.currentUrl ?? getDefaultDocsUrl();
235
+ return activeTab?.currentUrl ?? getDefaultDocsUrl();
236
236
  }
237
237
 
238
238
  function openDocBrowserState(