handoff-app 2.0.0-9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +90 -1
- package/{Changelog.md → CHANGELOG.md} +105 -1
- package/CLAUDE.md +92 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +818 -406
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/cli/create.js +1 -2
- package/dist/cli.js +1 -1
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +26 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/errors.js +4 -0
- package/dist/registry/publish/index.js +2 -0
- package/dist/registry/publish/package.d.ts +6 -0
- package/dist/registry/publish/package.js +29 -1
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/utils/build.d.ts +9 -0
- package/dist/transformers/utils/build.js +27 -5
- package/dist/types/config.d.ts +129 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/Preview.tsx +5 -2
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/cli/create.ts +1 -2
- package/src/cli.ts +1 -1
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +25 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +1 -1
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/errors.ts +4 -0
- package/src/registry/publish/index.ts +3 -1
- package/src/registry/publish/package.ts +33 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +22 -4
- package/src/types/config.ts +142 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ArrowLeft } from 'lucide-react';
|
|
2
2
|
import Head from 'next/head';
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
+
import type { ClientConfig } from '@handoff/types/config';
|
|
4
5
|
import type { ReactNode } from 'react';
|
|
5
|
-
import {
|
|
6
|
+
import { pageTitle } from '../../lib/utils';
|
|
7
|
+
import { ConfigContextProvider } from '../context/ConfigContext';
|
|
8
|
+
import { Header } from '../Layout/Header';
|
|
6
9
|
import { Button } from '../ui/button';
|
|
7
10
|
import { ThemeProvider } from '../util/theme-provider';
|
|
8
11
|
|
|
@@ -10,16 +13,26 @@ const isRegistryRuntime = process.env.HANDOFF_RUNTIME_MODE === 'registry';
|
|
|
10
13
|
|
|
11
14
|
interface AuthShellProps {
|
|
12
15
|
children: ReactNode;
|
|
16
|
+
config?: ClientConfig;
|
|
13
17
|
title: string;
|
|
14
18
|
description?: string;
|
|
15
19
|
wide?: boolean;
|
|
16
20
|
hideNav?: boolean;
|
|
21
|
+
centered?: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
export function AuthShell({ children, title, description, wide = false, hideNav = false }: AuthShellProps) {
|
|
24
|
+
export function AuthShell({ children, config, title, description, wide = false, hideNav = false, centered = false }: AuthShellProps) {
|
|
25
|
+
const head = (
|
|
26
|
+
<Head>
|
|
27
|
+
<title>{pageTitle({ title }, config?.app?.title)}</title>
|
|
28
|
+
{description ? <meta name="description" content={description} /> : null}
|
|
29
|
+
</Head>
|
|
30
|
+
);
|
|
31
|
+
|
|
20
32
|
if (!isRegistryRuntime) {
|
|
21
33
|
return (
|
|
22
34
|
<main className="flex min-h-screen items-center justify-center px-6">
|
|
35
|
+
{head}
|
|
23
36
|
<div className="max-w-md text-center">
|
|
24
37
|
<h1 className="text-2xl font-semibold">Registry only</h1>
|
|
25
38
|
<p className="mt-2 text-sm text-muted-foreground">Account management is available only in a deployed registry.</p>
|
|
@@ -36,38 +49,19 @@ export function AuthShell({ children, title, description, wide = false, hideNav
|
|
|
36
49
|
|
|
37
50
|
return (
|
|
38
51
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt="Handoff" />
|
|
50
|
-
<Badge variant="info">Registry</Badge>
|
|
51
|
-
</span>
|
|
52
|
-
) : (
|
|
53
|
-
<Link href="/" className="flex items-center gap-3">
|
|
54
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
55
|
-
<img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt="Handoff" />
|
|
56
|
-
<Badge variant="info">Registry</Badge>
|
|
57
|
-
</Link>
|
|
58
|
-
)}
|
|
59
|
-
{hideNav ? null : (
|
|
60
|
-
<Button asChild variant="ghost" size="sm" className="gap-2 text-muted-foreground hover:text-foreground">
|
|
61
|
-
<Link href="/">
|
|
62
|
-
<ArrowLeft className="h-4 w-4" />
|
|
63
|
-
Back to documentation
|
|
64
|
-
</Link>
|
|
65
|
-
</Button>
|
|
66
|
-
)}
|
|
52
|
+
{head}
|
|
53
|
+
{centered ? (
|
|
54
|
+
<main className="flex min-h-[calc(100vh-8rem)] items-center justify-center bg-background px-4 py-12">
|
|
55
|
+
<div className="w-full max-w-md">{children}</div>
|
|
56
|
+
</main>
|
|
57
|
+
) : (
|
|
58
|
+
<ConfigContextProvider defaultConfig={config}>
|
|
59
|
+
<div className="min-h-screen bg-background">
|
|
60
|
+
<Header hideNav={hideNav} />
|
|
61
|
+
<main className={`container mx-auto px-8 ${wide ? 'max-w-[1500px]' : 'max-w-xl py-10'}`}>{children}</main>
|
|
67
62
|
</div>
|
|
68
|
-
</
|
|
69
|
-
|
|
70
|
-
</div>
|
|
63
|
+
</ConfigContextProvider>
|
|
64
|
+
)}
|
|
71
65
|
</ThemeProvider>
|
|
72
66
|
);
|
|
73
67
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRouter } from 'next/router';
|
|
2
2
|
import { useEffect, type ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { loadInstallStatus } from './installStatus';
|
|
4
4
|
|
|
5
5
|
const isRegistryRuntime = process.env.HANDOFF_RUNTIME_MODE === 'registry';
|
|
6
6
|
|
|
@@ -10,13 +10,9 @@ export function RegistryInstallGate({ children }: { children: ReactNode }) {
|
|
|
10
10
|
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
if (!isRegistryRuntime || !router.isReady || router.pathname === '/install') return;
|
|
13
|
-
void
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const status = (await response.json()) as { installed?: boolean };
|
|
17
|
-
if (status.installed === false) await router.replace('/install');
|
|
18
|
-
})
|
|
19
|
-
.catch(() => undefined);
|
|
13
|
+
void loadInstallStatus().then(async (status) => {
|
|
14
|
+
if (status?.installed === false) await router.replace('/install');
|
|
15
|
+
});
|
|
20
16
|
}, [router]);
|
|
21
17
|
|
|
22
18
|
return <>{children}</>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { authApiUrl } from './api';
|
|
2
|
+
|
|
3
|
+
export interface InstallStatus {
|
|
4
|
+
installed?: boolean;
|
|
5
|
+
emailConfigured?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Installation is one-way, so an installed result is kept until a hard reload. Concurrent callers
|
|
9
|
+
// share one request.
|
|
10
|
+
let cachedStatus: InstallStatus | null = null;
|
|
11
|
+
let inFlight: Promise<InstallStatus | null> | null = null;
|
|
12
|
+
|
|
13
|
+
/** The registry installation status, or `null` when it could not be read. */
|
|
14
|
+
export const loadInstallStatus = (): Promise<InstallStatus | null> => {
|
|
15
|
+
if (cachedStatus) return Promise.resolve(cachedStatus);
|
|
16
|
+
if (inFlight) return inFlight;
|
|
17
|
+
inFlight = fetch(authApiUrl('/api/install'), { credentials: 'include', cache: 'no-store' })
|
|
18
|
+
.then(async (response) => {
|
|
19
|
+
if (!response.ok) return null;
|
|
20
|
+
const status = (await response.json()) as InstallStatus;
|
|
21
|
+
if (status.installed === true) cachedStatus = status;
|
|
22
|
+
return status;
|
|
23
|
+
})
|
|
24
|
+
.catch(() => null)
|
|
25
|
+
.finally(() => {
|
|
26
|
+
inFlight = null;
|
|
27
|
+
});
|
|
28
|
+
return inFlight;
|
|
29
|
+
};
|
|
@@ -285,11 +285,14 @@ export const ComponentDisplay: React.FC<{
|
|
|
285
285
|
const originStyle: React.CSSProperties = originRect
|
|
286
286
|
? { top: `${originRect.top}%`, left: `${originRect.left}%`, width: `${originRect.width}%`, transform: 'translateY(0)' }
|
|
287
287
|
: {};
|
|
288
|
+
// Positioned against the viewport, so it subtracts the assistant's column itself. The margin is a
|
|
289
|
+
// share of what is left, not of the whole viewport, so the card stays centered in usable space.
|
|
290
|
+
const freeWidth = 'calc(100vw - var(--ai-dock-width, 0px))';
|
|
288
291
|
const fullSizeStyle: React.CSSProperties = {
|
|
289
292
|
...collapsedStyle,
|
|
290
293
|
top: '50%',
|
|
291
|
-
left:
|
|
292
|
-
width:
|
|
294
|
+
left: `calc(${freeWidth} * 0.02)`,
|
|
295
|
+
width: `calc(${freeWidth} * 0.96)`,
|
|
293
296
|
maxHeight: '92vh',
|
|
294
297
|
transform: 'translateY(-50%)',
|
|
295
298
|
};
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Separator } from '../ui/separator';
|
|
17
17
|
import { Sheet, SheetClose, SheetContent, SheetHeader } from '../ui/sheet';
|
|
18
18
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
19
|
-
import { hexToRgb } from '../util/colors';
|
|
19
|
+
import { hexToRgb, hexToRgbaCss } from '../util/colors';
|
|
20
20
|
import ColorContrast from './ColorContrast';
|
|
21
21
|
import ColorInfo from './ColorInfo';
|
|
22
22
|
import ColorSpaces from './ColorSpaces';
|
|
@@ -25,7 +25,6 @@ import ColorTailwind from './ColorTailwind';
|
|
|
25
25
|
type ColorGridProps = {
|
|
26
26
|
title: string;
|
|
27
27
|
group: string;
|
|
28
|
-
description: string;
|
|
29
28
|
colors: CoreTypes.IColorObject[];
|
|
30
29
|
};
|
|
31
30
|
|
|
@@ -33,7 +32,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
33
32
|
<div className="@container">
|
|
34
33
|
<div className="mb-6 grid grid-cols-1 gap-6 @md:grid-cols-2">
|
|
35
34
|
{colors.map((color) => (
|
|
36
|
-
<
|
|
35
|
+
<div className="flex flex-col items-start" key={color.group + '-' + color.name}>
|
|
37
36
|
<div
|
|
38
37
|
className="group relative mb-2 block h-32 w-full rounded-lg"
|
|
39
38
|
style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
|
|
@@ -42,7 +41,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
42
41
|
</div>
|
|
43
42
|
<p className="mb-1 text-sm font-medium line-clamp-1" title={color.name}>{color.name}</p>
|
|
44
43
|
<small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
|
|
45
|
-
</
|
|
44
|
+
</div>
|
|
46
45
|
))}
|
|
47
46
|
</div>
|
|
48
47
|
</div>
|
|
@@ -52,10 +51,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
52
51
|
// group colors by subgroup
|
|
53
52
|
const groupedColors = colors.reduce(
|
|
54
53
|
(acc, color) => {
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const subgroup = color.subgroup ?? '';
|
|
55
|
+
if (!acc[subgroup]) {
|
|
56
|
+
acc[subgroup] = [];
|
|
57
57
|
}
|
|
58
|
-
acc[
|
|
58
|
+
acc[subgroup].push(color);
|
|
59
59
|
return acc;
|
|
60
60
|
},
|
|
61
61
|
{} as Record<string, CoreTypes.IColorObject[]>
|
|
@@ -64,11 +64,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
64
64
|
<>
|
|
65
65
|
{Object.entries(groupedColors).map(([subgroup]) => (
|
|
66
66
|
<div className="mb-5" key={`group-${subgroup}`}>
|
|
67
|
-
<HeadersType.H4
|
|
67
|
+
{subgroup && <HeadersType.H4>{startCase(subgroup.replaceAll('-', ' '))}</HeadersType.H4>}
|
|
68
68
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(60px,60px))] gap-6">
|
|
69
69
|
{groupedColors[subgroup].map((color) => (
|
|
70
70
|
<React.Fragment key={`group-${subgroup}-${color.name}`}>
|
|
71
|
-
<
|
|
71
|
+
<div className="flex flex-col">
|
|
72
72
|
<div
|
|
73
73
|
className="group relative mb-2 block h-14 rounded-lg"
|
|
74
74
|
style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
|
|
@@ -77,7 +77,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
77
77
|
</div>
|
|
78
78
|
<p className="mb-1 text-xs font-medium line-clamp-1" title={color.name}>{color.name}</p>
|
|
79
79
|
<small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
|
|
80
|
-
</
|
|
80
|
+
</div>
|
|
81
81
|
</React.Fragment>
|
|
82
82
|
))}
|
|
83
83
|
</div>
|
|
@@ -87,7 +87,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
|
|
|
87
87
|
);
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const ColorGrid: React.FC<ColorGridProps> = ({ title,
|
|
90
|
+
const ColorGrid: React.FC<ColorGridProps> = ({ title, colors, group }) => {
|
|
91
91
|
const [open, setOpen] = React.useState(false);
|
|
92
92
|
const [selectedColor, setSelectedColor] = React.useState(null);
|
|
93
93
|
const openSheet = (color) => {
|
|
@@ -96,8 +96,7 @@ const ColorGrid: React.FC<ColorGridProps> = ({ title, description, colors, group
|
|
|
96
96
|
};
|
|
97
97
|
return (
|
|
98
98
|
<div id={`${group}-colors`} className="scroll-mt-24 scroll-smooth pb-10">
|
|
99
|
-
<h3 className="text-lg font-medium">{title}</h3>
|
|
100
|
-
<p className="mb-8">{description}</p>
|
|
99
|
+
<h3 className="mb-8 text-lg font-medium">{title}</h3>
|
|
101
100
|
{colors.length < 5 ? <LargeColorGrid colors={colors} setOpen={openSheet} /> : <SmallColorGrid colors={colors} setOpen={openSheet} />}
|
|
102
101
|
<ColorSheet color={selectedColor} open={open} setOpen={setOpen} />
|
|
103
102
|
</div>
|
|
@@ -122,7 +121,7 @@ const ColorDropdown: React.FC<{ color: CoreTypes.IColorObject; openSheet: (color
|
|
|
122
121
|
<Copy className="text-gray-400" /> HEX
|
|
123
122
|
<DropdownMenuShortcut className="max-w-[100px] truncate">{color.value}</DropdownMenuShortcut>
|
|
124
123
|
</DropdownMenuItem>
|
|
125
|
-
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(
|
|
124
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(hexToRgbaCss(color.value) ?? color.value)}>
|
|
126
125
|
<Copy className="text-gray-400" /> RGBA
|
|
127
126
|
<DropdownMenuShortcut className="max-w-[100px] truncate">{hexToRgb(color.value)}</DropdownMenuShortcut>
|
|
128
127
|
</DropdownMenuItem>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Link from 'next/link';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
+
import { AiAssistantTrigger } from '../../components/AiAssistant/AiAssistantProvider';
|
|
3
4
|
import { McpConfigDialog } from '../../components/McpIntegration/McpConfigDialog';
|
|
4
5
|
import { ModeToggle } from '../../components/ModeSwitcher';
|
|
5
6
|
import { MainNav } from '../../components/Navigation/MainNav';
|
|
@@ -9,7 +10,7 @@ import { useConfigContext } from '../context/ConfigContext';
|
|
|
9
10
|
import { AuthControls } from '../Auth/AuthControls';
|
|
10
11
|
import { RuntimeModeBadge } from './RuntimeModeBadge';
|
|
11
12
|
|
|
12
|
-
export function Header() {
|
|
13
|
+
export function Header({ hideNav = false }: { hideNav?: boolean }) {
|
|
13
14
|
const context = useConfigContext();
|
|
14
15
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
15
16
|
const runtimeMode = context.config?.runtime?.mode ?? 'workspace';
|
|
@@ -26,30 +27,35 @@ export function Header() {
|
|
|
26
27
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
27
28
|
}, []);
|
|
28
29
|
|
|
30
|
+
const logo = <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />;
|
|
31
|
+
|
|
29
32
|
return (
|
|
30
33
|
<div
|
|
31
34
|
className={cn(
|
|
32
|
-
'sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
|
|
33
|
-
isScrolled && 'bg-background/70
|
|
35
|
+
'ai-dock-bleed h-17 sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
|
|
36
|
+
isScrolled && 'bg-background/70 shadow-[0_0_4px_0_rgba(0,0,0,0.15)]'
|
|
34
37
|
)}
|
|
35
38
|
>
|
|
36
39
|
<header className="border-grid container mx-auto w-full max-w-[1500px] bg-transparent px-8">
|
|
37
40
|
<div className="mx-auto flex items-center justify-between @container">
|
|
38
41
|
<div className="flex items-center gap-3">
|
|
39
|
-
<Link href="/">
|
|
40
|
-
<img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
|
|
41
|
-
</Link>
|
|
42
|
+
{hideNav ? logo : <Link href="/">{logo}</Link>}
|
|
42
43
|
{!isStaticSnapshot && <RuntimeModeBadge mode={runtimeMode} />}
|
|
43
44
|
</div>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
{hideNav ? null : (
|
|
46
|
+
<>
|
|
47
|
+
<div className="hidden items-center gap-4 @2xl:flex">
|
|
48
|
+
<MainNav />
|
|
49
|
+
<AiAssistantTrigger />
|
|
50
|
+
<McpConfigDialog />
|
|
51
|
+
{runtimeMode === 'registry' ? <AuthControls /> : null}
|
|
52
|
+
<ModeToggle />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex items-center gap-4 @2xl:hidden">
|
|
55
|
+
<MobileNav />
|
|
56
|
+
</div>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
53
59
|
</div>
|
|
54
60
|
</header>
|
|
55
61
|
</div>
|
|
@@ -5,7 +5,7 @@ import { Header } from '../../components/Layout/Header';
|
|
|
5
5
|
import { ThemeProvider } from '../../components/util/theme-provider';
|
|
6
6
|
import SideNav from '../Navigation/SideNav';
|
|
7
7
|
import { ConfigContextProvider } from '../context/ConfigContext';
|
|
8
|
-
import { hasRenderableNav } from '../../lib/utils';
|
|
8
|
+
import { hasRenderableNav, pageTitle } from '../../lib/utils';
|
|
9
9
|
import { SidebarInset, SidebarProvider } from '../ui/sidebar';
|
|
10
10
|
import { SectionLink } from '../util';
|
|
11
11
|
|
|
@@ -34,7 +34,7 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
|
|
|
34
34
|
<ConfigContextProvider defaultConfig={config} defaultMenu={menu}>
|
|
35
35
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
36
36
|
<Head>
|
|
37
|
-
<title>{metadata
|
|
37
|
+
<title>{pageTitle(metadata, config?.app?.title)}</title>
|
|
38
38
|
<meta name="description" content={metadata.metaDescription} />
|
|
39
39
|
</Head>
|
|
40
40
|
<div className="absolute left-[-200px] top-[-200px] z-[-1] h-[400px] w-[600px] bg-[#111111] opacity-[0.05] blur-[350px]">
|
|
@@ -70,38 +70,36 @@ function extractText(node: any): string {
|
|
|
70
70
|
|
|
71
71
|
const Headings = (element: any) => {
|
|
72
72
|
const { children, node } = element;
|
|
73
|
-
if (children
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} else {
|
|
104
|
-
return <h1>children</h1>;
|
|
73
|
+
if (React.Children.count(children) === 0) return <></>;
|
|
74
|
+
|
|
75
|
+
const type = node.tagName || 'h6';
|
|
76
|
+
|
|
77
|
+
let anchor = extractText(children).toLowerCase();
|
|
78
|
+
anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
|
|
79
|
+
anchor = anchor.replace(/ /g, '-');
|
|
80
|
+
|
|
81
|
+
const container = (children: React.ReactNode): React.ReactNode => (
|
|
82
|
+
<>
|
|
83
|
+
{children}
|
|
84
|
+
<a id={anchor} href={`#${anchor}`} className="doc-link"></a>
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'h1':
|
|
90
|
+
return <h1 className="text-2xl font-bold">{container(children)}</h1>;
|
|
91
|
+
case 'h2':
|
|
92
|
+
return <h2 className="text-xl font-bold">{container(children)}</h2>;
|
|
93
|
+
case 'h3':
|
|
94
|
+
return <h3 className="text-lg font-bold">{container(children)}</h3>;
|
|
95
|
+
case 'h4':
|
|
96
|
+
return <h4 className="text-base font-bold">{container(children)}</h4>;
|
|
97
|
+
case 'h5':
|
|
98
|
+
return <h5 className="text-sm font-bold">{container(children)}</h5>;
|
|
99
|
+
case 'h6':
|
|
100
|
+
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
101
|
+
default:
|
|
102
|
+
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
105
103
|
}
|
|
106
104
|
};
|
|
107
105
|
|
|
@@ -134,18 +134,13 @@ type EndpointStatus = 'checking' | 'live' | 'unreachable';
|
|
|
134
134
|
let cachedStatus: EndpointStatus | null = null;
|
|
135
135
|
let inFlight: Promise<EndpointStatus> | null = null;
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
* Statuses that prove the route is being served. A GET is the cheapest question to ask: the handler
|
|
139
|
-
* answers 405 without ever building an MCP server, and a registry answers 401 ahead of that because
|
|
140
|
-
* the credential check runs first. Anything else, 404 and 5xx included, means it is not there.
|
|
141
|
-
*/
|
|
142
|
-
const LIVE_STATUSES = new Set([200, 401, 403, 405]);
|
|
143
|
-
|
|
144
137
|
const probeEndpoint = (url: string): Promise<EndpointStatus> => {
|
|
145
138
|
if (cachedStatus) return Promise.resolve(cachedStatus);
|
|
146
139
|
if (inFlight) return inFlight;
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
// The endpoint answers HEAD with 204 before the credential check. Thus a served route never logs an
|
|
141
|
+
// error status to the console.
|
|
142
|
+
inFlight = fetch(url, { method: 'HEAD' })
|
|
143
|
+
.then((res): EndpointStatus => (res.ok ? 'live' : 'unreachable'))
|
|
149
144
|
.catch((): EndpointStatus => 'unreachable')
|
|
150
145
|
.then((status) => {
|
|
151
146
|
cachedStatus = status;
|
|
@@ -184,29 +179,19 @@ const StatusDot: React.FC<{ status: EndpointStatus; className?: string }> = ({ s
|
|
|
184
179
|
<span role="status" aria-label={DOT[status].label} title={DOT[status].label} className={cn('size-1.5 rounded-full', DOT[status].className, className)} />
|
|
185
180
|
);
|
|
186
181
|
|
|
187
|
-
const Snippet: React.FC<{ label: string; value: string
|
|
182
|
+
const Snippet: React.FC<{ label: string; value: string }> = ({ label, value }) => (
|
|
188
183
|
<div className="min-w-0 space-y-1.5">
|
|
189
184
|
<div className="flex items-center justify-between gap-2">
|
|
190
185
|
<code className="truncate rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">{label}</code>
|
|
191
186
|
<CopyToClipboard value={value} tooltip={`Copy ${label}`} showTooltip={false} className="relative size-7 shrink-0" />
|
|
192
187
|
</div>
|
|
193
|
-
<pre
|
|
194
|
-
className={cn(
|
|
195
|
-
'overflow-auto rounded-md border bg-muted/50 p-3 leading-relaxed',
|
|
196
|
-
compact ? 'max-h-48 text-[10px]' : 'max-h-64 text-xs'
|
|
197
|
-
)}
|
|
198
|
-
>
|
|
188
|
+
<pre className="max-h-64 overflow-auto rounded-md border bg-muted/50 p-3 text-xs leading-relaxed">
|
|
199
189
|
<code>{value}</code>
|
|
200
190
|
</pre>
|
|
201
191
|
</div>
|
|
202
192
|
);
|
|
203
193
|
|
|
204
|
-
const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget
|
|
205
|
-
client,
|
|
206
|
-
name,
|
|
207
|
-
target,
|
|
208
|
-
compact,
|
|
209
|
-
}) => {
|
|
194
|
+
const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget }> = ({ client, name, target }) => {
|
|
210
195
|
// A one-click install would write the `<your-token>` placeholder verbatim and register a server
|
|
211
196
|
// that cannot connect, so a registry gets the snippet and the token pointer instead.
|
|
212
197
|
const deeplink = isRegistryRuntime ? undefined : client.deeplink?.(name, target);
|
|
@@ -222,8 +207,8 @@ const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarge
|
|
|
222
207
|
</a>
|
|
223
208
|
</Button>
|
|
224
209
|
)}
|
|
225
|
-
{command && <Snippet label="command line" value={command}
|
|
226
|
-
<Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)}
|
|
210
|
+
{command && <Snippet label="command line" value={command} />}
|
|
211
|
+
<Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)} />
|
|
227
212
|
<p className="text-xs text-muted-foreground">{client.note}</p>
|
|
228
213
|
</div>
|
|
229
214
|
);
|
|
@@ -261,8 +246,8 @@ const Credential: React.FC = () => (
|
|
|
261
246
|
</p>
|
|
262
247
|
);
|
|
263
248
|
|
|
264
|
-
/** Endpoint, per-client config and credential guidance.
|
|
265
|
-
const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus
|
|
249
|
+
/** Endpoint, per-client config and credential guidance. */
|
|
250
|
+
const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus }> = ({ url, status }) => {
|
|
266
251
|
const name = useServerName();
|
|
267
252
|
const target: McpTarget = { url, headers: isRegistryRuntime ? CREDENTIAL_HEADERS : undefined };
|
|
268
253
|
|
|
@@ -279,7 +264,7 @@ const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus; compact
|
|
|
279
264
|
</TabsList>
|
|
280
265
|
{CLIENTS.map((client) => (
|
|
281
266
|
<TabsContent key={client.id} value={client.id} className="min-w-0">
|
|
282
|
-
<ClientConfig client={client} name={name} target={target}
|
|
267
|
+
<ClientConfig client={client} name={name} target={target} />
|
|
283
268
|
</TabsContent>
|
|
284
269
|
))}
|
|
285
270
|
</Tabs>
|
|
@@ -323,27 +308,3 @@ const McpDialog: React.FC = () => {
|
|
|
323
308
|
</Dialog>
|
|
324
309
|
);
|
|
325
310
|
};
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Inline variant for the mobile nav, which is itself a sheet, so a nested dialog would sit on top of
|
|
329
|
-
* it. It owns its divider, so a build without the endpoint renders nothing and leaves no stray rule.
|
|
330
|
-
*/
|
|
331
|
-
export function McpConfigMobile() {
|
|
332
|
-
return isAvailable ? <McpMobile /> : null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const McpMobile: React.FC = () => {
|
|
336
|
-
const url = useMcpEndpoint();
|
|
337
|
-
const status = useEndpointStatus(url);
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<div className="mt-4 space-y-3 border-t pt-4">
|
|
341
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
342
|
-
<McpIcon className="h-4 w-4" />
|
|
343
|
-
<span className="text-sm font-medium">Connect a coding agent</span>
|
|
344
|
-
<RuntimeModeBadge mode={mode} />
|
|
345
|
-
</div>
|
|
346
|
-
<ConnectionDetails url={url} status={status} compact />
|
|
347
|
-
</div>
|
|
348
|
-
);
|
|
349
|
-
};
|
|
@@ -3,7 +3,7 @@ import { Menu, Monitor, Moon, Sun, X } from 'lucide-react';
|
|
|
3
3
|
import { useTheme } from 'next-themes';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { useRouter } from 'next/router';
|
|
6
|
-
import {
|
|
6
|
+
import { AiAssistantMobileTrigger } from '../../components/AiAssistant/AiAssistantProvider';
|
|
7
7
|
import { Button } from '../../components/ui/button';
|
|
8
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../components/ui/sheet';
|
|
9
9
|
import { cn } from '../../lib/utils';
|
|
@@ -52,6 +52,7 @@ export function MobileNav() {
|
|
|
52
52
|
</SheetTrigger>
|
|
53
53
|
</SheetHeader>
|
|
54
54
|
<div className="mt-8 flex flex-col space-y-4">
|
|
55
|
+
<AiAssistantMobileTrigger />
|
|
55
56
|
{menu &&
|
|
56
57
|
menu.map((section) => {
|
|
57
58
|
const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
|
|
@@ -70,7 +71,6 @@ export function MobileNav() {
|
|
|
70
71
|
</Link>
|
|
71
72
|
);
|
|
72
73
|
})}
|
|
73
|
-
<McpConfigMobile />
|
|
74
74
|
<div className="mt-4 border-t pt-4">
|
|
75
75
|
<Button variant="ghost" className="w-full justify-start font-normal" onClick={toggleTheme}>
|
|
76
76
|
{theme === 'light' && (
|
|
@@ -31,23 +31,9 @@ import {
|
|
|
31
31
|
SidebarMenuSub,
|
|
32
32
|
SidebarSeparator,
|
|
33
33
|
} from '../../components/ui/sidebar';
|
|
34
|
-
import { normalizePathForMatch } from '../../lib/utils';
|
|
34
|
+
import { normalizePathForMatch, stripBasePath } from '../../lib/utils';
|
|
35
35
|
import { useNav } from '../context/NavProvider';
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* `next/link` re-applies `basePath`, but the menu item paths already carry the base prefix (built by
|
|
39
|
-
* the server navigation resolver). Strip the leading base segment once so the link is not
|
|
40
|
-
* double-prefixed; with no base path this just returns a leading-slash route.
|
|
41
|
-
*/
|
|
42
|
-
const stripBasePath = (p: string): string => {
|
|
43
|
-
const base = (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '');
|
|
44
|
-
let rel = p.replace(/^\/+/, '');
|
|
45
|
-
if (base && (rel === base || rel.startsWith(`${base}/`))) {
|
|
46
|
-
rel = rel.slice(base.length).replace(/^\/+/, '');
|
|
47
|
-
}
|
|
48
|
-
return `/${rel}`;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
37
|
const NormalMenuItem = ({ title, icon, path }) => {
|
|
52
38
|
const router = useRouter();
|
|
53
39
|
const isActive = normalizePathForMatch(path) === normalizePathForMatch(router.asPath);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { NavData, SectionLink } from '@handoff/nav';
|
|
4
|
+
import { useRouter } from 'next/router';
|
|
4
5
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
5
6
|
|
|
6
7
|
export type { NavData, NavEntity } from '@handoff/nav';
|
|
@@ -13,6 +14,8 @@ interface INavContext {
|
|
|
13
14
|
const EMPTY_NAV: NavData = { shell: [] };
|
|
14
15
|
const NavContext = createContext<INavContext>({ nav: EMPTY_NAV, currentSectionId: '' });
|
|
15
16
|
const isRegistryMode = process.env.HANDOFF_RUNTIME_MODE === 'registry';
|
|
17
|
+
/** Routes that render no navigation, so they do not fetch it. */
|
|
18
|
+
const NAVLESS_ROUTES = ['/install', '/login', '/reset-password'];
|
|
16
19
|
|
|
17
20
|
// A module-level fulfilled cache and in-flight guard make the registry refresh a single hard-load
|
|
18
21
|
// operation, including StrictMode remounts. A browser hard reload resets both and observes publishes.
|
|
@@ -43,9 +46,11 @@ export const NavProvider: React.FC<{
|
|
|
43
46
|
// Registry alone owns refresh state. Workspace renders the latest page prop directly, so soft
|
|
44
47
|
// navigation cannot briefly expose the preceding page's tree and triggers no provider effect.
|
|
45
48
|
const [registryNav, setRegistryNav] = useState<NavData>(() => cachedNav ?? initialNav);
|
|
49
|
+
const { pathname } = useRouter();
|
|
50
|
+
const hasNav = !NAVLESS_ROUTES.includes(pathname);
|
|
46
51
|
|
|
47
52
|
useEffect(() => {
|
|
48
|
-
if (!isRegistryMode || cachedNav) return;
|
|
53
|
+
if (!isRegistryMode || !hasNav || cachedNav) return;
|
|
49
54
|
let active = true;
|
|
50
55
|
loadNav().then((data) => {
|
|
51
56
|
if (active && data) setRegistryNav(data);
|
|
@@ -53,8 +58,7 @@ export const NavProvider: React.FC<{
|
|
|
53
58
|
return () => {
|
|
54
59
|
active = false;
|
|
55
60
|
};
|
|
56
|
-
|
|
57
|
-
}, []);
|
|
61
|
+
}, [hasNav]);
|
|
58
62
|
|
|
59
63
|
const nav = isRegistryMode ? registryNav : initialNav;
|
|
60
64
|
return <NavContext.Provider value={{ nav, currentSectionId }}>{children}</NavContext.Provider>;
|