@clidey/dory 0.30.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 (74) hide show
  1. package/bin/dist/dory.js +1 -8
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +54 -0
  4. package/package.json +12 -4
  5. package/src/app.css +5 -0
  6. package/src/app.tsx +13 -0
  7. package/src/components/dropdown.tsx +53 -0
  8. package/src/components/hooks.tsx +36 -0
  9. package/src/components/loading.tsx +21 -0
  10. package/src/components/mdx-provider.tsx +13 -0
  11. package/src/components/navigation.tsx +58 -0
  12. package/src/components/notification.tsx +13 -0
  13. package/src/components/routes.tsx +92 -0
  14. package/src/components/search-index.ts +43 -0
  15. package/src/components/search.tsx +448 -0
  16. package/src/components/store.ts +216 -0
  17. package/src/config/mdx.js +12 -0
  18. package/src/config/mdx.ts +12 -0
  19. package/src/embed/loader.ts +200 -0
  20. package/src/embed/types.ts +54 -0
  21. package/src/embed/widget-iframe.ts +302 -0
  22. package/src/main.tsx +22 -0
  23. package/src/mdx/accordion.tsx +41 -0
  24. package/src/mdx/api-playground.tsx +659 -0
  25. package/src/mdx/api.tsx +354 -0
  26. package/src/mdx/async-api.tsx +353 -0
  27. package/src/mdx/callout.tsx +64 -0
  28. package/src/mdx/card.tsx +54 -0
  29. package/src/mdx/code.tsx +289 -0
  30. package/src/mdx/expandable.tsx +36 -0
  31. package/src/mdx/fence.tsx +74 -0
  32. package/src/mdx/icon.tsx +73 -0
  33. package/src/mdx/latex.tsx +26 -0
  34. package/src/mdx/list.tsx +7 -0
  35. package/src/mdx/mdx.tsx +47 -0
  36. package/src/mdx/mermaid.tsx +147 -0
  37. package/src/mdx/open-api.tsx +311 -0
  38. package/src/mdx/processor.ts +174 -0
  39. package/src/mdx/source.tsx +58 -0
  40. package/src/mdx/step.tsx +51 -0
  41. package/src/mdx/table.tsx +20 -0
  42. package/src/mdx/tag.tsx +53 -0
  43. package/src/mdx/text.tsx +99 -0
  44. package/src/mdx/websocket-playground.tsx +824 -0
  45. package/src/plugins/docs-assets.ts +79 -0
  46. package/src/plugins/frontmatter-dev-server.ts +158 -0
  47. package/src/plugins/frontmatter-generator.ts +157 -0
  48. package/src/plugins/html-files-middleware.ts +108 -0
  49. package/src/plugins/html-metadata-injector.ts +76 -0
  50. package/src/plugins/llm-txt-dev-server.ts +183 -0
  51. package/src/plugins/llm-txt-generator.ts +186 -0
  52. package/src/plugins/sanitize.ts +163 -0
  53. package/src/plugins/unified-mdx-plugin.ts +35 -0
  54. package/src/sdk/index.ts +89 -0
  55. package/src/sdk/react.tsx +139 -0
  56. package/src/sdk/vue.ts +107 -0
  57. package/src/theme.scss +72 -0
  58. package/src/types/config.ts +89 -0
  59. package/src/ui/header.tsx +96 -0
  60. package/src/ui/layout.tsx +237 -0
  61. package/src/ui/mobile-navigation.tsx +72 -0
  62. package/src/ui/prev-next-link.tsx +74 -0
  63. package/src/ui/table-of-content.tsx +167 -0
  64. package/src/utils/colors.ts +54 -0
  65. package/src/utils/fonts.ts +23 -0
  66. package/src/utils/functions.ts +12 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/tsconfig.app.json +33 -0
  69. package/tsconfig.bin.json +25 -0
  70. package/tsconfig.json +7 -0
  71. package/tsconfig.node.json +26 -0
  72. package/vite.config.embed-loader.ts +28 -0
  73. package/vite.config.embed-widget.ts +31 -0
  74. package/vite.config.ts +45 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Dory Embed Loader Script (Stage 1)
