@nextclaw/ui 0.15.1 → 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 +31 -0
- package/dist/assets/{api-b1R9VdeH.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-B77lQ2Tg.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/{chat-page-CZ7WxPir.js → chat-page-C_5dcuE3.js} +1 -1
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-BGR3HUi1.js} +1 -1
- package/dist/assets/{dist-jdGmbb6w.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-YtoqU-AT.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-bNpL2LKy.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-BM6fk8q-.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-CntWolRA.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-CLmUstLN.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-Cm_a4GHf.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-BN8uzPnp.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-BpVcFPmJ.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-BpFW5fpk.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-D0uO7enf.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-Bh-CrA4p.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-2DWauOjY.js} +1 -1
- package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-CFLJFNSd.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-CVPgsBmU.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-CSeBk9SF.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/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
- 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/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/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/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/utils/chat-sidebar-session-groups.utils.ts +25 -3
- 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 +159 -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 +66 -7
- 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/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- 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/config/runtime-config-editor.tsx +5 -4
- 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/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -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-DcsIQc6A.js +0 -8
- package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
- package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
- package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
- package/dist/assets/index-BIcEw90u.js +0 -104
- package/dist/assets/index-BiNSTKTU.css +0 -1
- package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
- package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
- package/dist/assets/plus-vDqW5Kxp.js +0 -1
- package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
- package/dist/assets/remote-CLQ0IBM8.js +0 -1
- package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
- package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
- package/dist/assets/search-config-page-D0klFUXd.js +0 -1
- package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
- package/dist/assets/security-config-C5gnlkuj.js +0 -1
- package/dist/assets/x-BF2KQ--V.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
|
@@ -137,7 +137,7 @@ export function ProvidersConfigPage() {
|
|
|
137
137
|
<div className="grid max-h-[24rem] grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3">
|
|
138
138
|
<button
|
|
139
139
|
type="button"
|
|
140
|
-
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-dashed border-border bg-
|
|
140
|
+
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-dashed border-border/70 bg-muted/20 px-3 py-2 text-left transition-colors hover:border-border hover:bg-muted/45"
|
|
141
141
|
onClick={async () => {
|
|
142
142
|
try {
|
|
143
143
|
const result = await createProvider.mutateAsync({ data: { providerType: null } });
|
|
@@ -150,7 +150,7 @@ export function ProvidersConfigPage() {
|
|
|
150
150
|
}
|
|
151
151
|
}}
|
|
152
152
|
>
|
|
153
|
-
<LogoBadge name="custom" className="h-8 w-8 rounded-lg border border-border bg-
|
|
153
|
+
<LogoBadge name="custom" className="h-8 w-8 rounded-lg border border-border/60 bg-muted/50" />
|
|
154
154
|
<span className="line-clamp-2 min-w-0 text-xs font-semibold leading-4 text-foreground">
|
|
155
155
|
{t('providerAddCustom')}
|
|
156
156
|
</span>
|
|
@@ -159,7 +159,7 @@ export function ProvidersConfigPage() {
|
|
|
159
159
|
<button
|
|
160
160
|
key={template.providerType}
|
|
161
161
|
type="button"
|
|
162
|
-
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-border bg-
|
|
162
|
+
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-border/55 bg-muted/15 px-3 py-2 text-left transition-colors hover:border-border/80 hover:bg-muted/40"
|
|
163
163
|
onClick={async () => {
|
|
164
164
|
try {
|
|
165
165
|
const result = await createProvider.mutateAsync({ data: { providerType: template.providerType } });
|
|
@@ -175,7 +175,7 @@ export function ProvidersConfigPage() {
|
|
|
175
175
|
<LogoBadge
|
|
176
176
|
name={template.providerType}
|
|
177
177
|
src={template.logo ? `/logos/${template.logo}` : null}
|
|
178
|
-
className="h-8 w-8 rounded-lg border border-border bg-background"
|
|
178
|
+
className="h-8 w-8 rounded-lg border border-border/55 bg-background"
|
|
179
179
|
imgClassName="h-4 w-4 object-contain"
|
|
180
180
|
/>
|
|
181
181
|
<span className="line-clamp-2 min-w-0 text-xs font-semibold leading-4 text-foreground">
|
|
@@ -221,10 +221,10 @@ export function ProvidersConfigPage() {
|
|
|
221
221
|
role="button"
|
|
222
222
|
tabIndex={0}
|
|
223
223
|
className={cn(
|
|
224
|
-
'w-full rounded-xl border p-2.5 text-left transition-
|
|
224
|
+
'group w-full rounded-xl border p-2.5 text-left transition-colors',
|
|
225
225
|
resolvedSelectedProvider === provider.providerId
|
|
226
|
-
? 'border-
|
|
227
|
-
: 'border-
|
|
226
|
+
? 'border-border/70 bg-muted/65 text-foreground'
|
|
227
|
+
: 'border-transparent bg-transparent text-muted-foreground hover:border-border/50 hover:bg-muted/40 hover:text-foreground',
|
|
228
228
|
)}
|
|
229
229
|
>
|
|
230
230
|
<div className="relative min-h-10">
|
|
@@ -232,10 +232,7 @@ export function ProvidersConfigPage() {
|
|
|
232
232
|
<LogoBadge
|
|
233
233
|
name={provider.providerType ?? provider.providerId}
|
|
234
234
|
src={template?.logo ? `/logos/${template.logo}` : null}
|
|
235
|
-
className=
|
|
236
|
-
'h-10 w-10 rounded-lg border',
|
|
237
|
-
isReady ? 'border-primary/35 bg-background' : 'border-border bg-background',
|
|
238
|
-
)}
|
|
235
|
+
className="h-10 w-10 rounded-lg border border-border/55 bg-muted/40"
|
|
239
236
|
imgClassName="h-5 w-5 object-contain"
|
|
240
237
|
fallback={<span className="text-sm font-semibold uppercase text-muted-foreground">{provider.providerId[0]}</span>}
|
|
241
238
|
/>
|
|
@@ -247,7 +244,7 @@ export function ProvidersConfigPage() {
|
|
|
247
244
|
</p>
|
|
248
245
|
</div>
|
|
249
246
|
</div>
|
|
250
|
-
<div className="absolute right-0 top-0 flex items-center gap-1.5">
|
|
247
|
+
<div className="absolute right-0 top-0 flex items-center gap-1.5 opacity-70 transition-opacity group-hover:opacity-100">
|
|
251
248
|
<div onClick={(event) => event.stopPropagation()}>
|
|
252
249
|
<Switch
|
|
253
250
|
checked={isEnabled}
|
|
@@ -268,7 +265,7 @@ export function ProvidersConfigPage() {
|
|
|
268
265
|
<PopoverTrigger asChild>
|
|
269
266
|
<button
|
|
270
267
|
type="button"
|
|
271
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground/70 hover:bg-
|
|
268
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground/70 hover:bg-muted hover:text-foreground"
|
|
272
269
|
onClick={(event) => event.stopPropagation()}
|
|
273
270
|
title={t('more')}
|
|
274
271
|
>
|
|
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { ExternalLink, KeyRound, Search as SearchIcon } from "lucide-react";
|
|
3
3
|
import { PageHeader, PageLayout } from "@/app/components/layout/page-layout";
|
|
4
4
|
import { Button } from "@/shared/components/ui/button";
|
|
5
|
+
import { FormActions } from "@/shared/components/ui/actions/form-actions";
|
|
5
6
|
import { Input } from "@/shared/components/ui/input";
|
|
6
7
|
import { Label } from "@/shared/components/ui/label";
|
|
7
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select";
|
|
@@ -456,11 +457,13 @@ function SearchConfigForm(props: {
|
|
|
456
457
|
/>
|
|
457
458
|
</ConfigSplitPaneBody>
|
|
458
459
|
|
|
459
|
-
<ConfigSplitPaneFooter
|
|
460
|
-
<
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
460
|
+
<ConfigSplitPaneFooter>
|
|
461
|
+
<FormActions>
|
|
462
|
+
<Button type="submit" size="sm" disabled={isPending}>
|
|
463
|
+
<KeyRound className="mr-1.5 h-3.5 w-3.5" />
|
|
464
|
+
{isPending ? t("saving") : t("saveChanges")}
|
|
465
|
+
</Button>
|
|
466
|
+
</FormActions>
|
|
464
467
|
</ConfigSplitPaneFooter>
|
|
465
468
|
</form>
|
|
466
469
|
</ConfigSplitDetailPane>
|
|
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
|
|
2
2
|
import { Save } from 'lucide-react';
|
|
3
3
|
import type { ConfigView, RuntimeConfigUpdate } from '@/shared/lib/api';
|
|
4
4
|
import { Button } from '@/shared/components/ui/button';
|
|
5
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
5
6
|
import { PageLayout } from '@/app/components/layout/page-layout';
|
|
6
7
|
import { t } from '@/shared/lib/i18n';
|
|
7
8
|
import { RuntimeConfigOverview } from '@/features/system-status/components/config/runtime-config-overview';
|
|
@@ -111,12 +112,12 @@ export function RuntimeConfigEditor({
|
|
|
111
112
|
onRemoveBinding={(index) => setBindings((previous) => previous.filter((_, cursor) => cursor !== index))}
|
|
112
113
|
onAddBinding={() => setBindings((previous) => [...previous, createEmptyRuntimeBinding()])}
|
|
113
114
|
/>
|
|
114
|
-
<
|
|
115
|
-
<Button type="button" onClick={handleSave} disabled={updateRuntime.isPending}>
|
|
116
|
-
<Save className="h-
|
|
115
|
+
<FormActions>
|
|
116
|
+
<Button type="button" size="sm" onClick={handleSave} disabled={updateRuntime.isPending}>
|
|
117
|
+
<Save className="mr-1.5 h-3.5 w-3.5" />
|
|
117
118
|
{updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
|
|
118
119
|
</Button>
|
|
119
|
-
</
|
|
120
|
+
</FormActions>
|
|
120
121
|
</PageLayout>
|
|
121
122
|
);
|
|
122
123
|
}
|
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 ? (
|