handoff-app 2.0.0-9 → 2.0.0
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 → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +813 -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/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/package.js +8 -0
- 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 +5 -0
- package/dist/transformers/utils/build.js +21 -4
- 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/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/package.ts +9 -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 +16 -3
- 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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { resolveEnvReferences } from '@handoff/config/from-env';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Import a consumer's server-only module (a custom asset storage, AI or email
|
|
8
|
+
* provider) by the path the config gives, relative to the project.
|
|
9
|
+
*
|
|
10
|
+
* A registry bundle mirrors the tracing root and runs with its root as the working directory, so the
|
|
11
|
+
* traced copy is at `<cwd>/<HANDOFF_BUNDLE_PROJECT_DIR>/<module>`. A workspace server reads the
|
|
12
|
+
* original file under `HANDOFF_WORKING_PATH`. A bare import would resolve against the webpack chunk.
|
|
13
|
+
*/
|
|
14
|
+
export const importServerModule = async (specifier: string): Promise<any> => {
|
|
15
|
+
const candidates = path.isAbsolute(specifier)
|
|
16
|
+
? [specifier]
|
|
17
|
+
: [
|
|
18
|
+
path.resolve(process.cwd(), process.env.HANDOFF_BUNDLE_PROJECT_DIR ?? '', specifier),
|
|
19
|
+
path.resolve(process.env.HANDOFF_WORKING_PATH ?? process.cwd(), specifier),
|
|
20
|
+
];
|
|
21
|
+
const target = candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[candidates.length - 1];
|
|
22
|
+
return import(/* webpackIgnore: true */ pathToFileURL(target).href);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Import a custom module and return its default export. A factory export is called first, with
|
|
27
|
+
* `fromEnv()` values in `context.options` resolved.
|
|
28
|
+
*/
|
|
29
|
+
export const loadServerModule = async (specifier: string, context: { options?: Record<string, unknown>; [key: string]: unknown }): Promise<unknown> => {
|
|
30
|
+
const mod = await importServerModule(specifier);
|
|
31
|
+
const exported = mod?.default ?? mod;
|
|
32
|
+
if (typeof exported !== 'function') return exported;
|
|
33
|
+
return exported({ ...context, options: resolveEnvReferences(context.options ?? {}, process.env), env: process.env });
|
|
34
|
+
};
|
package/src/app/lib/utils.ts
CHANGED
|
@@ -14,6 +14,19 @@ export const toAbsolutePath = (input: string): string => {
|
|
|
14
14
|
return `/${trimSlashes(input)}`;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A server-built path with the app's base path removed, so `next/link` does not re-apply it. Menu
|
|
19
|
+
* items and tool sources already carry the prefix; with no base path this returns the route as is.
|
|
20
|
+
*/
|
|
21
|
+
export const stripBasePath = (input: string): string => {
|
|
22
|
+
const base = trimSlashes(process.env.HANDOFF_APP_BASE_PATH ?? '');
|
|
23
|
+
let rel = input.replace(/^\/+/, '');
|
|
24
|
+
if (base && (rel === base || rel.startsWith(`${base}/`))) {
|
|
25
|
+
rel = rel.slice(base.length).replace(/^\/+/, '');
|
|
26
|
+
}
|
|
27
|
+
return `/${rel}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
17
30
|
export const normalizePathForMatch = (input: string): string => {
|
|
18
31
|
const [pathname] = input.split(/[?#]/);
|
|
19
32
|
return trimSlashes(pathname);
|
|
@@ -38,3 +51,10 @@ export const filterOutNull = <T>(value: T): value is NonNullable<T> => value !==
|
|
|
38
51
|
*/
|
|
39
52
|
export const hasRenderableNav = (section?: SectionLink | null): boolean =>
|
|
40
53
|
!!section?.subSections?.some((sub) => (sub.menu && sub.menu.length > 0) || !!sub.dynamic);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The browser title: a page's own `metaTitle` as written, otherwise its title followed by the
|
|
57
|
+
* site title from `app.title`.
|
|
58
|
+
*/
|
|
59
|
+
export const pageTitle = (metadata: { title?: string; metaTitle?: string }, siteTitle?: string): string =>
|
|
60
|
+
metadata.metaTitle || [metadata.title, siteTitle].filter(Boolean).join(' | ');
|
package/src/app/next.config.mjs
CHANGED
|
@@ -18,18 +18,7 @@ const resolveBasePath = (rawBasePath) => {
|
|
|
18
18
|
// - otherwise → a normal server (workspace `next dev`/`start`).
|
|
19
19
|
const handoffBuildTarget = process.env.HANDOFF_BUILD_TARGET;
|
|
20
20
|
const handoffWorkingPath = path.resolve('%HANDOFF_WORKING_PATH%');
|
|
21
|
-
|
|
22
|
-
// A configured custom asset-storage adapter module (relative to the consumer project). It is loaded
|
|
23
|
-
// by a variable dynamic import at runtime, which nft cannot statically trace, so force it (and its
|
|
24
|
-
// resolvable deps) into the registry bundle. Empty/unset for the built-in database/Vercel adapters.
|
|
25
|
-
const handoffAssetStorageModule = '%HANDOFF_ASSET_STORAGE_MODULE%';
|
|
26
|
-
const resolveAssetStorageInclude = () => {
|
|
27
|
-
if (handoffBuildTarget !== 'registry' || !handoffAssetStorageModule || handoffAssetStorageModule.startsWith('%HANDOFF_')) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const abs = path.isAbsolute(handoffAssetStorageModule) ? handoffAssetStorageModule : path.resolve(handoffWorkingPath, handoffAssetStorageModule);
|
|
31
|
-
return { '/api/**': [abs] };
|
|
32
|
-
};
|
|
21
|
+
const handoffTracingRoot = path.resolve('%HANDOFF_TRACING_ROOT%');
|
|
33
22
|
|
|
34
23
|
const resolveOutputMode = (target) => {
|
|
35
24
|
if (target === 'static') {
|
|
@@ -49,11 +38,10 @@ const resolveOutputMode = (target) => {
|
|
|
49
38
|
/** @type {import('next').NextConfig} */
|
|
50
39
|
const nextConfig = {
|
|
51
40
|
output: resolveOutputMode(handoffBuildTarget),
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
outputFileTracingRoot: handoffBuildTarget === 'registry' ? handoffWorkingPath : undefined,
|
|
41
|
+
// Rooted at the common ancestor of the staged app and the `node_modules` that the runtime
|
|
42
|
+
// dependencies resolve from. The builder bakes this value (`resolveRegistryTracingRoot`), so the
|
|
43
|
+
// assembly step uses the same root. This applies to the registry target only.
|
|
44
|
+
outputFileTracingRoot: handoffBuildTarget === 'registry' ? handoffTracingRoot : undefined,
|
|
57
45
|
// nft speculatively traces the export-only paths the app references (`export-detail.json` and the
|
|
58
46
|
// materializer's `.next/export/**` targets) as runtime deps. They never exist in a `standalone`
|
|
59
47
|
// build, and `copyTracedFiles` copies traced files with no existence guard, so leaving them in
|
|
@@ -61,9 +49,6 @@ const nextConfig = {
|
|
|
61
49
|
// Registry-only; static export, which legitimately produces these files, is untouched.
|
|
62
50
|
outputFileTracingExcludes:
|
|
63
51
|
handoffBuildTarget === 'registry' ? { '**': ['**/export-detail.json', '**/.next/export/**'] } : undefined,
|
|
64
|
-
// Force a configured custom asset-storage module into the registry trace (dynamic import is opaque
|
|
65
|
-
// to nft). Its SDK deps are additionally asserted via `getRequiredRegistryRuntimeModules`.
|
|
66
|
-
outputFileTracingIncludes: resolveAssetStorageInclude(),
|
|
67
52
|
reactStrictMode: true,
|
|
68
53
|
pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
|
69
54
|
trailingSlash: true,
|
|
@@ -102,6 +87,9 @@ const nextConfig = {
|
|
|
102
87
|
// static export). Read straight from the build env — empty for workspace dev/start.
|
|
103
88
|
HANDOFF_BUILD_TARGET: handoffBuildTarget ?? '',
|
|
104
89
|
HANDOFF_WORKING_PATH: '%HANDOFF_WORKING_PATH%',
|
|
90
|
+
// Where the project sits inside a registry bundle, which mirrors the tracing root. Custom server
|
|
91
|
+
// modules are imported from there (see `lib/server-module.ts`).
|
|
92
|
+
HANDOFF_BUNDLE_PROJECT_DIR: path.relative(handoffTracingRoot, handoffWorkingPath),
|
|
105
93
|
HANDOFF_MODULE_PATH: '%HANDOFF_MODULE_PATH%',
|
|
106
94
|
HANDOFF_EXPORT_PATH: '%HANDOFF_EXPORT_PATH%',
|
|
107
95
|
HANDOFF_WEBSOCKET_PORT: '%HANDOFF_WEBSOCKET_PORT%',
|
|
@@ -114,15 +102,29 @@ const nextConfig = {
|
|
|
114
102
|
// Whether this build serves `/api/mcp/`. Both the route and the header UI read this, so a
|
|
115
103
|
// disabled build hides the affordance instead of showing it fail against a 404.
|
|
116
104
|
HANDOFF_MCP_ENABLED: '%HANDOFF_MCP_ENABLED%',
|
|
105
|
+
// Whether this build serves the docs AI assistant. Config-only and baked, like the MCP flag, so
|
|
106
|
+
// a build without it leaves out both `/api/ai/*` and the header control.
|
|
107
|
+
HANDOFF_AI_ENABLED: '%HANDOFF_AI_ENABLED%',
|
|
108
|
+
// The connection list `runtime.ai.connections` declared (env-var *names* and non-secret options
|
|
109
|
+
// only). Deliberately not named `HANDOFF_AI_CONNECTIONS`: Next inlines every name in this block
|
|
110
|
+
// at build time, which would shadow the deployment-supplied list of that name that the server
|
|
111
|
+
// reads at request time and merges over this one.
|
|
112
|
+
HANDOFF_AI_BAKED_CONNECTIONS: '%HANDOFF_AI_BAKED_CONNECTIONS%',
|
|
113
|
+
HANDOFF_AI_DEFAULT_MODEL: '%HANDOFF_AI_DEFAULT_MODEL%',
|
|
117
114
|
HANDOFF_REGISTRY_DRIVER: '%HANDOFF_REGISTRY_DRIVER%',
|
|
118
115
|
HANDOFF_REGISTRY_DATABASE_URL_ENV: '%HANDOFF_REGISTRY_DATABASE_URL_ENV%',
|
|
119
|
-
// Asset storage
|
|
120
|
-
//
|
|
121
|
-
|
|
116
|
+
// Asset storage provider, module, and options JSON. Option references keep only their variable
|
|
117
|
+
// name and are resolved at request time, so secret values are never baked.
|
|
118
|
+
HANDOFF_ASSET_STORAGE_PROVIDER: '%HANDOFF_ASSET_STORAGE_PROVIDER%',
|
|
122
119
|
HANDOFF_ASSET_STORAGE_MODULE: '%HANDOFF_ASSET_STORAGE_MODULE%',
|
|
123
|
-
HANDOFF_ASSET_STORAGE_TOKEN_ENV: '%HANDOFF_ASSET_STORAGE_TOKEN_ENV%',
|
|
124
120
|
HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES: '%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%',
|
|
125
121
|
HANDOFF_ASSET_STORAGE_OPTIONS: '%HANDOFF_ASSET_STORAGE_OPTIONS%',
|
|
122
|
+
// Registry email provider, sender, module, and options JSON. Option references keep only their
|
|
123
|
+
// env-var names; the values are read at request time.
|
|
124
|
+
HANDOFF_EMAIL_PROVIDER: '%HANDOFF_EMAIL_PROVIDER%',
|
|
125
|
+
HANDOFF_EMAIL_FROM: '%HANDOFF_EMAIL_FROM%',
|
|
126
|
+
HANDOFF_EMAIL_MODULE: '%HANDOFF_EMAIL_MODULE%',
|
|
127
|
+
HANDOFF_EMAIL_OPTIONS: '%HANDOFF_EMAIL_OPTIONS%',
|
|
126
128
|
},
|
|
127
129
|
images: {
|
|
128
130
|
unoptimized: true,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../../components/util';
|
|
21
21
|
import { resolveDocsBackend } from '../../lib/docs-api/backend';
|
|
22
22
|
import { documentationMetadata, REGISTRY_PAGE_REVALIDATE_SECONDS } from '../../lib/docs-api/page-rendering';
|
|
23
|
+
import { pageTitle } from '../../lib/utils';
|
|
23
24
|
import { Home, getStaticProps as getHomeStaticProps } from '../index';
|
|
24
25
|
|
|
25
26
|
type CatchAllDocumentationProps = DocumentationProps & { homeAlias?: boolean };
|
|
@@ -114,7 +115,7 @@ export default function DocCatchAllPage({
|
|
|
114
115
|
return (
|
|
115
116
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
|
|
116
117
|
<Head>
|
|
117
|
-
<title>
|
|
118
|
+
<title>{pageTitle({ title: 'Page Not Found' }, config?.app?.title)}</title>
|
|
118
119
|
<meta name="description" content="Page Not Found" />
|
|
119
120
|
</Head>
|
|
120
121
|
<NotFound />
|
package/src/app/pages/_app.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppProps } from 'next/app';
|
|
2
2
|
|
|
3
|
+
import { AiAssistantProvider } from '../components/AiAssistant/AiAssistantProvider';
|
|
3
4
|
import { RegistrySessionProvider } from '../components/Auth/RegistrySessionProvider';
|
|
4
5
|
import { RegistryInstallGate } from '../components/Auth/RegistryInstallGate';
|
|
5
6
|
import { NavProvider } from '../components/context/NavProvider';
|
|
@@ -13,8 +14,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|
|
13
14
|
<RegistrySessionProvider>
|
|
14
15
|
<RegistryInstallGate>
|
|
15
16
|
<NavProvider initialNav={pageProps.navData} currentSectionId={pageProps.currentSectionId}>
|
|
16
|
-
{/*
|
|
17
|
-
<
|
|
17
|
+
{/* Above the page tree: every page builds its own Layout, which a soft navigation replaces. */}
|
|
18
|
+
<AiAssistantProvider>
|
|
19
|
+
{/* @ts-ignore */}
|
|
20
|
+
<Component {...pageProps} />
|
|
21
|
+
</AiAssistantProvider>
|
|
18
22
|
</NavProvider>
|
|
19
23
|
</RegistryInstallGate>
|
|
20
24
|
</RegistrySessionProvider>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
|
2
2
|
import Script from 'next/script';
|
|
3
|
+
import { dockWidthScript } from '../components/AiAssistant/dockState';
|
|
3
4
|
import { getClientRuntimeConfig } from '../components/util';
|
|
4
5
|
|
|
5
6
|
const config = getClientRuntimeConfig();
|
|
6
7
|
|
|
8
|
+
// A build with no assistant must not reserve a column for state an earlier build left behind.
|
|
9
|
+
const hasAssistant = process.env.HANDOFF_AI_ENABLED === 'true' && process.env.HANDOFF_BUILD_TARGET !== 'static';
|
|
10
|
+
|
|
7
11
|
class MyDocument extends Document {
|
|
8
12
|
render() {
|
|
9
13
|
return (
|
|
@@ -11,6 +15,8 @@ class MyDocument extends Document {
|
|
|
11
15
|
<Head>
|
|
12
16
|
<link rel="shortcut icon" href={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/favicon.ico`} />
|
|
13
17
|
<link rel="icon" sizes="16x16 32x32 64x64" href={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/favicon.ico`} />
|
|
18
|
+
{/* Blocking on purpose: it reserves the assistant's column before the first paint. */}
|
|
19
|
+
{hasAssistant && <script dangerouslySetInnerHTML={{ __html: dockWidthScript }} />}
|
|
14
20
|
{config?.app?.google_tag_manager && (
|
|
15
21
|
<Script id="google-tag-manager" strategy="afterInteractive">
|
|
16
22
|
{`
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ClientConfig } from '@handoff/types/config';
|
|
2
|
+
import type { GetStaticProps } from 'next';
|
|
3
|
+
import { getClientRuntimeConfig } from '../../components/util';
|
|
4
|
+
import { Check, KeyRound, Trash2 } from 'lucide-react';
|
|
5
|
+
import { useSession } from 'next-auth/react';
|
|
6
|
+
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { AccountLayout } from '../../components/Auth/AccountLayout';
|
|
8
|
+
import { authApiUrl, readApiError } from '../../components/Auth/api';
|
|
9
|
+
import { Alert, AlertDescription } from '../../components/ui/alert';
|
|
10
|
+
import { Badge } from '../../components/ui/badge';
|
|
11
|
+
import { Button } from '../../components/ui/button';
|
|
12
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
|
|
13
|
+
import { ConfirmDialog } from '../../components/ui/confirm-dialog';
|
|
14
|
+
import { Input } from '../../components/ui/input';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `/account/ai`: the one reader-facing setting this feature adds.
|
|
18
|
+
*
|
|
19
|
+
* One list, no add form. Each row is a connection the config declares with `credential: 'user'`,
|
|
20
|
+
* its endpoint and models shown as read-only text. There is no endpoint or model to enter.
|
|
21
|
+
*
|
|
22
|
+
* A key is write-only: once saved, the row shows only that it is configured.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface AiKeyConnection {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
models?: string[];
|
|
30
|
+
configured: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
|
|
34
|
+
|
|
35
|
+
export default function AiKeysPage({ config }: { config: ClientConfig }) {
|
|
36
|
+
if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
|
|
37
|
+
return <AccountLayout config={config} title="AI providers">{null}</AccountLayout>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <RegistryAiKeysPage config={config} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function RegistryAiKeysPage({ config }: { config: ClientConfig }) {
|
|
44
|
+
const { data: session } = useSession();
|
|
45
|
+
const [connections, setConnections] = useState<AiKeyConnection[]>([]);
|
|
46
|
+
const [pending, setPending] = useState<string | null>(null);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [saved, setSaved] = useState<string | null>(null);
|
|
49
|
+
const [connectionToRemove, setConnectionToRemove] = useState<AiKeyConnection | null>(null);
|
|
50
|
+
|
|
51
|
+
const load = useCallback(async () => {
|
|
52
|
+
const response = await fetch(authApiUrl('/api/account/ai/keys'), { credentials: 'include', cache: 'no-store' });
|
|
53
|
+
if (!response.ok) throw new Error(await readApiError(response, 'Could not load AI providers.'));
|
|
54
|
+
const body = (await response.json()) as { connections: AiKeyConnection[] };
|
|
55
|
+
setConnections(body.connections);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (session?.user) void load().catch((reason: Error) => setError(reason.message));
|
|
60
|
+
}, [load, session]);
|
|
61
|
+
|
|
62
|
+
const save = async (connection: AiKeyConnection, event: FormEvent<HTMLFormElement>) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
const form = event.currentTarget;
|
|
65
|
+
const key = String(new FormData(form).get('key') ?? '').trim();
|
|
66
|
+
setPending(connection.id);
|
|
67
|
+
setError(null);
|
|
68
|
+
setSaved(null);
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(authApiUrl(`/api/account/ai/keys/${encodeURIComponent(connection.id)}`), {
|
|
71
|
+
method: 'PUT',
|
|
72
|
+
credentials: 'include',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ key }),
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
setError(await readApiError(response, 'Could not save the API key.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
form.reset();
|
|
81
|
+
setSaved(connection.id);
|
|
82
|
+
await load();
|
|
83
|
+
} catch {
|
|
84
|
+
setError('Could not connect to the registry.');
|
|
85
|
+
} finally {
|
|
86
|
+
setPending(null);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const remove = async (connection: AiKeyConnection) => {
|
|
91
|
+
setPending(connection.id);
|
|
92
|
+
setError(null);
|
|
93
|
+
setSaved(null);
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(authApiUrl(`/api/account/ai/keys/${encodeURIComponent(connection.id)}`), {
|
|
96
|
+
method: 'DELETE',
|
|
97
|
+
credentials: 'include',
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
setError(await readApiError(response, 'Could not remove the API key.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await load();
|
|
104
|
+
} finally {
|
|
105
|
+
setPending(null);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<AccountLayout config={config} title="AI providers" description="Add your own keys for the AI providers this site uses.">
|
|
111
|
+
<div className="space-y-6">
|
|
112
|
+
{error && (
|
|
113
|
+
<Alert variant="destructive">
|
|
114
|
+
<AlertDescription>{error}</AlertDescription>
|
|
115
|
+
</Alert>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{connections.length === 0 ? (
|
|
119
|
+
<p className="text-sm text-muted-foreground">This site does not ask readers for their own AI provider keys.</p>
|
|
120
|
+
) : (
|
|
121
|
+
connections.map((connection) => (
|
|
122
|
+
<Card key={connection.id}>
|
|
123
|
+
<CardHeader>
|
|
124
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
125
|
+
<CardTitle className="text-base">{connection.label}</CardTitle>
|
|
126
|
+
{connection.configured && (
|
|
127
|
+
<Badge variant="secondary" className="gap-1">
|
|
128
|
+
<Check className="h-3 w-3" />
|
|
129
|
+
Configured
|
|
130
|
+
</Badge>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
<CardDescription>
|
|
134
|
+
{connection.baseUrl}
|
|
135
|
+
{connection.models?.length ? ` · ${connection.models.join(', ')}` : ''}
|
|
136
|
+
</CardDescription>
|
|
137
|
+
</CardHeader>
|
|
138
|
+
<CardContent>
|
|
139
|
+
{connection.configured ? (
|
|
140
|
+
<Button variant="outline" onClick={() => setConnectionToRemove(connection)} disabled={pending === connection.id}>
|
|
141
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
142
|
+
Remove
|
|
143
|
+
</Button>
|
|
144
|
+
) : (
|
|
145
|
+
<form onSubmit={(event) => void save(connection, event)} className="flex flex-wrap items-center gap-2">
|
|
146
|
+
<Input
|
|
147
|
+
name="key"
|
|
148
|
+
type="password"
|
|
149
|
+
autoComplete="off"
|
|
150
|
+
placeholder={`${connection.label} API key`}
|
|
151
|
+
className="max-w-sm"
|
|
152
|
+
required
|
|
153
|
+
/>
|
|
154
|
+
<Button type="submit" disabled={pending === connection.id}>
|
|
155
|
+
<KeyRound className="mr-2 h-4 w-4" />
|
|
156
|
+
Save
|
|
157
|
+
</Button>
|
|
158
|
+
{saved === connection.id && <span className="text-sm text-muted-foreground">Saved.</span>}
|
|
159
|
+
</form>
|
|
160
|
+
)}
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
))
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{connectionToRemove ? (
|
|
168
|
+
<ConfirmDialog
|
|
169
|
+
open
|
|
170
|
+
onOpenChange={(open) => {
|
|
171
|
+
if (!open) setConnectionToRemove(null);
|
|
172
|
+
}}
|
|
173
|
+
title={`Remove your ${connectionToRemove.label} API key?`}
|
|
174
|
+
description="You can add a new key at any time."
|
|
175
|
+
confirmLabel="Remove"
|
|
176
|
+
pendingLabel="Removing…"
|
|
177
|
+
variant="destructive"
|
|
178
|
+
onConfirm={() => remove(connectionToRemove)}
|
|
179
|
+
/>
|
|
180
|
+
) : null}
|
|
181
|
+
</AccountLayout>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { ClientConfig } from '@handoff/types/config';
|
|
2
|
+
import type { GetStaticProps } from 'next';
|
|
3
|
+
import { getClientRuntimeConfig } from '../../components/util';
|
|
1
4
|
import { AccountLayout } from '../../components/Auth/AccountLayout';
|
|
2
5
|
import { authApiUrl, readApiError } from '../../components/Auth/api';
|
|
3
6
|
import { Alert, AlertDescription } from '../../components/ui/alert';
|
|
4
7
|
import { Badge } from '../../components/ui/badge';
|
|
5
8
|
import { Button } from '../../components/ui/button';
|
|
6
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
|
|
7
9
|
import { Input } from '../../components/ui/input';
|
|
8
10
|
import { Label } from '../../components/ui/label';
|
|
9
11
|
import { useSession } from 'next-auth/react';
|
|
@@ -19,15 +21,17 @@ interface AccountUser {
|
|
|
19
21
|
status?: 'invited' | 'active' | 'deactivated';
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export
|
|
24
|
+
export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
|
|
25
|
+
|
|
26
|
+
export default function AccountPage({ config }: { config: ClientConfig }) {
|
|
23
27
|
if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
|
|
24
|
-
return <AccountLayout title="Profile">{null}</AccountLayout>;
|
|
28
|
+
return <AccountLayout config={config} title="Profile">{null}</AccountLayout>;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
return <RegistryAccountPage />;
|
|
31
|
+
return <RegistryAccountPage config={config} />;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
function RegistryAccountPage() {
|
|
34
|
+
function RegistryAccountPage({ config }: { config: ClientConfig }) {
|
|
31
35
|
const { data: session } = useSession();
|
|
32
36
|
const [user, setUser] = useState<AccountUser | null>(null);
|
|
33
37
|
const [name, setName] = useState('');
|
|
@@ -83,71 +87,67 @@ function RegistryAccountPage() {
|
|
|
83
87
|
.toUpperCase();
|
|
84
88
|
|
|
85
89
|
return (
|
|
86
|
-
<AccountLayout title="Profile">
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<div>
|
|
110
|
-
<p className="font-medium">{name || user.email}</p>
|
|
111
|
-
<Badge variant={user.status === 'deactivated' ? 'destructive' : 'secondary'} className="mt-1">
|
|
112
|
-
{user.role}
|
|
113
|
-
</Badge>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div className="space-y-2">
|
|
117
|
-
<Label htmlFor="account-email">Email</Label>
|
|
118
|
-
<Input id="account-email" value={user.email} disabled />
|
|
119
|
-
<p className="text-xs text-muted-foreground">Email addresses cannot be changed.</p>
|
|
120
|
-
</div>
|
|
121
|
-
<div className="space-y-2">
|
|
122
|
-
<Label htmlFor="account-name">Display name</Label>
|
|
123
|
-
<Input id="account-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={100} />
|
|
90
|
+
<AccountLayout config={config} title="Profile" description="Manage how your account appears in this registry.">
|
|
91
|
+
<div className="space-y-5">
|
|
92
|
+
{message ? (
|
|
93
|
+
<Alert variant={message.error ? 'destructive' : 'green'}>
|
|
94
|
+
<AlertDescription>{message.text}</AlertDescription>
|
|
95
|
+
</Alert>
|
|
96
|
+
) : null}
|
|
97
|
+
{loading ? (
|
|
98
|
+
<p className="text-sm text-muted-foreground">Loading profile…</p>
|
|
99
|
+
) : user ? (
|
|
100
|
+
<>
|
|
101
|
+
<div className="flex items-center gap-4">
|
|
102
|
+
{avatarUrl ? (
|
|
103
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
104
|
+
<img src={avatarUrl} alt="" className="h-16 w-16 rounded-full border object-cover" />
|
|
105
|
+
) : (
|
|
106
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-semibold">{initials}</div>
|
|
107
|
+
)}
|
|
108
|
+
<div>
|
|
109
|
+
<p className="font-medium">{name || user.email}</p>
|
|
110
|
+
<Badge variant={user.status === 'deactivated' ? 'destructive' : 'secondary'} className="mt-1">
|
|
111
|
+
{user.role}
|
|
112
|
+
</Badge>
|
|
124
113
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
114
|
+
</div>
|
|
115
|
+
<div className="space-y-2">
|
|
116
|
+
<Label htmlFor="account-email">Email</Label>
|
|
117
|
+
<Input id="account-email" value={user.email} disabled />
|
|
118
|
+
<p className="text-xs text-muted-foreground">Email addresses cannot be changed.</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
<Label htmlFor="account-name">Display name</Label>
|
|
122
|
+
<Input id="account-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={100} />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="space-y-2">
|
|
125
|
+
<div className="flex items-center justify-between gap-2">
|
|
126
|
+
<Label htmlFor="account-avatar">Avatar URL</Label>
|
|
127
|
+
{gravatarUrl ? (
|
|
128
|
+
<Button type="button" variant="ghost" size="sm" onClick={() => setAvatarUrl(gravatarUrl)}>
|
|
129
|
+
Use Gravatar
|
|
130
|
+
</Button>
|
|
131
|
+
) : null}
|
|
143
132
|
</div>
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
133
|
+
<Input
|
|
134
|
+
id="account-avatar"
|
|
135
|
+
type="url"
|
|
136
|
+
placeholder="https://example.com/avatar.png"
|
|
137
|
+
value={avatarUrl}
|
|
138
|
+
onChange={(event) => setAvatarUrl(event.target.value)}
|
|
139
|
+
maxLength={500}
|
|
140
|
+
/>
|
|
141
|
+
<p className="text-xs text-muted-foreground">
|
|
142
|
+
Use an HTTPS image URL, click Use Gravatar, or leave this empty to show your initials.
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
<Button onClick={() => void save()} disabled={saving}>
|
|
146
|
+
{saving ? 'Saving…' : 'Save changes'}
|
|
147
|
+
</Button>
|
|
148
|
+
</>
|
|
149
|
+
) : null}
|
|
150
|
+
</div>
|
|
151
151
|
</AccountLayout>
|
|
152
152
|
);
|
|
153
153
|
}
|