3
+ * This is the minimal initial script that sets up the global API
4
+ * and lazy-loads the widget on demand.
5
+ * Target size: ~3-5KB gzipped
6
+ */
7
+
8
+ import type { DoryEmbedConfig, DoryDocsAPI } from './types';
9
+
10
+ // Prevent double-loading
11
+ if (typeof window !== 'undefined' && window.DoryDocs) {
12
+ console.warn('[DoryDocs] Already loaded');
13
+ } else {
14
+ // Main initialization
15
+ (function() {
16
+ 'use strict';
17
+
18
+ // Extract base URL from script tag
19
+ const currentScript = document.currentScript as HTMLScriptElement;
20
+ const scriptSrc = currentScript?.src || '';
21
+ const baseUrl = scriptSrc.replace(/\/embed\.js.*$/, '');
22
+
23
+ // Configuration storage
24
+ let config: DoryEmbedConfig = {
25
+ baseUrl,
26
+ position: 'right',
27
+ width: 500,
28
+ height: '100vh',
29
+ theme: 'inherit',
30
+ initialPath: '/',
31
+ autoInit: true,
32
+ showOverlay: true,
33
+ closeOnEscape: true,
34
+ closeOnOverlayClick: true,
35
+ };
36
+
37
+ // Widget instance (loaded lazily)
38
+ let widgetInstance: any = null;
39
+ let widgetLoading = false;
40
+ let widgetLoadPromise: Promise<any> | null = null;
41
+
42
+ // Load the widget script
43
+ async function loadWidget(): Promise<any> {
44
+ if (widgetInstance) return widgetInstance;
45
+ if (widgetLoading) return widgetLoadPromise;
46
+
47
+ widgetLoading = true;
48
+ widgetLoadPromise = new Promise((resolve, reject) => {
49
+ const script = document.createElement('script');
50
+ script.src = `${baseUrl}/embed-widget.js`;
51
+ script.async = true;
52
+ script.type = 'module';
53
+
54
+ script.onload = () => {
55
+ // Widget script exposes window.__DoryWidget__
56
+ if (window.__DoryWidget__) {
57
+ widgetInstance = new window.__DoryWidget__(config);
58
+ resolve(widgetInstance);
59
+ } else {
60
+ reject(new Error('[DoryDocs] Widget failed to load'));
61
+ }
62
+ };
63
+
64
+ script.onerror = () => {
65
+ widgetLoading = false;
66
+ widgetLoadPromise = null;
67
+ reject(new Error('[DoryDocs] Failed to load widget script'));
68
+ };
69
+
70
+ document.head.appendChild(script);
71
+ });
72
+
73
+ return widgetLoadPromise;
74
+ }
75
+
76
+ // Public API
77
+ const DoryDocs: DoryDocsAPI = {
78
+ // Configuration
79
+ init(userConfig: Partial<DoryEmbedConfig>) {
80
+ config = { ...config, ...userConfig };
81
+
82
+ // Auto-attach trigger if specified
83
+ if (config.trigger) {
84
+ const attachTrigger = () => {
85
+ const element = typeof config.trigger === 'string'
86
+ ? document.querySelector(config.trigger)
87
+ : config.trigger;
88
+
89
+ if (element) {
90
+ element.addEventListener('click', () => this.open());
91
+ } else if (typeof config.trigger === 'string') {
92
+ console.warn(`[DoryDocs] Trigger element not found: ${config.trigger}`);
93
+ }
94
+ };
95
+
96
+ // Try immediately, or wait for DOM ready
97
+ if (document.readyState === 'loading') {
98
+ document.addEventListener('DOMContentLoaded', attachTrigger);
99
+ } else {
100
+ attachTrigger();
101
+ }
102
+ }
103
+
104
+ return this;
105
+ },
106
+
107
+ // Open documentation
108
+ async open(path?: string) {
109
+ try {
110
+ const widget = await loadWidget();
111
+ await widget.open(path || config.initialPath);
112
+ config.onOpen?.();
113
+ } catch (error) {
114
+ console.error('[DoryDocs] Failed to open:', error);
115
+ }
116
+ },
117
+
118
+ // Close documentation
119
+ async close() {
120
+ if (!widgetInstance) return;
121
+ widgetInstance.close();
122
+ config.onClose?.();
123
+ },
124
+
125
+ // Toggle documentation
126
+ async toggle() {
127
+ try {
128
+ const widget = await loadWidget();
129
+ widget.toggle();
130
+ } catch (error) {
131
+ console.error('[DoryDocs] Failed to toggle:', error);
132
+ }
133
+ },
134
+
135
+ // Navigate to path
136
+ async navigate(path: string) {
137
+ try {
138
+ const widget = await loadWidget();
139
+ widget.navigate(path);
140
+ } catch (error) {
141
+ console.error('[DoryDocs] Failed to navigate:', error);
142
+ }
143
+ },
144
+
145
+ // Set theme
146
+ async setTheme(theme: 'light' | 'dark') {
147
+ try {
148
+ const widget = await loadWidget();
149
+ widget.setTheme(theme);
150
+ } catch (error) {
151
+ console.error('[DoryDocs] Failed to set theme:', error);
152
+ }
153
+ },
154
+
155
+ // Get current state
156
+ isOpen() {
157
+ return widgetInstance?.isOpen() || false;
158
+ },
159
+
160
+ // Destroy widget
161
+ async destroy() {
162
+ if (widgetInstance) {
163
+ widgetInstance.destroy();
164
+ widgetInstance = null;
165
+ widgetLoading = false;
166
+ widgetLoadPromise = null;
167
+ }
168
+ },
169
+
170
+ // Event listeners
171
+ on(event: string, handler: Function) {
172
+ const eventKey = `on${event.charAt(0).toUpperCase()}${event.slice(1)}` as keyof DoryEmbedConfig;
173
+ (config as any)[eventKey] = handler;
174
+ if (widgetInstance) {
175
+ widgetInstance.on(event, handler);
176
+ }
177
+ return this;
178
+ },
179
+ };
180
+
181
+ // Expose global API
182
+ window.DoryDocs = DoryDocs;
183
+
184
+ // Auto-initialize if configured
185
+ if (config.autoInit) {
186
+ // Check for URL hash deep linking
187
+ if (window.location.hash.startsWith('#docs=')) {
188
+ const path = decodeURIComponent(window.location.hash.replace('#docs=', ''));
189
+ DoryDocs.open(path);
190
+ }
191
+ }
192
+
193
+ // Dispatch ready event
194
+ window.dispatchEvent(new CustomEvent('dory:ready', { detail: { api: DoryDocs } }));
195
+
196
+ // Call onReady callback
197
+ config.onReady?.();
198
+
199
+ })();
200
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * TypeScript interfaces for Dory embedded documentation
3
+ */
4
+
5
+ export interface DoryEmbedConfig {
6
+ baseUrl: string;
7
+ position?: 'left' | 'right' | 'bottom';
8
+ width?: number | string;
9
+ height?: string;
10
+ theme?: 'light' | 'dark' | 'inherit';
11
+ initialPath?: string;
12
+ trigger?: string | HTMLElement;
13
+ autoInit?: boolean;
14
+ showOverlay?: boolean;
15
+ closeOnEscape?: boolean;
16
+ closeOnOverlayClick?: boolean;
17
+ onOpen?: () => void;
18
+ onClose?: () => void;
19
+ onNavigate?: (path: string) => void;
20
+ onReady?: () => void;
21
+ authToken?: string;
22
+ getAuthToken?: () => Promise<string>;
23
+ }
24
+
25
+ export interface EmbeddedAppConfig {
26
+ container: HTMLElement;
27
+ config: DoryEmbedConfig;
28
+ onNavigate: (path: string) => void;
29
+ onClose: () => void;
30
+ }
31
+
32
+ export interface RouterContext {
33
+ currentPath: string;
34
+ navigate: (path: string) => void;
35
+ }
36
+
37
+ declare global {
38
+ interface Window {
39
+ DoryDocs: DoryDocsAPI;
40
+ __DoryWidget__: any;
41
+ }
42
+ }
43
+
44
+ export interface DoryDocsAPI {
45
+ init(userConfig: Partial<DoryEmbedConfig>): DoryDocsAPI;
46
+ open(path?: string): Promise<void>;
47
+ close(): Promise<void>;
48
+ toggle(): Promise<void>;
49
+ navigate(path: string): Promise<void>;
50
+ setTheme(theme: 'light' | 'dark'): Promise<void>;
51
+ isOpen(): boolean;
52
+ destroy(): Promise<void>;
53
+ on(event: string, handler: Function): DoryDocsAPI;
54
+ }
@@ -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
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,22 @@
1
+ import { render } from 'preact'
2
+ import '@clidey/ux/styles.css';
3
+ import { App } from './app.tsx'
4
+ import { withMDX } from './components/mdx-provider.tsx'
5
+ import { loadFonts } from './utils/fonts'
6
+ import { loadColors } from './utils/colors'
7
+ import { Toaster } from '@clidey/ux'
8
+ import { ThemeProvider } from '@clidey/ux'
9
+
10
+ // Load fonts and colors before rendering the app
11
+ loadFonts()
12
+ loadColors()
13
+
14
+ const AppWithMDX = withMDX(App)
15
+
16
+ render(
17
+ <ThemeProvider storageKey="@clidey/dory/theme">
18
+ <Toaster />
19
+ <AppWithMDX />
20
+ </ThemeProvider>,
21
+ document.getElementById('app')!
22
+ )
@@ -0,0 +1,41 @@
1
+ import {
2
+ Accordion as UXAccordion,
3
+ AccordionItem,
4
+ AccordionTrigger,
5
+ AccordionContent
6
+ } from '@clidey/ux'
7
+ import { Icon } from './icon'
8
+
9
+ export function AccordionGroup({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <UXAccordion type="single" collapsible className="my-6">
12
+ {children}
13
+ </UXAccordion>
14
+ )
15
+ }
16
+
17
+ export function Accordion({
18
+ title,
19
+ children,
20
+ icon,
21
+ }: {
22
+ title: string
23
+ children: React.ReactNode
24
+ icon?: React.ComponentProps<typeof Icon>['icon']
25
+ }) {
26
+ return (
27
+ <AccordionItem value={title}>
28
+ <AccordionTrigger className="text-left">
29
+ <div className="flex items-center gap-2">
30
+ {icon && <Icon icon={icon} className="h-5 w-5" />}
31
+ <span className="text-base font-semibold">{title}</span>
32
+ </div>
33
+ </AccordionTrigger>
34
+ <AccordionContent>
35
+ <div className="text-base leading-relaxed">
36
+ {children}
37
+ </div>
38
+ </AccordionContent>
39
+ </AccordionItem>
40
+ )
41
+ }