@current-docs/embed 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/package.json +47 -0
- package/src/adaptive.tsx +57 -0
- package/src/api-page.tsx +4 -0
- package/src/docs-action.tsx +48 -0
- package/src/index.tsx +56 -0
- package/templates/app/api/docs-proxy/route.ts +8 -0
- package/templates/app/api/docs-search/route.ts +5 -0
- package/templates/app/help/[[...slug]]/page.tsx +41 -0
- package/templates/app/help/layout.tsx +16 -0
- package/templates/lib/docs-source.ts +16 -0
- package/templates/mdx-components.tsx +9 -0
- package/templates/source.config.ts +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dror Ivry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @current-docs/embed
|
|
2
|
+
|
|
3
|
+
Embeddable Current docs surface for Next.js App Router hosts. It re-exports the Fumadocs layout, page, search, and OpenAPI building blocks behind one stable import, plus `templates/` with the files the docs-embed skill copies into a host app (route group, search route, OpenAPI proxy).
|
|
4
|
+
|
|
5
|
+
The package ships as TypeScript source, not compiled JS. The host's Next.js build transpiles it, so `transpilePackages: ['@current-docs/embed']` in `next.config.ts` is required. The `fumadocs-*` packages are regular dependencies and come with the install; the host only needs `next`, `react`, and `react-dom` (peer deps).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @current-docs/embed fumadocs-mdx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Inside this monorepo use `"@current-docs/embed": "workspace:*"`; in an onboarded repo use `"@current-docs/embed": "^0.1.0"` from npm.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// next.config.ts: transpilePackages: ['@current-docs/embed'], wrap with createMDX()
|
|
19
|
+
// lib/docs-source.ts
|
|
20
|
+
import { createOpenAPI, loader } from '@current-docs/embed';
|
|
21
|
+
import { docs } from '@/.source/server';
|
|
22
|
+
|
|
23
|
+
export const source = loader({ baseUrl: '/help', source: docs.toFumadocsSource() });
|
|
24
|
+
|
|
25
|
+
// app/help/layout.tsx
|
|
26
|
+
import { DocsLayout, RootProvider } from '@current-docs/embed';
|
|
27
|
+
// wrap children: <RootProvider><DocsLayout tree={source.getPageTree()}>{children}</DocsLayout></RootProvider>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The full wiring (layout, catch-all page, search and proxy routes, source config) lives in `templates/`; the docs-embed skill copies them in and adjusts paths.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@current-docs/embed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embeddable Current docs surface for Next.js App Router hosts: layout, page, search, and OpenAPI components plus install templates. Published as TypeScript source; consume via transpilePackages.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/drorIvry/current-docs.git",
|
|
10
|
+
"directory": "src/embed"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"templates",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/index.tsx",
|
|
25
|
+
"./api-page": "./src/api-page.tsx",
|
|
26
|
+
"./templates/*": "./templates/*"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"fumadocs-core": "^16.10.7",
|
|
30
|
+
"fumadocs-mdx": "^15.0.13",
|
|
31
|
+
"fumadocs-openapi": "^11.0.6",
|
|
32
|
+
"fumadocs-ui": "^16.10.7"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"next": "^16.0.0",
|
|
36
|
+
"react": "^19.2.0",
|
|
37
|
+
"react-dom": "^19.2.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
42
|
+
"@types/mdx": "^2.0.13"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"lint": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/adaptive.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Adaptive content + action registry primitives for the embedded docs.
|
|
2
|
+
// Claims come from the HOST's own session (auth-inherit); the standalone
|
|
3
|
+
// public site passes no claims, so gated content stays hidden there.
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface VisitorClaims {
|
|
7
|
+
roles: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RoleGateProps {
|
|
11
|
+
role: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Bind RoleGate to the host session's claims. */
|
|
16
|
+
export function makeRoleGate(claims?: VisitorClaims) {
|
|
17
|
+
return function RoleGate({ role, children }: RoleGateProps) {
|
|
18
|
+
if (!claims?.roles.includes(role)) return null;
|
|
19
|
+
return <>{children}</>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** JSON-schema described action a host app exposes to the docs surface. */
|
|
24
|
+
export interface DocsActionDef {
|
|
25
|
+
description: string;
|
|
26
|
+
/** JSON schema for args; validated by the host's action endpoint. */
|
|
27
|
+
schema: Record<string, unknown>;
|
|
28
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ActionRegistry = Record<string, DocsActionDef>;
|
|
32
|
+
|
|
33
|
+
/** Minimal required-property validation against the action's JSON schema. */
|
|
34
|
+
export function validateActionArgs(def: DocsActionDef, args: Record<string, unknown>): string | null {
|
|
35
|
+
const required = (def.schema.required as string[] | undefined) ?? [];
|
|
36
|
+
for (const key of required) {
|
|
37
|
+
if (!(key in args)) return `missing required arg: ${key}`;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Serve an action registry as a POST handler ({name, args} -> {result}). */
|
|
43
|
+
export function createActionsHandler(registry: ActionRegistry) {
|
|
44
|
+
return async function POST(req: Request): Promise<Response> {
|
|
45
|
+
const body = (await req.json().catch(() => null)) as { name?: string; args?: Record<string, unknown> } | null;
|
|
46
|
+
const def = body?.name ? registry[body.name] : undefined;
|
|
47
|
+
if (!def) return Response.json({ error: `unknown action: ${body?.name ?? '(none)'}` }, { status: 404 });
|
|
48
|
+
const args = body?.args ?? {};
|
|
49
|
+
const invalid = validateActionArgs(def, args);
|
|
50
|
+
if (invalid) return Response.json({ error: invalid }, { status: 400 });
|
|
51
|
+
try {
|
|
52
|
+
return Response.json({ result: await def.handler(args) });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/api-page.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// A docs-embedded button that invokes a host-registered action. On the public
|
|
3
|
+
// standalone site (no endpoint) it renders as an inert hint instead.
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface DocsActionProps {
|
|
7
|
+
name: string;
|
|
8
|
+
label: string;
|
|
9
|
+
args?: Record<string, unknown>;
|
|
10
|
+
/** Host endpoint serving the action registry; absent = standalone site. */
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DocsAction({ name, label, args, endpoint }: DocsActionProps) {
|
|
15
|
+
const [state, setState] = useState<string>('');
|
|
16
|
+
if (!endpoint) {
|
|
17
|
+
return <em data-docs-action={name}>{label} (available inside the app)</em>;
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<span style={{ display: 'inline-flex', gap: '0.5rem', alignItems: 'center' }}>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
data-docs-action={name}
|
|
24
|
+
onClick={async () => {
|
|
25
|
+
setState('running');
|
|
26
|
+
const res = await fetch(endpoint, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ name, args: args ?? {} }),
|
|
30
|
+
});
|
|
31
|
+
const body = (await res.json()) as { result?: unknown; error?: string };
|
|
32
|
+
setState(res.ok ? `done: ${JSON.stringify(body.result).slice(0, 80)}` : `error: ${body.error}`);
|
|
33
|
+
}}
|
|
34
|
+
style={{
|
|
35
|
+
background: 'var(--cur-primary, #4f46e5)',
|
|
36
|
+
color: '#fff',
|
|
37
|
+
border: 'none',
|
|
38
|
+
borderRadius: 8,
|
|
39
|
+
padding: '0.4rem 0.8rem',
|
|
40
|
+
cursor: 'pointer',
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{label}
|
|
44
|
+
</button>
|
|
45
|
+
<code style={{ fontSize: '0.8em' }}>{state}</code>
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Embeddable docs surface for a host Next.js App Router app.
|
|
2
|
+
// The host mounts docs (browse + search + API reference) as a route group and
|
|
3
|
+
// keeps its own auth: whatever gates the mounting route gates the docs.
|
|
4
|
+
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|
5
|
+
import { makeRoleGate, type VisitorClaims } from './adaptive';
|
|
6
|
+
import { DocsAction } from './docs-action';
|
|
7
|
+
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
|
8
|
+
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
|
9
|
+
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
|
10
|
+
import { Card, Cards } from 'fumadocs-ui/components/card';
|
|
11
|
+
import type { MDXComponents } from 'mdx/types';
|
|
12
|
+
|
|
13
|
+
export { RootProvider } from 'fumadocs-ui/provider/next';
|
|
14
|
+
export { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|
15
|
+
export {
|
|
16
|
+
DocsBody,
|
|
17
|
+
DocsDescription,
|
|
18
|
+
DocsPage,
|
|
19
|
+
DocsTitle,
|
|
20
|
+
} from 'fumadocs-ui/layouts/docs/page';
|
|
21
|
+
export { loader } from 'fumadocs-core/source';
|
|
22
|
+
export { createFromSource } from 'fumadocs-core/search/server';
|
|
23
|
+
export { createOpenAPI } from 'fumadocs-openapi/server';
|
|
24
|
+
|
|
25
|
+
export { makeRoleGate, createActionsHandler, validateActionArgs } from './adaptive';
|
|
26
|
+
export type { ActionRegistry, DocsActionDef, VisitorClaims } from './adaptive';
|
|
27
|
+
export { DocsAction } from './docs-action';
|
|
28
|
+
|
|
29
|
+
export interface EmbedMDXOptions {
|
|
30
|
+
/** Host session claims driving RoleGate visibility (absent = hide gated). */
|
|
31
|
+
claims?: VisitorClaims;
|
|
32
|
+
/** Host actions endpoint enabling DocsAction buttons (absent = inert). */
|
|
33
|
+
actionsEndpoint?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getEmbedMDXComponents(
|
|
37
|
+
components?: MDXComponents,
|
|
38
|
+
options?: EmbedMDXOptions,
|
|
39
|
+
): MDXComponents {
|
|
40
|
+
return {
|
|
41
|
+
...defaultMdxComponents,
|
|
42
|
+
Tab,
|
|
43
|
+
Tabs,
|
|
44
|
+
Step,
|
|
45
|
+
Steps,
|
|
46
|
+
Accordion,
|
|
47
|
+
Accordions,
|
|
48
|
+
Card,
|
|
49
|
+
Cards,
|
|
50
|
+
RoleGate: makeRoleGate(options?.claims),
|
|
51
|
+
DocsAction: (props: React.ComponentProps<typeof DocsAction>) => (
|
|
52
|
+
<DocsAction {...props} endpoint={options?.actionsEndpoint} />
|
|
53
|
+
),
|
|
54
|
+
...components,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Copied to <host>/app/api/docs-proxy/route.ts by the docs-embed skill.
|
|
2
|
+
import { openapi } from '@/lib/docs-source';
|
|
3
|
+
|
|
4
|
+
const proxy = openapi.createProxy({
|
|
5
|
+
allowedOrigins: ['http://localhost:4001', 'http://localhost:4002'],
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const { GET, POST, PUT, DELETE, PATCH, HEAD } = proxy;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copied to <host>/app/help/[[...slug]]/page.tsx by the docs-embed skill.
|
|
2
|
+
import {
|
|
3
|
+
DocsBody,
|
|
4
|
+
DocsDescription,
|
|
5
|
+
DocsPage,
|
|
6
|
+
DocsTitle,
|
|
7
|
+
} from '@current-docs/embed';
|
|
8
|
+
import { OpenAPIPage } from '@current-docs/embed/api-page';
|
|
9
|
+
import { notFound } from 'next/navigation';
|
|
10
|
+
import { openapi, source } from '@/lib/docs-source';
|
|
11
|
+
import { getMDXComponents } from '@/mdx-components';
|
|
12
|
+
|
|
13
|
+
export default async function Page(props: {
|
|
14
|
+
params: Promise<{ slug?: string[] }>;
|
|
15
|
+
}) {
|
|
16
|
+
const params = await props.params;
|
|
17
|
+
const page = source.getPage(params.slug);
|
|
18
|
+
if (!page) notFound();
|
|
19
|
+
|
|
20
|
+
const MDX = page.data.body;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<DocsPage toc={page.data.toc} full={page.data.full}>
|
|
24
|
+
<DocsTitle>{page.data.title}</DocsTitle>
|
|
25
|
+
<DocsDescription>{page.data.description}</DocsDescription>
|
|
26
|
+
<DocsBody>
|
|
27
|
+
<MDX
|
|
28
|
+
components={getMDXComponents({
|
|
29
|
+
OpenAPIPage: async (props: object) => (
|
|
30
|
+
<OpenAPIPage {...(await openapi.preloadOpenAPIPage(page))} {...(props as never)} />
|
|
31
|
+
),
|
|
32
|
+
})}
|
|
33
|
+
/>
|
|
34
|
+
</DocsBody>
|
|
35
|
+
</DocsPage>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function generateStaticParams() {
|
|
40
|
+
return source.generateParams();
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copied to <host>/app/help/layout.tsx by the docs-embed skill.
|
|
2
|
+
// Theming: DocsLayout inherits the host's --cur-* CSS variables because the
|
|
3
|
+
// docs render the same @acme/ui components the host renders.
|
|
4
|
+
import { DocsLayout, RootProvider } from '@current-docs/embed';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { source } from '@/lib/docs-source';
|
|
7
|
+
|
|
8
|
+
export default function HelpLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<RootProvider theme={{ enabled: false }} search={{ options: { api: '/api/docs-search' } }}>
|
|
11
|
+
<DocsLayout nav={{ title: 'Help' }} tree={source.getPageTree()}>
|
|
12
|
+
{children}
|
|
13
|
+
</DocsLayout>
|
|
14
|
+
</RootProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copied to <host>/lib/docs-source.ts by the docs-embed skill.
|
|
2
|
+
import { docs } from '@/.source/server';
|
|
3
|
+
import { createOpenAPI, loader } from '@current-docs/embed';
|
|
4
|
+
|
|
5
|
+
export const source = loader({
|
|
6
|
+
baseUrl: '/help',
|
|
7
|
+
source: docs.toFumadocsSource(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const openapi = createOpenAPI({
|
|
11
|
+
input: {
|
|
12
|
+
'tasks-api': '../../specs/tasks-api.json',
|
|
13
|
+
'notes-api': '../../specs/notes-api.json',
|
|
14
|
+
},
|
|
15
|
+
proxyUrl: '/api/docs-proxy',
|
|
16
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Copied into the host app root by the docs-embed skill.
|
|
2
|
+
import { getEmbedMDXComponents } from '@current-docs/embed';
|
|
3
|
+
import type { MDXComponents } from 'mdx/types';
|
|
4
|
+
|
|
5
|
+
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
|
6
|
+
return getEmbedMDXComponents(components);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useMDXComponents = getMDXComponents;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copied into the host app root by the docs-embed skill.
|
|
2
|
+
// The host must depend on "@current-docs/embed" (workspace:* inside this monorepo,
|
|
3
|
+
// ^0.1.0 from npm in an onboarded repo) and list it in transpilePackages.
|
|
4
|
+
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
|
5
|
+
|
|
6
|
+
export const docs = defineDocs({
|
|
7
|
+
dir: '../../content/docs',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
mdxOptions: {
|
|
12
|
+
providerImportSource: '@/mdx-components',
|
|
13
|
+
},
|
|
14
|
+
});
|