@nextclaw/ui 0.12.35-beta.1 → 0.12.35-beta.2
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/CHANGELOG.md +64 -0
- package/dist/assets/api-D3uoaOR8.js +15 -0
- package/dist/assets/{book-open-825Pcd0z.js → book-open--T9dJHnp.js} +1 -1
- package/dist/assets/channels-list-page-CzDMCXR_.js +8 -0
- package/dist/assets/{chat-page-RsUTK_bY.js → chat-page-Doi-sasO.js} +2 -2
- package/dist/assets/{config-split-page-BvfxFQSb.js → config-split-page-Dutq3R5o.js} +1 -1
- package/dist/assets/{createLucideIcon-CeNHY6BW.js → createLucideIcon-Cwr2o6Zq.js} +1 -1
- package/dist/assets/{desktop-update-config-B_YIv5ZJ.js → desktop-update-config-Dd4DdPH3.js} +1 -1
- package/dist/assets/{dist-uH7tiW6E.js → dist-e5WATjIW.js} +1 -1
- package/dist/assets/doc-browser-DInzGY0W.js +1 -0
- package/dist/assets/doc-browser-DxVvmYQk.js +1 -0
- package/dist/assets/{doc-browser-context-CQAx_92e.js → doc-browser-context-CLczyX2Z.js} +1 -1
- package/dist/assets/{es2015-c9XYpli5.js → es2015-BJw-hrex.js} +1 -1
- package/dist/assets/{external-link-BE1deZ_c.js → external-link-CZ9bQY0L.js} +1 -1
- package/dist/assets/i18n-tWKGEGxB.js +1 -0
- package/dist/assets/index-HvJDzfSU.css +1 -0
- package/dist/assets/{index-BjH_p3R6.js → index-dHwM4pEn.js} +44 -44
- package/dist/assets/{key-round-CHo3gqpO.js → key-round-B8tTb3ZJ.js} +1 -1
- package/dist/assets/mcp-marketplace-page-D-dydvbL.js +1 -0
- package/dist/assets/{mcp-marketplace-page-0C214hGi.js → mcp-marketplace-page-n9Z25Cyk.js} +2 -2
- package/dist/assets/model-config-8BVoSVXQ.js +1 -0
- package/dist/assets/{notice-card-DO33_LzD.js → notice-card-DUDbOvqx.js} +1 -1
- package/dist/assets/play-CV_kwi9X.js +1 -0
- package/dist/assets/plus-BqfNq7Tu.js +1 -0
- package/dist/assets/{provider-scoped-model-input-B0_r_4EY.js → provider-scoped-model-input-eO4XY0iY.js} +1 -1
- package/dist/assets/providers-list-CjJmW-od.js +1 -0
- package/dist/assets/remote-DxTFnQm8.js +1 -0
- package/dist/assets/{rotate-cw-CYUByuqo.js → rotate-cw-BKEhWuWp.js} +1 -1
- package/dist/assets/runtime-config-page-Ba6atCMq.js +1 -0
- package/dist/assets/{save-B6Ga8JIq.js → save-B6wbBLkU.js} +1 -1
- package/dist/assets/{search-5kKpgNoP.js → search-Czito3Dk.js} +1 -1
- package/dist/assets/search-config-BtCNY431.js +1 -0
- package/dist/assets/{secrets-config-CrXA11bo.js → secrets-config-Ddh05-J5.js} +2 -2
- package/dist/assets/{select-C0hGOCOK.js → select-CmmDgG8o.js} +1 -1
- package/dist/assets/{setting-row-xsM_0nTd.js → setting-row-BUO6FeN1.js} +1 -1
- package/dist/assets/{tabs-custom-Bnqfhe4p.js → tabs-custom-D3IZL9WX.js} +1 -1
- package/dist/assets/{tag-chip-cktJJMzh.js → tag-chip-Dm_pOtE3.js} +1 -1
- package/dist/assets/x-DBP3WiC9.js +1 -0
- package/dist/index.html +17 -17
- package/package.json +9 -9
- package/src/app/components/layout/app-layout.tsx +3 -2
- package/src/features/chat/components/chat-session-workspace-panel.tsx +1 -1
- package/src/features/chat/components/layout/chat-sidebar-read-state.test.tsx +145 -0
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +16 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
- package/src/features/panel-apps/components/panel-app-list-item.tsx +83 -0
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +34 -0
- package/src/features/panel-apps/components/panel-apps-list.tsx +109 -0
- package/src/features/panel-apps/hooks/use-panel-apps.ts +39 -0
- package/src/features/panel-apps/index.ts +4 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +56 -0
- package/src/features/panel-apps/utils/panel-app-view.utils.ts +43 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
- package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
- package/src/shared/components/doc-browser/doc-browser-context.tsx +7 -7
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +201 -0
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +19 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +67 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +74 -145
- package/src/shared/lib/api/types.ts +24 -0
- package/src/shared/lib/i18n/index.ts +2 -12
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +29 -0
- package/dist/assets/api-De1PAch0.js +0 -15
- package/dist/assets/channels-list-page-CGjXDSD-.js +0 -8
- package/dist/assets/doc-browser-CDe9yz9n.js +0 -1
- package/dist/assets/doc-browser-DwT64GuF.js +0 -1
- package/dist/assets/i18n-DnubSHoK.js +0 -1
- package/dist/assets/index-6WZ6JjaU.css +0 -1
- package/dist/assets/mcp-marketplace-page-amWcMncu.js +0 -1
- package/dist/assets/model-config-B8zNrkag.js +0 -1
- package/dist/assets/play-CqlWbVNo.js +0 -1
- package/dist/assets/plus-p61tQh2-.js +0 -1
- package/dist/assets/providers-list-B6v3foi7.js +0 -1
- package/dist/assets/remote-k671rLmY.js +0 -1
- package/dist/assets/runtime-config-page-DgH-XJwK.js +0 -1
- package/dist/assets/search-config-DPZ6d9y6.js +0 -1
- package/dist/assets/x-DDQw-C2x.js +0 -1
|
@@ -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,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
|
-
|
|
228
|
-
|
|
227
|
+
const { activeTab, kind, url } = params;
|
|
228
|
+
if (url && url.trim().length > 0) {
|
|
229
|
+
return normalizeUrlByKind(url, kind);
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
if (
|
|
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(
|