@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.
Files changed (52) hide show
  1. package/bin/dist/dory.js +108 -29
  2. package/bin/dist/dory.js.map +1 -1
  3. package/package.json +20 -4
  4. package/src/components/hooks.tsx +8 -0
  5. package/src/components/search.tsx +17 -4
  6. package/src/embed/loader.ts +200 -0
  7. package/src/embed/types.ts +54 -0
  8. package/src/embed/widget-iframe.ts +302 -0
  9. package/src/plugins/html-files-middleware.ts +108 -0
  10. package/src/sdk/index.ts +89 -0
  11. package/src/sdk/react.tsx +139 -0
  12. package/src/sdk/vue.ts +107 -0
  13. package/src/types/config.ts +42 -0
  14. package/src/ui/header.tsx +6 -1
  15. package/src/ui/layout.tsx +16 -5
  16. package/src/ui/mobile-navigation.tsx +11 -3
  17. package/src/ui/table-of-content.tsx +2 -1
  18. package/tsconfig.bin.json +1 -0
  19. package/vite.config.embed-loader.ts +28 -0
  20. package/vite.config.embed-widget.ts +31 -0
  21. package/vite.config.ts +2 -0
  22. package/.dockerignore +0 -4
  23. package/CLAUDE.md +0 -178
  24. package/CODE_OF_CONDUCT.md +0 -128
  25. package/CONTRIBUTING.md +0 -91
  26. package/SECURITY.md +0 -11
  27. package/bin/dory.ts +0 -433
  28. package/dist/favicon.svg +0 -75
  29. package/dist/frontmatter.json +0 -77
  30. package/dist/index.html +0 -55
  31. package/dist/llms.txt +0 -561
  32. package/dist/logo/logo.svg +0 -18
  33. package/docs/api-playground-demo.mdx +0 -119
  34. package/docs/api-reference/asyncapi.json +0 -324
  35. package/docs/api-reference/openapi.json +0 -366
  36. package/docs/api-reference/operations/create.mdx +0 -5
  37. package/docs/api-reference/operations/delete.mdx +0 -5
  38. package/docs/api-reference/operations/get.mdx +0 -5
  39. package/docs/api-reference/operations/update.mdx +0 -5
  40. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +0 -5
  41. package/docs/api-reference/websocket-operations/receive-notifications.mdx +0 -5
  42. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +0 -5
  43. package/docs/api-reference/websocket-operations/send-chat-message.mdx +0 -5
  44. package/docs/api-reference/websocket-operations/send-presence-update.mdx +0 -5
  45. package/docs/dory.json +0 -64
  46. package/docs/favicon.svg +0 -75
  47. package/docs/features.mdx +0 -440
  48. package/docs/introduction.mdx +0 -44
  49. package/docs/logo/logo.svg +0 -18
  50. package/docs/websocket-playground-demo.mdx +0 -135
  51. package/scripts/generate-frontmatter-json.js +0 -154
  52. 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
+ }
@@ -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
+ }