@nextclaw/ui 0.15.0 → 0.15.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 +50 -0
- package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/chat-page-C_5dcuE3.js +1 -0
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
- package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
- package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
- package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
- package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
- package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
- package/dist/assets/index-CvrvblgX.css +1 -0
- package/dist/assets/index-Obd4JYU2.js +104 -0
- package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
- package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
- package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
- package/dist/assets/plus-Bx7wQbhG.js +1 -0
- package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
- package/dist/assets/remote-CDkHZtA8.js +1 -0
- package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
- package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
- package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
- package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
- package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
- package/dist/assets/security-config-B_gCq9fn.js +1 -0
- package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/settings-2-2DWauOjY.js +1 -0
- package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
- package/dist/assets/x-DT5x45JU.js +1 -0
- package/dist/index.html +38 -30
- package/index.html +12 -4
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
- package/src/app/components/layout/sidebar-items.tsx +43 -4
- package/src/app/components/layout/sidebar.tsx +83 -61
- package/src/app/configs/app-navigation.config.ts +49 -0
- package/src/app/styles/design-system.css +82 -0
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
- package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- package/src/features/chat/managers/chat-thread.manager.ts +8 -2
- package/src/features/chat/stores/chat-session-list.store.ts +28 -5
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
- package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
- package/src/features/panel-apps/index.ts +1 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
- package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
- package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
- package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
- package/src/features/settings/components/config/provider-form.tsx +11 -8
- package/src/features/settings/components/config/provider-models-section.tsx +7 -7
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
- package/src/features/settings/pages/model-config-page.tsx +26 -25
- package/src/features/settings/pages/providers-config-page.tsx +10 -13
- package/src/features/settings/pages/search-config-page.tsx +8 -5
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/index.css +18 -0
- package/src/shared/components/config-split-page.tsx +6 -6
- package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
- package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
- package/src/shared/components/ui/actions/form-actions.tsx +25 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
- package/src/shared/components/ui/card.tsx +12 -4
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
- package/src/shared/hooks/use-server-path-browse.ts +26 -4
- package/src/shared/hooks/use-server-path-read.ts +11 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
- package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
- package/src/shared/lib/theme/index.test.ts +21 -0
- package/src/shared/lib/theme/index.ts +6 -4
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
- package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
- package/dist/assets/config-split-page-EidRBpP2.js +0 -1
- package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
- package/dist/assets/doc-browser-B76XOXmW.js +0 -1
- package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
- package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
- package/dist/assets/ellipsis-2uzhypw5.js +0 -1
- package/dist/assets/index-CBRP-7-a.css +0 -1
- package/dist/assets/index-Z1fYpbNd.js +0 -104
- package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
- package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
- package/dist/assets/plus-3ztRZD_l.js +0 -1
- package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
- package/dist/assets/remote-CKWZfVVB.js +0 -1
- package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
- package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
- package/dist/assets/search-config-page-Dm8daYny.js +0 -1
- package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
- package/dist/assets/security-config-Ds0LsJmo.js +0 -1
- package/dist/assets/x-DqL1mYLC.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { UpdateSnapshot } from '@nextclaw/shared';
|
|
2
|
+
|
|
3
|
+
export type ReleaseNotesLocale = 'zh-CN' | 'en-US';
|
|
4
|
+
|
|
5
|
+
export type ReleaseNotesText = Partial<Record<ReleaseNotesLocale, string>>;
|
|
6
|
+
|
|
7
|
+
export type ReleaseNotesItem = {
|
|
8
|
+
title: ReleaseNotesText;
|
|
9
|
+
body?: ReleaseNotesText;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ReleaseNotesSection = {
|
|
13
|
+
kind: string;
|
|
14
|
+
title?: ReleaseNotesText;
|
|
15
|
+
items: ReleaseNotesItem[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ReleaseNotesPayload = {
|
|
19
|
+
title?: ReleaseNotesText;
|
|
20
|
+
summary?: ReleaseNotesText;
|
|
21
|
+
sections: ReleaseNotesSection[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function readReleaseNotesText(
|
|
25
|
+
value: ReleaseNotesText | undefined,
|
|
26
|
+
locale: ReleaseNotesLocale
|
|
27
|
+
): string | null {
|
|
28
|
+
const fallbackLocale = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
|
29
|
+
const text = value?.[locale]?.trim() || value?.[fallbackLocale]?.trim();
|
|
30
|
+
return text || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveReleaseNotesDataUrl(snapshot: UpdateSnapshot): string | null {
|
|
34
|
+
const releaseNotesUrl = snapshot.releaseNotesUrl?.trim();
|
|
35
|
+
const version = (snapshot.availableVersion ?? snapshot.downloadedVersion)?.trim();
|
|
36
|
+
if (!releaseNotesUrl || !version || !/^[0-9A-Za-z.-]+$/.test(version)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return new URL(`/release-notes/nextclaw-v${version}.json`, releaseNotesUrl).toString();
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isReleaseNotesText(value: unknown): value is ReleaseNotesText {
|
|
52
|
+
if (!isRecord(value)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return typeof value['zh-CN'] === 'string' || typeof value['en-US'] === 'string';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isReleaseNotesItem(value: unknown): value is ReleaseNotesItem {
|
|
59
|
+
if (!isRecord(value) || !isReleaseNotesText(value.title)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return value.body === undefined || isReleaseNotesText(value.body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isReleaseNotesSection(value: unknown): value is ReleaseNotesSection {
|
|
66
|
+
return isRecord(value)
|
|
67
|
+
&& typeof value.kind === 'string'
|
|
68
|
+
&& Array.isArray(value.items)
|
|
69
|
+
&& value.items.every(isReleaseNotesItem)
|
|
70
|
+
&& (value.title === undefined || isReleaseNotesText(value.title));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isReleaseNotesPayload(value: unknown): value is ReleaseNotesPayload {
|
|
74
|
+
return isRecord(value)
|
|
75
|
+
&& Array.isArray(value.sections)
|
|
76
|
+
&& value.sections.every(isReleaseNotesSection)
|
|
77
|
+
&& (value.title === undefined || isReleaseNotesText(value.title))
|
|
78
|
+
&& (value.summary === undefined || isReleaseNotesText(value.summary));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function fetchReleaseNotesData(url: string): Promise<ReleaseNotesPayload> {
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
cache: 'no-store',
|
|
84
|
+
headers: { accept: 'application/json' }
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`Release notes request failed with HTTP ${response.status}.`);
|
|
88
|
+
}
|
|
89
|
+
const payload: unknown = await response.json();
|
|
90
|
+
if (!isReleaseNotesPayload(payload)) {
|
|
91
|
+
throw new Error('Release notes payload has an unsupported shape.');
|
|
92
|
+
}
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
package/src/index.css
CHANGED
|
@@ -675,6 +675,24 @@
|
|
|
675
675
|
--md-table-head-bg: rgba(191, 219, 254, 0.16);
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
:root[data-theme="work"] .chat-markdown-assistant {
|
|
679
|
+
--md-link: #1f73c9;
|
|
680
|
+
--md-border: rgba(0, 0, 0, 0.11);
|
|
681
|
+
--md-table-head-bg: rgba(0, 0, 0, 0.045);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
:root[data-theme="work"] .chat-markdown-user {
|
|
685
|
+
--md-text: #212121;
|
|
686
|
+
--md-muted: #737373;
|
|
687
|
+
--md-border: rgba(0, 0, 0, 0.12);
|
|
688
|
+
--md-link: #1f73c9;
|
|
689
|
+
--md-inline-code-bg: rgba(0, 0, 0, 0.06);
|
|
690
|
+
--md-inline-code-color: #212121;
|
|
691
|
+
--md-blockquote-border: rgba(0, 0, 0, 0.2);
|
|
692
|
+
--md-blockquote-bg: rgba(0, 0, 0, 0.04);
|
|
693
|
+
--md-table-head-bg: rgba(0, 0, 0, 0.045);
|
|
694
|
+
}
|
|
695
|
+
|
|
678
696
|
:root[data-theme-appearance="dark"] .chat-markdown-assistant {
|
|
679
697
|
--md-text: hsl(var(--foreground));
|
|
680
698
|
--md-muted: hsl(var(--foreground-tertiary));
|
|
@@ -3,7 +3,7 @@ import { ArrowLeft, type LucideIcon } from "lucide-react";
|
|
|
3
3
|
import { t } from "@/shared/lib/i18n";
|
|
4
4
|
import { cn } from "@/shared/lib/utils";
|
|
5
5
|
|
|
6
|
-
const CARD_CLASS = "min-w-0 overflow-hidden rounded-2xl border border-border bg-card text-card-foreground
|
|
6
|
+
const CARD_CLASS = "min-w-0 overflow-hidden rounded-2xl border border-border/75 bg-card text-card-foreground xl:h-[calc(100vh-180px)] xl:max-h-[860px]";
|
|
7
7
|
type DivProps = HTMLAttributes<HTMLDivElement>; type SectionProps = HTMLAttributes<HTMLElement>;
|
|
8
8
|
|
|
9
9
|
function ConfigSplitPane({ className, ...props }: SectionProps) {
|
|
@@ -102,7 +102,7 @@ export function ConfigSplitPaneBody({
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export function ConfigSplitPaneFooter({ className, ...props }: DivProps) {
|
|
105
|
-
return <div className={cn("shrink-0 border-t border-border/70", className)} {...props} />;
|
|
105
|
+
return <div className={cn("shrink-0 border-t border-border/70 px-4 py-2.5", className)} {...props} />;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export function ConfigSelectionCard({
|
|
@@ -115,10 +115,10 @@ export function ConfigSelectionCard({
|
|
|
115
115
|
<button
|
|
116
116
|
type={type}
|
|
117
117
|
className={cn(
|
|
118
|
-
"w-full rounded-xl border p-2.5 text-left transition-
|
|
118
|
+
"w-full rounded-xl border p-2.5 text-left transition-colors",
|
|
119
119
|
active
|
|
120
|
-
? "border-
|
|
121
|
-
: "border-
|
|
120
|
+
? "border-border/70 bg-muted/65 text-foreground"
|
|
121
|
+
: "border-transparent bg-transparent text-muted-foreground hover:border-border/50 hover:bg-muted/40 hover:text-foreground",
|
|
122
122
|
className,
|
|
123
123
|
)}
|
|
124
124
|
{...props}
|
|
@@ -140,7 +140,7 @@ export function ConfigSplitEmptyState({
|
|
|
140
140
|
return (
|
|
141
141
|
<div
|
|
142
142
|
className={cn(
|
|
143
|
-
"flex min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/
|
|
143
|
+
"flex min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-border/70 bg-muted/35 px-4 py-10 text-center",
|
|
144
144
|
className,
|
|
145
145
|
)}
|
|
146
146
|
{...props}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
async function importDocsUrlUtils(language: 'en' | 'zh' = 'en') {
|
|
4
|
+
const i18n = await import('@/shared/lib/i18n');
|
|
5
|
+
i18n.setLanguage(language);
|
|
6
|
+
return await import('@/shared/components/doc-browser/utils/doc-browser-url.utils');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stubTimeZone(timeZone: string) {
|
|
10
|
+
vi.spyOn(Intl, 'DateTimeFormat').mockReturnValue({
|
|
11
|
+
resolvedOptions: () => ({ timeZone }),
|
|
12
|
+
} as Intl.DateTimeFormat);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('doc browser docs URL utilities', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
stubTimeZone('America/Los_Angeles');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.unstubAllEnvs();
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('uses the official docs origin for English users outside mainland by default', async () => {
|
|
27
|
+
const { getDefaultDocsUrl } = await importDocsUrlUtils('en');
|
|
28
|
+
|
|
29
|
+
expect(getDefaultDocsUrl()).toBe('https://docs.nextclaw.io/en/guide/getting-started');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('uses the mainland mirror for users in the mainland timezone', async () => {
|
|
33
|
+
stubTimeZone('Asia/Shanghai');
|
|
34
|
+
|
|
35
|
+
const { getDefaultDocsUrl } = await importDocsUrlUtils('en');
|
|
36
|
+
|
|
37
|
+
expect(getDefaultDocsUrl()).toBe('https://docs.nextclaw.net/en/guide/getting-started');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('uses the mainland mirror for Chinese users by default', async () => {
|
|
41
|
+
const { getDefaultDocsUrl } = await importDocsUrlUtils('zh');
|
|
42
|
+
|
|
43
|
+
expect(getDefaultDocsUrl()).toBe('https://docs.nextclaw.net/zh/guide/getting-started');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('lets an explicit docs base override the automatic origin', async () => {
|
|
47
|
+
vi.stubEnv('VITE_NEXTCLAW_DOCS_BASE_URL', 'https://docs.example.com/');
|
|
48
|
+
|
|
49
|
+
const { getDocsUrl } = await importDocsUrlUtils('zh');
|
|
50
|
+
|
|
51
|
+
expect(getDocsUrl('/guide/model-selection')).toBe('https://docs.example.com/zh/guide/model-selection');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not rewrite non-docs absolute URLs', async () => {
|
|
55
|
+
const { getDocsUrl } = await importDocsUrlUtils('zh');
|
|
56
|
+
|
|
57
|
+
expect(getDocsUrl('https://example.com/help')).toBe('https://example.com/help');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rewrites official docs URLs to the selected docs origin', async () => {
|
|
61
|
+
const { getDocsUrl } = await importDocsUrlUtils('zh');
|
|
62
|
+
|
|
63
|
+
expect(getDocsUrl('https://docs.nextclaw.io/en/guide/commands?tab=cli#start')).toBe(
|
|
64
|
+
'https://docs.nextclaw.net/en/guide/commands?tab=cli#start',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('recognizes the mainland mirror as a docs URL', async () => {
|
|
69
|
+
const { isDocsUrl } = await importDocsUrlUtils('en');
|
|
70
|
+
|
|
71
|
+
expect(isDocsUrl('https://docs.nextclaw.net/zh/guide/getting-started')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -397,6 +397,54 @@ describe("DocBrowser", () => {
|
|
|
397
397
|
expect(screen.getByText("Help Docs")).toBeTruthy();
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
it("focuses panel app iframes on pointer entry without applying the behavior to generic content iframes", () => {
|
|
401
|
+
const panelAppTab: DocBrowserTab = {
|
|
402
|
+
id: "piano",
|
|
403
|
+
kind: "panel-app" as const,
|
|
404
|
+
title: "Piano",
|
|
405
|
+
currentUrl: "/api/panel-apps/piano/content",
|
|
406
|
+
history: ["/api/panel-apps/piano/content"],
|
|
407
|
+
historyIndex: 0,
|
|
408
|
+
navVersion: 0,
|
|
409
|
+
};
|
|
410
|
+
docBrowserState.tabs = [panelAppTab];
|
|
411
|
+
docBrowserState.activeTabId = panelAppTab.id;
|
|
412
|
+
docBrowserState.currentTab = panelAppTab;
|
|
413
|
+
|
|
414
|
+
const { container, unmount } = render(
|
|
415
|
+
<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const panelAppIframe = container.querySelector('iframe[title="Piano"]') as HTMLIFrameElement;
|
|
419
|
+
const panelAppIframeFocus = vi.spyOn(panelAppIframe, "focus");
|
|
420
|
+
const panelAppContentFocus = vi.spyOn(panelAppIframe.contentWindow!, "focus").mockImplementation(() => undefined);
|
|
421
|
+
|
|
422
|
+
fireEvent.pointerOver(panelAppIframe);
|
|
423
|
+
|
|
424
|
+
expect(panelAppIframe.getAttribute("tabindex")).toBe("0");
|
|
425
|
+
expect(panelAppIframeFocus).toHaveBeenCalledWith({ preventScroll: true });
|
|
426
|
+
expect(panelAppContentFocus).toHaveBeenCalled();
|
|
427
|
+
|
|
428
|
+
unmount();
|
|
429
|
+
resetDocBrowserTestState();
|
|
430
|
+
const contentTab: DocBrowserTab = {
|
|
431
|
+
id: "local-app",
|
|
432
|
+
kind: "content",
|
|
433
|
+
title: "Local App",
|
|
434
|
+
currentUrl: "http://127.0.0.1:5173/dashboard",
|
|
435
|
+
history: ["http://127.0.0.1:5173/dashboard"],
|
|
436
|
+
historyIndex: 0,
|
|
437
|
+
navVersion: 0,
|
|
438
|
+
};
|
|
439
|
+
docBrowserState.tabs = [contentTab];
|
|
440
|
+
docBrowserState.activeTabId = contentTab.id;
|
|
441
|
+
docBrowserState.currentTab = contentTab;
|
|
442
|
+
|
|
443
|
+
render(<DocBrowser />);
|
|
444
|
+
|
|
445
|
+
expect(screen.getByTitle("Local App").getAttribute("tabindex")).toBeNull();
|
|
446
|
+
});
|
|
447
|
+
|
|
400
448
|
});
|
|
401
449
|
|
|
402
450
|
describe("DocBrowser floating interactions", () => {
|
|
@@ -22,6 +22,10 @@ export {
|
|
|
22
22
|
DOC_BROWSER_HOME_TAB_KIND,
|
|
23
23
|
DOC_BROWSER_HOME_URL,
|
|
24
24
|
DOCS_DEFAULT_BASE_URL,
|
|
25
|
+
DOCS_MAINLAND_BASE_URL,
|
|
26
|
+
getDocsBaseUrl,
|
|
27
|
+
getDocsUrl,
|
|
28
|
+
getDefaultDocsUrl,
|
|
25
29
|
isDocsUrl,
|
|
26
30
|
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
27
31
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode, Ref } from 'react';
|
|
1
|
+
import type { PointerEventHandler, ReactNode, Ref } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ExternalLink,
|
|
4
4
|
RefreshCw,
|
|
@@ -29,6 +29,7 @@ type DocBrowserFrameContentProps = {
|
|
|
29
29
|
iframeSandbox?: string;
|
|
30
30
|
isDragging: boolean;
|
|
31
31
|
isResizing: boolean;
|
|
32
|
+
onIframePointerOver?: PointerEventHandler<HTMLIFrameElement>;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
export function DocBrowserAddressToolbar({
|
|
@@ -74,6 +75,7 @@ export function DocBrowserFrameContent({
|
|
|
74
75
|
iframeSandbox,
|
|
75
76
|
isDragging,
|
|
76
77
|
isResizing,
|
|
78
|
+
onIframePointerOver,
|
|
77
79
|
}: DocBrowserFrameContentProps) {
|
|
78
80
|
return (
|
|
79
81
|
<div className="flex-1 relative overflow-hidden">
|
|
@@ -87,6 +89,8 @@ export function DocBrowserFrameContent({
|
|
|
87
89
|
className="absolute inset-0 w-full h-full border-0"
|
|
88
90
|
title={currentTab?.title || 'NextClaw Docs'}
|
|
89
91
|
sandbox={iframeSandbox}
|
|
92
|
+
tabIndex={onIframePointerOver ? 0 : undefined}
|
|
93
|
+
onPointerOver={onIframePointerOver}
|
|
90
94
|
allow="clipboard-read; clipboard-write"
|
|
91
95
|
allowFullScreen
|
|
92
96
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import type { PointerEvent, ReactNode } from 'react';
|
|
2
2
|
import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-context';
|
|
3
3
|
|
|
4
4
|
export type DocBrowserCustomTabRenderParams = {
|
|
@@ -20,6 +20,7 @@ export type DocBrowserCustomTabRenderer = {
|
|
|
20
20
|
getIframeSandbox?: (tab: DocBrowserTab) => string | undefined;
|
|
21
21
|
getTitle?: (tab: DocBrowserTab) => string;
|
|
22
22
|
onIframeMessage?: (params: DocBrowserIframeMessageParams) => void;
|
|
23
|
+
onIframePointerOver?: (event: PointerEvent<HTMLIFrameElement>) => void;
|
|
23
24
|
renderContent?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
|
|
24
25
|
renderIcon?: (tab: DocBrowserTab) => ReactNode;
|
|
25
26
|
renderToolbar?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
|
|
@@ -117,20 +117,20 @@ export function DocBrowserTabStrip({
|
|
|
117
117
|
tabs={compactTabs}
|
|
118
118
|
actions={actions}
|
|
119
119
|
className={cn(
|
|
120
|
-
"h-
|
|
121
|
-
isFullscreen && "h-[calc(env(safe-area-inset-top,0px)+2.
|
|
120
|
+
"h-10 gap-1.5 border-border/50 bg-card/80 px-2 shrink-0 select-none",
|
|
121
|
+
isFullscreen && "h-[calc(env(safe-area-inset-top,0px)+2.5rem)] pt-[env(safe-area-inset-top,0px)]",
|
|
122
122
|
)}
|
|
123
123
|
scrollClassName={cn(
|
|
124
|
-
"doc-browser-tab-scrollbar flex h-full items-center gap-
|
|
124
|
+
"doc-browser-tab-scrollbar flex h-full items-center gap-0.5",
|
|
125
125
|
!isDocked && !isFullscreen && "cursor-grab active:cursor-grabbing",
|
|
126
126
|
)}
|
|
127
|
-
tabsClassName="items-center gap-
|
|
128
|
-
actionsClassName="h-full items-center gap-
|
|
129
|
-
actionButtonClassName="rounded-md p-1.5 hover:text-foreground disabled:opacity-
|
|
130
|
-
tabBaseClassName="group inline-flex min-w-0 cursor-pointer items-center gap-
|
|
131
|
-
activeTabClassName="
|
|
132
|
-
inactiveTabClassName="
|
|
133
|
-
labelClassName="px-
|
|
127
|
+
tabsClassName="items-center gap-0.5"
|
|
128
|
+
actionsClassName="h-full items-center gap-0.5"
|
|
129
|
+
actionButtonClassName="rounded-md p-1.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground disabled:opacity-50"
|
|
130
|
+
tabBaseClassName="group inline-flex min-w-0 cursor-pointer items-center gap-0.5 h-7 px-2 rounded-md text-xs max-w-[220px] shrink-0 transition-colors"
|
|
131
|
+
activeTabClassName="bg-muted/80 text-foreground"
|
|
132
|
+
inactiveTabClassName="text-muted-foreground hover:bg-muted/45 hover:text-foreground"
|
|
133
|
+
labelClassName="px-0.5 text-xs font-normal"
|
|
134
134
|
onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
|
|
135
135
|
onScrollPointerDown={(event) => {
|
|
136
136
|
if (shouldBlockHeaderDrag(event.target)) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
DOCS_DEFAULT_BASE_URL,
|
|
4
3
|
DOC_BROWSER_HOME_TAB_KIND,
|
|
4
|
+
getDefaultDocsUrl,
|
|
5
|
+
getDocsUrl,
|
|
5
6
|
type DocBrowserDockControls,
|
|
6
7
|
type DocBrowserDockState,
|
|
7
8
|
type DocBrowserTab,
|
|
@@ -137,11 +138,11 @@ function useDocBrowserAddressBar({
|
|
|
137
138
|
const input = urlInput.trim();
|
|
138
139
|
if (!input) return;
|
|
139
140
|
if (isDocsTab && input.startsWith('/')) {
|
|
140
|
-
navigate(
|
|
141
|
+
navigate(getDocsUrl(input));
|
|
141
142
|
} else if (isDocsTab && input.startsWith('http')) {
|
|
142
143
|
navigate(input);
|
|
143
144
|
} else if (isDocsTab) {
|
|
144
|
-
navigate(
|
|
145
|
+
navigate(getDocsUrl(`/${input}`));
|
|
145
146
|
} else {
|
|
146
147
|
navigate(resolveContentUrlInput(input, currentUrl));
|
|
147
148
|
}
|
|
@@ -180,7 +181,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
|
|
|
180
181
|
const [floatRect, setFloatRect] = useState<FloatingPanelRect>(createInitialFloatingPanelRect);
|
|
181
182
|
const [floatInteraction, setFloatInteraction] = useState<FloatingPanelInteraction | null>(null);
|
|
182
183
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
183
|
-
const currentUrl = currentTab?.currentUrl ??
|
|
184
|
+
const currentUrl = currentTab?.currentUrl ?? getDefaultDocsUrl();
|
|
184
185
|
const navVersion = currentTab?.navVersion ?? 0;
|
|
185
186
|
const iframeInstanceId = `${activeTabId}:${navVersion}:${iframeReloadVersion}`;
|
|
186
187
|
const pendingParentDocsUrlRef = useRef<string | null>(null);
|
|
@@ -374,6 +375,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
|
|
|
374
375
|
iframeSandbox={iframeSandbox}
|
|
375
376
|
isDragging={floatInteraction?.kind === 'drag'}
|
|
376
377
|
isResizing={floatInteraction?.kind === 'resize'}
|
|
378
|
+
onIframePointerOver={customRenderer?.onIframePointerOver}
|
|
377
379
|
/>
|
|
378
380
|
|
|
379
381
|
<DocBrowserExternalLink currentUrl={currentUrl} isVisible={isDocsTab || isContentTab} />
|
|
@@ -2,15 +2,19 @@ import { getLanguage, type I18nLanguage } from '@/shared/lib/i18n';
|
|
|
2
2
|
import type { DocBrowserTabKind } from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
3
3
|
|
|
4
4
|
const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
|
|
5
|
+
const DOCS_MAINLAND_DOMAIN = 'docs.nextclaw.net';
|
|
5
6
|
const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
|
|
6
7
|
const DOCS_HOSTS = new Set([
|
|
7
8
|
DOCS_PRIMARY_DOMAIN,
|
|
8
9
|
`www.${DOCS_PRIMARY_DOMAIN}`,
|
|
10
|
+
DOCS_MAINLAND_DOMAIN,
|
|
11
|
+
`www.${DOCS_MAINLAND_DOMAIN}`,
|
|
9
12
|
DOCS_PAGES_DEV,
|
|
10
13
|
`www.${DOCS_PAGES_DEV}`,
|
|
11
14
|
]);
|
|
12
15
|
|
|
13
16
|
export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
|
|
17
|
+
export const DOCS_MAINLAND_BASE_URL = `https://${DOCS_MAINLAND_DOMAIN}`;
|
|
14
18
|
export const DOC_BROWSER_HOME_TAB_KIND = 'home';
|
|
15
19
|
export const DOC_BROWSER_HOME_URL = 'nextclaw://new-tab';
|
|
16
20
|
const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
|
|
@@ -33,19 +37,68 @@ function ensureLocalizedDocsPath(pathname: string, locale: 'en' | 'zh'): string
|
|
|
33
37
|
return `/${locale}${normalized}`;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
function normalizeDocsBaseUrl(value?: string): string | undefined {
|
|
41
|
+
const raw = value?.trim();
|
|
42
|
+
if (!raw) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return new URL(raw).origin;
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getBrowserTimeZone(): string {
|
|
54
|
+
try {
|
|
55
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone ?? '';
|
|
56
|
+
} catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAbsoluteUrl(value: string): boolean {
|
|
62
|
+
return /^[a-z][a-z\d+.-]*:/i.test(value.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isLikelyMainlandDocsAudience(): boolean {
|
|
66
|
+
return getLanguage() === 'zh' || getBrowserTimeZone() === 'Asia/Shanghai';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getDocsBaseUrl(): string {
|
|
70
|
+
const configuredBaseUrl = normalizeDocsBaseUrl(import.meta.env.VITE_NEXTCLAW_DOCS_BASE_URL);
|
|
71
|
+
if (configuredBaseUrl) {
|
|
72
|
+
return configuredBaseUrl;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isLikelyMainlandDocsAudience()) {
|
|
76
|
+
return normalizeDocsBaseUrl(import.meta.env.VITE_NEXTCLAW_DOCS_CN_BASE_URL) ?? DOCS_MAINLAND_BASE_URL;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return DOCS_DEFAULT_BASE_URL;
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
function resolveLocalizedDocsUrl(url: string): string {
|
|
37
83
|
const locale = toDocsLocale(getLanguage());
|
|
84
|
+
const docsBaseUrl = getDocsBaseUrl();
|
|
38
85
|
|
|
39
86
|
try {
|
|
40
|
-
const parsed = new URL(url,
|
|
41
|
-
|
|
87
|
+
const parsed = new URL(url, docsBaseUrl);
|
|
88
|
+
const docsBase = new URL(docsBaseUrl);
|
|
89
|
+
const shouldResolveAsDocs = !isAbsoluteUrl(url)
|
|
90
|
+
|| DOCS_HOSTS.has(parsed.hostname)
|
|
91
|
+
|| parsed.origin === docsBase.origin;
|
|
92
|
+
if (!shouldResolveAsDocs) {
|
|
42
93
|
return parsed.toString();
|
|
43
94
|
}
|
|
44
95
|
|
|
96
|
+
parsed.protocol = docsBase.protocol;
|
|
97
|
+
parsed.host = docsBase.host;
|
|
45
98
|
parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
|
|
46
99
|
return parsed.toString();
|
|
47
100
|
} catch {
|
|
48
|
-
return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`,
|
|
101
|
+
return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, docsBaseUrl).toString();
|
|
49
102
|
}
|
|
50
103
|
}
|
|
51
104
|
|
|
@@ -53,6 +106,10 @@ export function getDefaultDocsUrl(): string {
|
|
|
53
106
|
return resolveLocalizedDocsUrl(DOCS_DEFAULT_GUIDE_PATH);
|
|
54
107
|
}
|
|
55
108
|
|
|
109
|
+
export function getDocsUrl(pathOrUrl: string): string {
|
|
110
|
+
return resolveLocalizedDocsUrl(pathOrUrl);
|
|
111
|
+
}
|
|
112
|
+
|
|
56
113
|
export function normalizeDocUrl(url: string): string {
|
|
57
114
|
try {
|
|
58
115
|
return new URL(url).pathname.replace(/\.html$/, '').replace(/\/$/, '');
|
|
@@ -78,7 +135,8 @@ export function inferTabTitle(url: string, kind: DocBrowserTabKind, fallback = '
|
|
|
78
135
|
|
|
79
136
|
export function isDocsUrl(url: string): boolean {
|
|
80
137
|
try {
|
|
81
|
-
const
|
|
138
|
+
const fallbackOrigin = typeof window !== 'undefined' ? window.location.origin : DOCS_DEFAULT_BASE_URL;
|
|
139
|
+
const parsed = new URL(url, fallbackOrigin);
|
|
82
140
|
return DOCS_HOSTS.has(parsed.hostname);
|
|
83
141
|
} catch {
|
|
84
142
|
return false;
|
package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx
CHANGED
|
@@ -63,4 +63,22 @@ describe("ResizableRightPanel", () => {
|
|
|
63
63
|
).toBeNull();
|
|
64
64
|
expect(screen.getByTestId("right-panel").className).toContain("fixed");
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
it("can scope overlay positioning to its container", () => {
|
|
68
|
+
render(
|
|
69
|
+
<ResizableRightPanel
|
|
70
|
+
data-testid="right-panel"
|
|
71
|
+
overlay
|
|
72
|
+
overlayScope="container"
|
|
73
|
+
>
|
|
74
|
+
Content
|
|
75
|
+
</ResizableRightPanel>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
screen.queryByTestId("resizable-right-panel-handle"),
|
|
80
|
+
).toBeNull();
|
|
81
|
+
expect(screen.getByTestId("right-panel").className).toContain("absolute");
|
|
82
|
+
expect(screen.getByTestId("right-panel").className).not.toContain("fixed");
|
|
83
|
+
});
|
|
66
84
|
});
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import { useRef, useState, type ComponentPropsWithoutRef } from "react";
|
|
2
2
|
import { cn } from "@/shared/lib/utils";
|
|
3
3
|
|
|
4
|
-
type ResizableRightPanelProps = ComponentPropsWithoutRef<"aside"> & {
|
|
4
|
+
type ResizableRightPanelProps = ComponentPropsWithoutRef<"aside"> & {
|
|
5
|
+
defaultWidth?: number;
|
|
6
|
+
minWidth?: number;
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
overlay?: boolean;
|
|
9
|
+
overlayScope?: "viewport" | "container";
|
|
10
|
+
};
|
|
5
11
|
|
|
6
|
-
export function ResizableRightPanel({
|
|
12
|
+
export function ResizableRightPanel({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
defaultWidth = 420,
|
|
17
|
+
minWidth = 320,
|
|
18
|
+
maxWidth = 860,
|
|
19
|
+
overlay = false,
|
|
20
|
+
overlayScope = "viewport",
|
|
21
|
+
...props
|
|
22
|
+
}: ResizableRightPanelProps) {
|
|
7
23
|
const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
8
24
|
const [isResizing, setIsResizing] = useState(false);
|
|
9
25
|
const [width, setWidth] = useState(defaultWidth);
|
|
26
|
+
const overlayClassName = overlay
|
|
27
|
+
? overlayScope === "container"
|
|
28
|
+
? "absolute inset-0 z-30"
|
|
29
|
+
: "fixed inset-0 z-40"
|
|
30
|
+
: "border-l border-border";
|
|
10
31
|
|
|
11
32
|
const onResizeStart = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
12
33
|
if (overlay) return;
|
|
@@ -36,7 +57,11 @@ export function ResizableRightPanel({ children, className, style, defaultWidth =
|
|
|
36
57
|
return (
|
|
37
58
|
<aside
|
|
38
59
|
{...props}
|
|
39
|
-
className={cn(
|
|
60
|
+
className={cn(
|
|
61
|
+
"relative flex h-full min-h-0 shrink-0 overflow-hidden bg-card text-card-foreground",
|
|
62
|
+
overlayClassName,
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
40
65
|
style={overlay ? style : { ...style, width }}
|
|
41
66
|
>
|
|
42
67
|
{!overlay ? (
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
4
|
+
|
|
5
|
+
describe('IconActionButton', () => {
|
|
6
|
+
it('applies compact size and strong tone classes for nested hover surfaces', () => {
|
|
7
|
+
render(
|
|
8
|
+
<IconActionButton
|
|
9
|
+
size="sm"
|
|
10
|
+
tone="strong"
|
|
11
|
+
icon={<span data-testid="icon" />}
|
|
12
|
+
label="Pin"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const button = screen.getByRole('button', { name: 'Pin' });
|
|
17
|
+
expect(button.className).toContain('h-6');
|
|
18
|
+
expect(button.className).toContain('w-6');
|
|
19
|
+
expect(button.className).toContain('p-1');
|
|
20
|
+
expect(button.className).toContain('hover:bg-black/10');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('defaults to medium size and soft accent hover', () => {
|
|
24
|
+
render(
|
|
25
|
+
<IconActionButton
|
|
26
|
+
icon={<span data-testid="icon" />}
|
|
27
|
+
label="More"
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const button = screen.getByRole('button', { name: 'More' });
|
|
32
|
+
expect(button.className).toContain('h-7');
|
|
33
|
+
expect(button.className).toContain('w-7');
|
|
34
|
+
expect(button.className).toContain('hover:bg-accent');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '@/shared/lib/utils';
|
|
3
|
+
|
|
4
|
+
type FormActionsProps = HTMLAttributes<HTMLDivElement> & {
|
|
5
|
+
align?: 'start' | 'end' | 'between';
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function FormActions({
|
|
9
|
+
align = 'end',
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: FormActionsProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex flex-wrap items-center gap-2',
|
|
17
|
+
align === 'start' && 'justify-start',
|
|
18
|
+
align === 'end' && 'justify-end',
|
|
19
|
+
align === 'between' && 'justify-between',
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|