@clidey/dory 0.29.0 → 0.31.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/bin/dist/dory.js +108 -29
- package/bin/dist/dory.js.map +1 -1
- package/package.json +20 -4
- package/src/components/hooks.tsx +8 -0
- package/src/components/search.tsx +17 -4
- package/src/embed/loader.ts +200 -0
- package/src/embed/types.ts +54 -0
- package/src/embed/widget-iframe.ts +302 -0
- package/src/plugins/html-files-middleware.ts +108 -0
- package/src/sdk/index.ts +89 -0
- package/src/sdk/react.tsx +139 -0
- package/src/sdk/vue.ts +107 -0
- package/src/types/config.ts +42 -0
- package/src/ui/header.tsx +6 -1
- package/src/ui/layout.tsx +16 -5
- package/src/ui/mobile-navigation.tsx +11 -3
- package/src/ui/table-of-content.tsx +2 -1
- package/tsconfig.bin.json +1 -0
- package/vite.config.embed-loader.ts +28 -0
- package/vite.config.embed-widget.ts +31 -0
- package/vite.config.ts +2 -0
- package/.dockerignore +0 -4
- package/CLAUDE.md +0 -178
- package/CODE_OF_CONDUCT.md +0 -128
- package/CONTRIBUTING.md +0 -91
- package/SECURITY.md +0 -11
- package/bin/dory.ts +0 -433
- package/dist/favicon.svg +0 -75
- package/dist/frontmatter.json +0 -77
- package/dist/index.html +0 -55
- package/dist/llms.txt +0 -561
- package/dist/logo/logo.svg +0 -18
- package/docs/api-playground-demo.mdx +0 -119
- package/docs/api-reference/asyncapi.json +0 -324
- package/docs/api-reference/openapi.json +0 -366
- package/docs/api-reference/operations/create.mdx +0 -5
- package/docs/api-reference/operations/delete.mdx +0 -5
- package/docs/api-reference/operations/get.mdx +0 -5
- package/docs/api-reference/operations/update.mdx +0 -5
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +0 -5
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +0 -5
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +0 -5
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +0 -5
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +0 -5
- package/docs/dory.json +0 -64
- package/docs/favicon.svg +0 -75
- package/docs/features.mdx +0 -440
- package/docs/introduction.mdx +0 -44
- package/docs/logo/logo.svg +0 -18
- package/docs/websocket-playground-demo.mdx +0 -135
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dory Widget Shell (Stage 2) - Iframe Approach
|
|
3
|
+
* Creates an iframe pointing to the already-built documentation
|
|
4
|
+
* This is simpler and more reliable than rebuilding the app
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DoryEmbedConfig } from './types';
|
|
8
|
+
|
|
9
|
+
export class DoryWidget {
|
|
10
|
+
private config: DoryEmbedConfig;
|
|
11
|
+
private container: HTMLElement | null = null;
|
|
12
|
+
private iframe: HTMLIFrameElement | null = null;
|
|
13
|
+
private overlay: HTMLElement | null = null;
|
|
14
|
+
private isOpenState = false;
|
|
15
|
+
private eventHandlers: Map<string, Function[]> = new Map();
|
|
16
|
+
|
|
17
|
+
constructor(config: DoryEmbedConfig) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.init();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private init() {
|
|
23
|
+
this.createContainer();
|
|
24
|
+
this.setupEventListeners();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private createContainer() {
|
|
28
|
+
// Create overlay
|
|
29
|
+
if (this.config.showOverlay !== false) {
|
|
30
|
+
this.overlay = document.createElement('div');
|
|
31
|
+
this.overlay.className = 'dory-embed-overlay';
|
|
32
|
+
this.overlay.style.cssText = `
|
|
33
|
+
position: fixed;
|
|
34
|
+
inset: 0;
|
|
35
|
+
background: rgba(0, 0, 0, 0.5);
|
|
36
|
+
z-index: 999998;
|
|
37
|
+
opacity: 0;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
transition: opacity 0.3s ease;
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
if (this.config.closeOnOverlayClick !== false) {
|
|
43
|
+
this.overlay.addEventListener('click', () => this.close());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
document.body.appendChild(this.overlay);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create main container
|
|
50
|
+
this.container = document.createElement('div');
|
|
51
|
+
this.container.className = 'dory-embed-container';
|
|
52
|
+
this.container.setAttribute('data-position', this.config.position || 'right');
|
|
53
|
+
this.container.style.cssText = this.getContainerStyles();
|
|
54
|
+
|
|
55
|
+
// Create iframe
|
|
56
|
+
this.iframe = document.createElement('iframe');
|
|
57
|
+
this.iframe.style.cssText = 'width: 100%; height: 100%; border: none;';
|
|
58
|
+
this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
|
|
59
|
+
this.iframe.src = this.buildIframeUrl();
|
|
60
|
+
|
|
61
|
+
this.container.appendChild(this.iframe);
|
|
62
|
+
|
|
63
|
+
// Add container to body
|
|
64
|
+
document.body.appendChild(this.container);
|
|
65
|
+
|
|
66
|
+
// Setup iframe communication
|
|
67
|
+
this.setupIframeMessaging();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private buildIframeUrl(): string {
|
|
71
|
+
const baseUrl = this.config.baseUrl;
|
|
72
|
+
const path = this.config.initialPath || '/';
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
|
|
75
|
+
// Add embedded mode flag
|
|
76
|
+
params.set('embedded', 'true');
|
|
77
|
+
|
|
78
|
+
// Add theme
|
|
79
|
+
if (this.config.theme && this.config.theme !== 'inherit') {
|
|
80
|
+
params.set('theme', this.config.theme);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const url = new URL(baseUrl);
|
|
84
|
+
url.pathname = path;
|
|
85
|
+
url.search = params.toString();
|
|
86
|
+
|
|
87
|
+
return url.toString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private setupIframeMessaging() {
|
|
91
|
+
window.addEventListener('message', (event) => {
|
|
92
|
+
// Verify origin
|
|
93
|
+
if (event.origin !== new URL(this.config.baseUrl).origin) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const message = event.data;
|
|
98
|
+
|
|
99
|
+
switch (message.type) {
|
|
100
|
+
case 'navigate':
|
|
101
|
+
this.emit('navigate', message.path);
|
|
102
|
+
break;
|
|
103
|
+
case 'ready':
|
|
104
|
+
this.emit('ready');
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private getContainerStyles(): string {
|
|
111
|
+
const { position = 'right', width = 500, height = '100vh' } = this.config;
|
|
112
|
+
const widthStr = typeof width === 'number' ? `${width}px` : width;
|
|
113
|
+
|
|
114
|
+
const baseStyles = `
|
|
115
|
+
position: fixed;
|
|
116
|
+
z-index: 999999;
|
|
117
|
+
background: white;
|
|
118
|
+
box-shadow: -2px 0 20px rgba(0, 0, 0, 0.15);
|
|
119
|
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
120
|
+
will-change: transform;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const positionStyles = {
|
|
125
|
+
right: `
|
|
126
|
+
top: 0;
|
|
127
|
+
right: 0;
|
|
128
|
+
bottom: 0;
|
|
129
|
+
width: ${widthStr};
|
|
130
|
+
max-width: 100vw;
|
|
131
|
+
transform: translateX(100%);
|
|
132
|
+
`,
|
|
133
|
+
left: `
|
|
134
|
+
top: 0;
|
|
135
|
+
left: 0;
|
|
136
|
+
bottom: 0;
|
|
137
|
+
width: ${widthStr};
|
|
138
|
+
max-width: 100vw;
|
|
139
|
+
transform: translateX(-100%);
|
|
140
|
+
`,
|
|
141
|
+
bottom: `
|
|
142
|
+
bottom: 0;
|
|
143
|
+
left: 0;
|
|
144
|
+
right: 0;
|
|
145
|
+
height: ${height};
|
|
146
|
+
max-height: 90vh;
|
|
147
|
+
transform: translateY(100%);
|
|
148
|
+
`,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return baseStyles + positionStyles[position];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private setupEventListeners() {
|
|
155
|
+
// Keyboard shortcuts
|
|
156
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
157
|
+
// ESC to close
|
|
158
|
+
if (e.key === 'Escape' && this.isOpenState && this.config.closeOnEscape !== false) {
|
|
159
|
+
this.close();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
document.addEventListener('keydown', handleKeydown);
|
|
164
|
+
|
|
165
|
+
// Store cleanup function
|
|
166
|
+
(this.container as any).__cleanup = () => {
|
|
167
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async open(path?: string) {
|
|
172
|
+
if (this.isOpenState) {
|
|
173
|
+
if (path) this.navigate(path);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.isOpenState = true;
|
|
178
|
+
|
|
179
|
+
// Show overlay
|
|
180
|
+
if (this.overlay) {
|
|
181
|
+
this.overlay.style.opacity = '1';
|
|
182
|
+
this.overlay.style.pointerEvents = 'auto';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Slide in container
|
|
186
|
+
if (this.container) {
|
|
187
|
+
this.container.style.transform = 'translate(0, 0)';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update iframe URL if path is specified
|
|
191
|
+
if (path && this.iframe) {
|
|
192
|
+
const url = new URL(this.iframe.src);
|
|
193
|
+
url.pathname = path;
|
|
194
|
+
this.iframe.src = url.toString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Focus management
|
|
198
|
+
this.container?.focus();
|
|
199
|
+
|
|
200
|
+
// Disable body scroll
|
|
201
|
+
document.body.style.overflow = 'hidden';
|
|
202
|
+
|
|
203
|
+
// Emit event
|
|
204
|
+
this.emit('open');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
close() {
|
|
208
|
+
if (!this.isOpenState) return;
|
|
209
|
+
|
|
210
|
+
this.isOpenState = false;
|
|
211
|
+
|
|
212
|
+
// Hide overlay
|
|
213
|
+
if (this.overlay) {
|
|
214
|
+
this.overlay.style.opacity = '0';
|
|
215
|
+
this.overlay.style.pointerEvents = 'none';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Slide out container
|
|
219
|
+
if (this.container) {
|
|
220
|
+
const { position = 'right' } = this.config;
|
|
221
|
+
const transforms: Record<string, string> = {
|
|
222
|
+
right: 'translateX(100%)',
|
|
223
|
+
left: 'translateX(-100%)',
|
|
224
|
+
bottom: 'translateY(100%)',
|
|
225
|
+
};
|
|
226
|
+
this.container.style.transform = transforms[position];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Re-enable body scroll
|
|
230
|
+
document.body.style.overflow = '';
|
|
231
|
+
|
|
232
|
+
// Emit event
|
|
233
|
+
this.emit('close');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
toggle() {
|
|
237
|
+
this.isOpenState ? this.close() : this.open();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
navigate(path: string) {
|
|
241
|
+
if (this.iframe) {
|
|
242
|
+
const url = new URL(this.iframe.src);
|
|
243
|
+
url.pathname = path;
|
|
244
|
+
this.iframe.src = url.toString();
|
|
245
|
+
this.emit('navigate', path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setTheme(theme: 'light' | 'dark') {
|
|
250
|
+
if (this.iframe) {
|
|
251
|
+
const url = new URL(this.iframe.src);
|
|
252
|
+
url.searchParams.set('theme', theme);
|
|
253
|
+
this.iframe.src = url.toString();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
isOpen() {
|
|
258
|
+
return this.isOpenState;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
on(event: string, handler: Function) {
|
|
262
|
+
if (!this.eventHandlers.has(event)) {
|
|
263
|
+
this.eventHandlers.set(event, []);
|
|
264
|
+
}
|
|
265
|
+
this.eventHandlers.get(event)!.push(handler);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private emit(event: string, ...args: any[]) {
|
|
269
|
+
const handlers = this.eventHandlers.get(event);
|
|
270
|
+
if (handlers) {
|
|
271
|
+
handlers.forEach(handler => {
|
|
272
|
+
try {
|
|
273
|
+
handler(...args);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error(`[DoryDocs] Error in ${event} handler:`, error);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
destroy() {
|
|
282
|
+
// Cleanup event listeners
|
|
283
|
+
if (this.container && (this.container as any).__cleanup) {
|
|
284
|
+
(this.container as any).__cleanup();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Remove DOM elements
|
|
288
|
+
this.container?.remove();
|
|
289
|
+
this.overlay?.remove();
|
|
290
|
+
|
|
291
|
+
// Clear references
|
|
292
|
+
this.container = null;
|
|
293
|
+
this.iframe = null;
|
|
294
|
+
this.overlay = null;
|
|
295
|
+
this.eventHandlers.clear();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Expose widget globally for loader script
|
|
300
|
+
if (typeof window !== 'undefined') {
|
|
301
|
+
window.__DoryWidget__ = DoryWidget;
|
|
302
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
+
import { resolve, extname } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vite plugin to serve HTML files and assets directly from the docs directory
|
|
7
|
+
* and static files from the dist directory without routing them through the SPA
|
|
8
|
+
*/
|
|
9
|
+
export function htmlFilesMiddleware(): Plugin {
|
|
10
|
+
// Define asset extensions
|
|
11
|
+
const assetExtensions = new Set([
|
|
12
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
|
13
|
+
'.pdf', '.mp4', '.webm', '.mp3', '.wav', '.mov',
|
|
14
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// Map extensions to MIME types
|
|
18
|
+
const mimeTypes: Record<string, string> = {
|
|
19
|
+
'.html': 'text/html',
|
|
20
|
+
'.js': 'application/javascript',
|
|
21
|
+
'.css': 'text/css',
|
|
22
|
+
'.json': 'application/json',
|
|
23
|
+
'.svg': 'image/svg+xml',
|
|
24
|
+
'.png': 'image/png',
|
|
25
|
+
'.jpg': 'image/jpeg',
|
|
26
|
+
'.jpeg': 'image/jpeg',
|
|
27
|
+
'.gif': 'image/gif',
|
|
28
|
+
'.webp': 'image/webp',
|
|
29
|
+
'.ico': 'image/x-icon',
|
|
30
|
+
'.pdf': 'application/pdf',
|
|
31
|
+
'.mp4': 'video/mp4',
|
|
32
|
+
'.webm': 'video/webm',
|
|
33
|
+
'.mov': 'video/quicktime',
|
|
34
|
+
'.mp3': 'audio/mpeg',
|
|
35
|
+
'.wav': 'audio/wav',
|
|
36
|
+
'.woff': 'font/woff',
|
|
37
|
+
'.woff2': 'font/woff2',
|
|
38
|
+
'.ttf': 'font/ttf',
|
|
39
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: 'html-files-middleware',
|
|
44
|
+
configureServer(server) {
|
|
45
|
+
server.middlewares.use((req, res, next) => {
|
|
46
|
+
if (!req.url) {
|
|
47
|
+
next();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove query params
|
|
52
|
+
const url = req.url.split('?')[0];
|
|
53
|
+
|
|
54
|
+
// Check if the request is for a file in the dist directory
|
|
55
|
+
if (url.startsWith('/dist/')) {
|
|
56
|
+
const distPath = resolve(process.cwd(), url.slice(1));
|
|
57
|
+
|
|
58
|
+
if (existsSync(distPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(distPath, 'utf-8');
|
|
61
|
+
const ext = extname(url).toLowerCase();
|
|
62
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
63
|
+
|
|
64
|
+
res.setHeader('Content-Type', contentType);
|
|
65
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
66
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
67
|
+
res.end(content);
|
|
68
|
+
return;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`Error reading ${distPath}:`, error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if the request is for a file in the docs directory
|
|
76
|
+
// This handles both direct file requests and directory requests
|
|
77
|
+
const docsPath = resolve(process.cwd(), 'docs', url.slice(1));
|
|
78
|
+
|
|
79
|
+
if (existsSync(docsPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const stats = statSync(docsPath);
|
|
82
|
+
|
|
83
|
+
// If it's a file, serve it
|
|
84
|
+
if (stats.isFile()) {
|
|
85
|
+
const ext = extname(url).toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Only serve HTML files and assets, not MDX files
|
|
88
|
+
if (url.endsWith('.html') || assetExtensions.has(ext)) {
|
|
89
|
+
const content = readFileSync(docsPath);
|
|
90
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
91
|
+
|
|
92
|
+
res.setHeader('Content-Type', contentType);
|
|
93
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
94
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
95
|
+
res.end(content);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`Error reading ${docsPath}:`, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dory Embedded Documentation SDK
|
|
3
|
+
* Export all framework integrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export React SDK
|
|
7
|
+
export { useDoryDocs as useReactDoryDocs, DoryDocsTrigger, DoryDocsProvider } from './react';
|
|
8
|
+
|
|
9
|
+
// Re-export Vue SDK
|
|
10
|
+
export { useDoryDocs as useVueDoryDocs } from './vue';
|
|
11
|
+
|
|
12
|
+
// Export types
|
|
13
|
+
export type { DoryEmbedConfig } from '../embed/types';
|
|
14
|
+
|
|
15
|
+
// Export vanilla JS API reference
|
|
16
|
+
export interface DoryDocsAPI {
|
|
17
|
+
init(config: Partial<any>): DoryDocsAPI;
|
|
18
|
+
open(path?: string): Promise<void>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
toggle(): Promise<void>;
|
|
21
|
+
navigate(path: string): Promise<void>;
|
|
22
|
+
setTheme(theme: 'light' | 'dark'): Promise<void>;
|
|
23
|
+
isOpen(): boolean;
|
|
24
|
+
destroy(): Promise<void>;
|
|
25
|
+
on(event: string, handler: Function): DoryDocsAPI;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if DoryDocs is loaded
|
|
30
|
+
*/
|
|
31
|
+
export function isDoryDocsLoaded(): boolean {
|
|
32
|
+
return typeof window !== 'undefined' && !!window.DoryDocs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wait for DoryDocs to be loaded
|
|
37
|
+
*/
|
|
38
|
+
export function waitForDoryDocs(timeout = 5000): Promise<boolean> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
if (isDoryDocsLoaded()) {
|
|
41
|
+
resolve(true);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
window.removeEventListener('dory:ready', handleReady);
|
|
47
|
+
resolve(false);
|
|
48
|
+
}, timeout);
|
|
49
|
+
|
|
50
|
+
const handleReady = () => {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
resolve(true);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener('dory:ready', handleReady, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load Dory embed script dynamically
|
|
61
|
+
*/
|
|
62
|
+
export function loadDoryScript(docsUrl: string): Promise<void> {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
if (isDoryDocsLoaded()) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const script = document.createElement('script');
|
|
70
|
+
script.src = `${docsUrl}/embed.js`;
|
|
71
|
+
script.async = true;
|
|
72
|
+
|
|
73
|
+
script.onload = () => {
|
|
74
|
+
waitForDoryDocs().then((loaded) => {
|
|
75
|
+
if (loaded) {
|
|
76
|
+
resolve();
|
|
77
|
+
} else {
|
|
78
|
+
reject(new Error('DoryDocs failed to initialize'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
script.onerror = () => {
|
|
84
|
+
reject(new Error('Failed to load DoryDocs script'));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
document.head.appendChild(script);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React SDK for Dory Embedded Documentation
|
|
3
|
+
* Usage: npm install @clidey/dory-embedded
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
7
|
+
import type { DoryDocsAPI } from '../embed/types';
|
|
8
|
+
|
|
9
|
+
interface DoryDocsConfig {
|
|
10
|
+
docsUrl: string;
|
|
11
|
+
position?: 'left' | 'right' | 'bottom';
|
|
12
|
+
width?: number | string;
|
|
13
|
+
height?: string;
|
|
14
|
+
theme?: 'light' | 'dark' | 'inherit';
|
|
15
|
+
initialPath?: string;
|
|
16
|
+
showOverlay?: boolean;
|
|
17
|
+
closeOnEscape?: boolean;
|
|
18
|
+
closeOnOverlayClick?: boolean;
|
|
19
|
+
onOpen?: () => void;
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
onNavigate?: (path: string) => void;
|
|
22
|
+
onReady?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to use Dory Documentation in React components
|
|
27
|
+
*/
|
|
28
|
+
export function useDoryDocs(config: DoryDocsConfig) {
|
|
29
|
+
const docsRef = useRef<any>(null);
|
|
30
|
+
const configRef = useRef(config);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
configRef.current = config;
|
|
34
|
+
}, [config]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Wait for DoryDocs to be available
|
|
38
|
+
const checkDoryDocs = () => {
|
|
39
|
+
if (window.DoryDocs) {
|
|
40
|
+
docsRef.current = window.DoryDocs.init(configRef.current);
|
|
41
|
+
} else {
|
|
42
|
+
console.warn('[useDoryDocs] DoryDocs not loaded. Make sure to include the embed script.');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (document.readyState === 'loading') {
|
|
47
|
+
window.addEventListener('dory:ready', checkDoryDocs);
|
|
48
|
+
} else {
|
|
49
|
+
checkDoryDocs();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
window.removeEventListener('dory:ready', checkDoryDocs);
|
|
54
|
+
docsRef.current?.destroy();
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const open = useCallback((path?: string) => {
|
|
59
|
+
return docsRef.current?.open(path);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const close = useCallback(() => {
|
|
63
|
+
return docsRef.current?.close();
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const toggle = useCallback(() => {
|
|
67
|
+
return docsRef.current?.toggle();
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const navigate = useCallback((path: string) => {
|
|
71
|
+
return docsRef.current?.navigate(path);
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const setTheme = useCallback((theme: 'light' | 'dark') => {
|
|
75
|
+
return docsRef.current?.setTheme(theme);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const isOpen = useCallback(() => {
|
|
79
|
+
return docsRef.current?.isOpen() || false;
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
open,
|
|
84
|
+
close,
|
|
85
|
+
toggle,
|
|
86
|
+
navigate,
|
|
87
|
+
setTheme,
|
|
88
|
+
isOpen,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Component that renders a trigger button for Dory Documentation
|
|
94
|
+
*/
|
|
95
|
+
export function DoryDocsTrigger({
|
|
96
|
+
children,
|
|
97
|
+
config,
|
|
98
|
+
className,
|
|
99
|
+
path,
|
|
100
|
+
}: {
|
|
101
|
+
children: React.ReactNode;
|
|
102
|
+
config: DoryDocsConfig;
|
|
103
|
+
className?: string;
|
|
104
|
+
path?: string;
|
|
105
|
+
}) {
|
|
106
|
+
const docs = useDoryDocs(config);
|
|
107
|
+
|
|
108
|
+
const handleClick = () => {
|
|
109
|
+
docs.open(path);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<button onClick={handleClick} className={className}>
|
|
114
|
+
{children}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Component that initializes Dory Documentation without rendering anything
|
|
121
|
+
*/
|
|
122
|
+
export function DoryDocsProvider({
|
|
123
|
+
config,
|
|
124
|
+
children,
|
|
125
|
+
}: {
|
|
126
|
+
config: DoryDocsConfig;
|
|
127
|
+
children: React.ReactNode;
|
|
128
|
+
}) {
|
|
129
|
+
useDoryDocs(config);
|
|
130
|
+
|
|
131
|
+
return <>{children}</>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Type declarations
|
|
135
|
+
declare global {
|
|
136
|
+
interface Window {
|
|
137
|
+
DoryDocs: DoryDocsAPI;
|
|
138
|
+
}
|
|
139
|
+
}
|