@clidey/dory 1.0.8 → 1.9.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 (198) hide show
  1. package/README.md +12 -1
  2. package/bin/chat-server.ts +270 -0
  3. package/bin/dist/analytics-server.d.ts +11 -0
  4. package/bin/dist/analytics-server.d.ts.map +1 -0
  5. package/bin/dist/analytics-server.js +121 -0
  6. package/bin/dist/analytics-server.js.map +1 -0
  7. package/bin/dist/automation.d.ts +35 -0
  8. package/bin/dist/automation.d.ts.map +1 -0
  9. package/bin/dist/automation.js +259 -0
  10. package/bin/dist/automation.js.map +1 -0
  11. package/bin/dist/chat-server.d.ts +8 -0
  12. package/bin/dist/chat-server.d.ts.map +1 -0
  13. package/bin/dist/chat-server.js +322 -0
  14. package/bin/dist/chat-server.js.map +1 -0
  15. package/bin/dist/check.d.ts +16 -0
  16. package/bin/dist/check.d.ts.map +1 -0
  17. package/bin/dist/check.js +247 -0
  18. package/bin/dist/check.js.map +1 -0
  19. package/bin/dist/dory.js +252 -402
  20. package/bin/dist/dory.js.map +1 -1
  21. package/bin/dist/i18n-init.d.ts +6 -0
  22. package/bin/dist/i18n-init.d.ts.map +1 -0
  23. package/bin/dist/i18n-init.js +56 -0
  24. package/bin/dist/i18n-init.js.map +1 -0
  25. package/bin/dist/mcp-http.d.ts +8 -0
  26. package/bin/dist/mcp-http.d.ts.map +1 -0
  27. package/bin/dist/mcp-http.js +72 -0
  28. package/bin/dist/mcp-http.js.map +1 -0
  29. package/bin/dist/mcp.d.ts +2 -0
  30. package/bin/dist/mcp.d.ts.map +1 -0
  31. package/bin/dist/mcp.js +81 -0
  32. package/bin/dist/mcp.js.map +1 -0
  33. package/bin/dist/preview-entry.d.ts +2 -0
  34. package/bin/dist/preview-entry.d.ts.map +1 -0
  35. package/bin/dist/preview-entry.js +6 -0
  36. package/bin/dist/preview-entry.js.map +1 -0
  37. package/bin/dist/preview-server.d.ts +5 -0
  38. package/bin/dist/preview-server.d.ts.map +1 -0
  39. package/bin/dist/preview-server.js +159 -0
  40. package/bin/dist/preview-server.js.map +1 -0
  41. package/bin/dist/project-config.d.ts +22 -0
  42. package/bin/dist/project-config.d.ts.map +1 -0
  43. package/bin/dist/project-config.js +44 -0
  44. package/bin/dist/project-config.js.map +1 -0
  45. package/dist/index.html +57 -0
  46. package/dist/runtime/dory-runtime.css +2 -0
  47. package/dist/runtime/dory-runtime.html +14 -0
  48. package/dist/runtime/dory-runtime.js +5490 -0
  49. package/docs/api-playground-demo.mdx +119 -0
  50. package/docs/api-reference/asyncapi.json +324 -0
  51. package/docs/api-reference/openapi.json +366 -0
  52. package/docs/api-reference/operations/create.mdx +5 -0
  53. package/docs/api-reference/operations/delete.mdx +5 -0
  54. package/docs/api-reference/operations/get.mdx +5 -0
  55. package/docs/api-reference/operations/update.mdx +5 -0
  56. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  57. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  58. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  59. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  60. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  61. package/docs/assets/dory.jpeg +0 -0
  62. package/docs/components/accordion.mdx +145 -0
  63. package/docs/components/callouts.mdx +134 -0
  64. package/docs/components/cards.mdx +153 -0
  65. package/docs/components/code-blocks.mdx +281 -0
  66. package/docs/components/expandable.mdx +189 -0
  67. package/docs/components/icons.mdx +71 -0
  68. package/docs/components/images.mdx +82 -0
  69. package/docs/components/latex.mdx +148 -0
  70. package/docs/components/layout.mdx +175 -0
  71. package/docs/components/lists.mdx +134 -0
  72. package/docs/components/mermaid.mdx +149 -0
  73. package/docs/components/overview.mdx +81 -0
  74. package/docs/components/source.mdx +130 -0
  75. package/docs/components/steps.mdx +122 -0
  76. package/docs/components/tables.mdx +114 -0
  77. package/docs/components/tags.mdx +179 -0
  78. package/docs/components/typography.mdx +185 -0
  79. package/docs/configuration/analytics.mdx +45 -0
  80. package/docs/configuration/chat.mdx +59 -0
  81. package/docs/configuration/dory-json.mdx +278 -0
  82. package/docs/configuration/embed.mdx +73 -0
  83. package/docs/configuration/fonts.mdx +86 -0
  84. package/docs/configuration/internationalization.mdx +61 -0
  85. package/docs/configuration/navigation.mdx +217 -0
  86. package/docs/configuration/theming.mdx +166 -0
  87. package/docs/dory.json +129 -0
  88. package/docs/embed-example.html +176 -0
  89. package/docs/favicon.svg +18 -0
  90. package/docs/features.mdx +440 -0
  91. package/docs/getting-started/automation.mdx +76 -0
  92. package/docs/getting-started/configuration.mdx +95 -0
  93. package/docs/getting-started/installation.mdx +88 -0
  94. package/docs/getting-started/quick-start.mdx +53 -0
  95. package/docs/getting-started/whodb-ee-apps.mdx +122 -0
  96. package/docs/introduction.mdx +50 -0
  97. package/docs/logo/logo.svg +18 -0
  98. package/docs/playgrounds/api-playground.mdx +122 -0
  99. package/docs/playgrounds/websocket-playground.mdx +98 -0
  100. package/docs/social-card.png +0 -0
  101. package/docs/social-card.svg +17 -0
  102. package/docs/websocket-playground-demo.mdx +135 -0
  103. package/package.json +23 -17
  104. package/scripts/verify-runtime-parity.mjs +146 -0
  105. package/src/app.css +19 -1
  106. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  107. package/src/assets/fonts/OFL.txt +94 -0
  108. package/src/components/chat-assistant.tsx +229 -0
  109. package/src/components/dropdown.tsx +3 -3
  110. package/src/components/error-boundary.tsx +2 -2
  111. package/src/components/hooks.tsx +20 -1
  112. package/src/components/navigation.tsx +82 -51
  113. package/src/components/notification.tsx +2 -3
  114. package/src/components/routes.tsx +17 -19
  115. package/src/components/search-index.test.ts +15 -0
  116. package/src/components/search-index.ts +49 -48
  117. package/src/components/search.tsx +208 -104
  118. package/src/components/store.ts +27 -9
  119. package/src/config/mdx-common.ts +11 -0
  120. package/src/config/mdx.ts +10 -11
  121. package/src/embed/loader.ts +0 -3
  122. package/src/embed/types.ts +0 -2
  123. package/src/embed/widget-iframe.ts +19 -21
  124. package/src/embedded-runtime/config.ts +138 -0
  125. package/src/embedded-runtime/index.tsx +680 -0
  126. package/src/embedded-runtime/runtime.html +14 -0
  127. package/src/entry-server.tsx +46 -124
  128. package/src/main.tsx +16 -4
  129. package/src/mdx/accordion.tsx +3 -3
  130. package/src/mdx/api-playground.tsx +18 -16
  131. package/src/mdx/async-api.tsx +2 -2
  132. package/src/mdx/card.tsx +3 -2
  133. package/src/mdx/code.tsx +11 -9
  134. package/src/mdx/fence.tsx +6 -5
  135. package/src/mdx/icon-grid.tsx +4 -1
  136. package/src/mdx/icon-names.test.ts +15 -0
  137. package/src/mdx/icon-names.ts +60 -0
  138. package/src/mdx/icon-source-variants.ts +102 -0
  139. package/src/mdx/icon.tsx +12 -73
  140. package/src/mdx/latex.tsx +3 -2
  141. package/src/mdx/mdx.tsx +1 -1
  142. package/src/mdx/mermaid.tsx +12 -6
  143. package/src/mdx/open-api.tsx +130 -74
  144. package/src/mdx/preprocess.test.ts +16 -0
  145. package/src/mdx/preprocess.ts +61 -0
  146. package/src/mdx/processor.ts +4 -96
  147. package/src/mdx/source.tsx +5 -4
  148. package/src/mdx/specimen.tsx +38 -6
  149. package/src/mdx/tabs.tsx +29 -21
  150. package/src/mdx/text.tsx +9 -9
  151. package/src/mdx/try-it-panel.tsx +284 -0
  152. package/src/mdx/type-scale.tsx +4 -4
  153. package/src/mdx/websocket-playground.tsx +21 -14
  154. package/src/plugins/analytics-injector.ts +8 -0
  155. package/src/plugins/chat-dev-server.ts +32 -0
  156. package/src/plugins/docs-assets.test.ts +25 -0
  157. package/src/plugins/docs-assets.ts +6 -2
  158. package/src/plugins/frontmatter-dev-server.ts +2 -2
  159. package/src/plugins/frontmatter-generator.ts +1 -1
  160. package/src/plugins/html-files-middleware.test.ts +21 -1
  161. package/src/plugins/html-files-middleware.ts +26 -6
  162. package/src/plugins/html-metadata-injector.ts +24 -4
  163. package/src/plugins/llm-txt-dev-server.ts +2 -2
  164. package/src/plugins/llm-txt-generator.ts +21 -4
  165. package/src/plugins/prerender.ts +47 -20
  166. package/src/plugins/robots-generator.ts +1 -1
  167. package/src/plugins/sanitize.ts +1 -1
  168. package/src/plugins/shared.ts +6 -18
  169. package/src/plugins/sitemap-generator.ts +1 -1
  170. package/src/plugins/social-preview.test.ts +71 -0
  171. package/src/plugins/social-preview.ts +87 -0
  172. package/src/plugins/unified-mdx-plugin.ts +1 -1
  173. package/src/theme.scss +630 -207
  174. package/src/types/config.ts +53 -1
  175. package/src/ui/breadcrumb.tsx +17 -0
  176. package/src/ui/docs-header-view.tsx +32 -0
  177. package/src/ui/docs-navigation-view.tsx +69 -0
  178. package/src/ui/docs-page-frame.tsx +50 -0
  179. package/src/ui/dory-mark.tsx +9 -0
  180. package/src/ui/header.tsx +33 -90
  181. package/src/ui/layout.tsx +99 -153
  182. package/src/ui/mobile-navigation.tsx +10 -8
  183. package/src/ui/page-actions.tsx +67 -0
  184. package/src/ui/page-feedback.tsx +50 -0
  185. package/src/ui/page-footer.tsx +23 -0
  186. package/src/ui/prev-next-link.tsx +15 -69
  187. package/src/ui/prev-next-view.tsx +27 -0
  188. package/src/ui/table-of-content-view.tsx +167 -0
  189. package/src/ui/table-of-content.tsx +25 -183
  190. package/src/utils/analytics.test.ts +36 -0
  191. package/src/utils/analytics.ts +35 -0
  192. package/src/utils/colors.ts +7 -0
  193. package/src/utils/content-scope.test.ts +26 -0
  194. package/src/utils/content-scope.ts +39 -0
  195. package/src/utils/search-content.ts +12 -0
  196. package/src/utils/theme.ts +1 -7
  197. package/vite.config.ssr.ts +3 -3
  198. package/vite.config.ts +21 -15
@@ -0,0 +1,60 @@
1
+ import { iconSourceVariants } from './icon-source-variants'
2
+
3
+ // Keep common names used by existing Dory documents working after the icon migration.
4
+ const aliases: Record<string, string> = {
5
+ activity: 'progress',
6
+ 'alert-triangle': 'warning',
7
+ 'bar-chart': 'chart',
8
+ box: 'packages',
9
+ 'chart-column': 'chart',
10
+ 'chevrons-down-up': 'up-down',
11
+ 'circle-alert': 'alert-circle',
12
+ 'circle-check': 'check-circle',
13
+ 'code-2': 'code',
14
+ 'columns-2': 'columns',
15
+ 'columns-3': 'columns',
16
+ 'file-json': 'file-code',
17
+ 'external-link': 'external',
18
+ github: 'code',
19
+ 'git-branch': 'branch',
20
+ 'git-fork': 'branch',
21
+ 'git-pull-request': 'branch',
22
+ hammer: 'settings',
23
+ image: 'file-image',
24
+ 'layout-dashboard': 'dashboard',
25
+ layers: 'layout',
26
+ 'list-ordered': 'list',
27
+ 'message-square': 'chat',
28
+ 'maximize-2': 'fullscreen',
29
+ map: 'globe',
30
+ package: 'packages',
31
+ palette: 'layout',
32
+ 'play-circle': 'play',
33
+ 'share-2': 'share',
34
+ 'settings-2': 'settings',
35
+ 'shield-check': 'shield',
36
+ 'sliders-horizontal': 'sliders',
37
+ sparkles: 'ai',
38
+ 'square-terminal': 'terminal',
39
+ plug: 'integration',
40
+ tag: 'price-tag',
41
+ heart: 'star',
42
+ rocket: 'promote',
43
+ zap: 'bolt',
44
+ }
45
+
46
+ function normalize(name: string) {
47
+ const kebab = name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/_/g, '-').toLowerCase()
48
+ return aliases[kebab] ?? kebab
49
+ }
50
+
51
+ /** Compatibility names accepted by Dory's MDX Icon component. */
52
+ export function resolveIconName(name: string): string {
53
+ const [source, sourceName] = name.split('/', 2)
54
+ if (sourceName) {
55
+ if (source !== 'community' && source !== 'enterprise-core' && source !== 'enterprise') return name
56
+ const normalized = normalize(sourceName)
57
+ return iconSourceVariants[`${source}/${normalized}`] ?? normalized
58
+ }
59
+ return normalize(name)
60
+ }
@@ -0,0 +1,102 @@
1
+ /** Published UX names for source-specific variants in the older Dory icon API. */
2
+ export const iconSourceVariants: Record<string, string> = {
3
+ 'enterprise-core/banknotes': 'banknotes-alt',
4
+ 'enterprise-core/calendar': 'calendar-alt',
5
+ 'enterprise-core/check-circle': 'check-circle-alt',
6
+ 'enterprise-core/chevron-down': 'chevron-down-alt',
7
+ 'enterprise-core/chevron-left': 'chevron-left-alt',
8
+ 'enterprise-core/chevron-right': 'chevron-right-alt',
9
+ 'enterprise-core/chevron-up': 'chevron-up-alt',
10
+ 'enterprise-core/clock': 'clock-alt',
11
+ 'enterprise-core/code': 'code-alt',
12
+ 'enterprise-core/copy': 'copy-alt',
13
+ 'enterprise-core/download': 'download-alt',
14
+ 'enterprise-core/file-text': 'file-text-alt',
15
+ 'enterprise-core/globe': 'globe-alt',
16
+ 'enterprise-core/grid': 'grid-alt',
17
+ 'enterprise-core/hash': 'hash-alt',
18
+ 'enterprise-core/help': 'help-alt',
19
+ 'enterprise-core/home': 'home-alt',
20
+ 'enterprise-core/list': 'list-alt',
21
+ 'enterprise-core/mail': 'mail-alt',
22
+ 'enterprise-core/moon': 'moon-alt',
23
+ 'enterprise-core/play': 'play-alt',
24
+ 'enterprise-core/plus': 'plus-alt',
25
+ 'enterprise-core/plus-circle': 'plus-circle-alt',
26
+ 'enterprise-core/search': 'search-alt',
27
+ 'enterprise-core/secret': 'secret-alt',
28
+ 'enterprise-core/settings': 'settings-alt',
29
+ 'enterprise-core/sidebar': 'sidebar-alt',
30
+ 'enterprise-core/signout': 'signout-alt',
31
+ 'enterprise-core/source': 'source-alt',
32
+ 'enterprise-core/sun': 'sun-alt',
33
+ 'enterprise-core/trash': 'trash-alt',
34
+ 'enterprise-core/upload': 'upload-alt',
35
+ 'enterprise/arrow-right': 'arrow-right-alt',
36
+ 'enterprise/bag': 'bag-alt',
37
+ 'enterprise/bell': 'bell-alt',
38
+ 'enterprise/calculator': 'calculator-alt',
39
+ 'enterprise/calendar': 'calendar-alt-2',
40
+ 'enterprise/card': 'card-alt',
41
+ 'enterprise/chat': 'chat-alt',
42
+ 'enterprise/check': 'check-alt',
43
+ 'enterprise/checkbox': 'select-all',
44
+ 'enterprise/chevron-down': 'chevron-down-alt-2',
45
+ 'enterprise/chevron-left': 'chevron-left-alt-2',
46
+ 'enterprise/chevron-right': 'chevron-right-alt-2',
47
+ 'enterprise/chevron-up': 'chevron-up-alt-2',
48
+ 'enterprise/clipboard': 'clipboard-alt',
49
+ 'enterprise/clock': 'clock-alt-2',
50
+ 'enterprise/close': 'close-alt',
51
+ 'enterprise/cloud': 'cloud-alt',
52
+ 'enterprise/columns': 'columns-alt',
53
+ 'enterprise/copy': 'copy-alt-2',
54
+ 'enterprise/customer': 'user',
55
+ 'enterprise/dashboard': 'dashboard-alt',
56
+ 'enterprise/download': 'download-alt-2',
57
+ 'enterprise/edit': 'edit-alt',
58
+ 'enterprise/external': 'external-alt',
59
+ 'enterprise/eye': 'eye-alt',
60
+ 'enterprise/eye-off': 'eye-off-alt',
61
+ 'enterprise/file': 'file-alt',
62
+ 'enterprise/file-text': 'file-text-alt-2',
63
+ 'enterprise/filter': 'filter-alt',
64
+ 'enterprise/folder': 'folder-alt',
65
+ 'enterprise/formula': 'formula-alt',
66
+ 'enterprise/globe': 'globe-alt-2',
67
+ 'enterprise/group': 'folder-alt',
68
+ 'enterprise/hash': 'hash-alt-2',
69
+ 'enterprise/help': 'help-alt-2',
70
+ 'enterprise/home': 'home-alt-2',
71
+ 'enterprise/info': 'info-alt',
72
+ 'enterprise/link': 'link-alt',
73
+ 'enterprise/lock': 'lock-alt',
74
+ 'enterprise/mail': 'mail-alt-2',
75
+ 'enterprise/menu': 'menu-alt',
76
+ 'enterprise/minus': 'minus-alt',
77
+ 'enterprise/monitor': 'monitor-alt',
78
+ 'enterprise/moon': 'moon-alt-2',
79
+ 'enterprise/more-v': 'more-v-alt',
80
+ 'enterprise/order': 'order-alt',
81
+ 'enterprise/plus': 'plus-alt-2',
82
+ 'enterprise/redo': 'redo-alt',
83
+ 'enterprise/refresh': 'refresh-alt',
84
+ 'enterprise/save': 'save-alt',
85
+ 'enterprise/scan': 'scan-alt',
86
+ 'enterprise/search': 'search-alt-2',
87
+ 'enterprise/send': 'send-alt',
88
+ 'enterprise/settings': 'settings-alt-2',
89
+ 'enterprise/share': 'share-alt',
90
+ 'enterprise/sidebar': 'sidebar-alt-2',
91
+ 'enterprise/sliders': 'sliders-alt',
92
+ 'enterprise/star': 'star-alt',
93
+ 'enterprise/store': 'store-alt',
94
+ 'enterprise/sun': 'sun-alt-2',
95
+ 'enterprise/table': 'table-alt',
96
+ 'enterprise/toggle': 'toggle-alt',
97
+ 'enterprise/trash': 'trash-alt-2',
98
+ 'enterprise/undo': 'undo-alt',
99
+ 'enterprise/upload': 'upload-alt-2',
100
+ 'enterprise/users': 'users-alt',
101
+ 'enterprise/warning': 'warning-alt',
102
+ }
package/src/mdx/icon.tsx CHANGED
@@ -1,80 +1,19 @@
1
- import { library } from '@fortawesome/fontawesome-svg-core'
2
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
- import * as solidIcons from '@fortawesome/free-solid-svg-icons'
4
- import * as regularIcons from '@fortawesome/free-regular-svg-icons'
5
- import * as brandIcons from '@fortawesome/free-brands-svg-icons'
1
+ import { CatalogIcon } from '@clidey/ux'
2
+ import { resolveIconName } from './icon-names'
6
3
  import classNames from 'classnames'
7
- import * as LucideIcons from 'lucide-react'
8
4
 
9
- // Map types to icon sets and FontAwesome prefixes
10
- const iconSets = {
11
- solid: solidIcons,
12
- regular: regularIcons,
13
- brands: brandIcons,
14
- }
15
-
16
- const prefixes = {
17
- solid: 'fas',
18
- regular: 'far',
19
- brands: 'fab',
20
- } as const
21
-
22
- export function Icon({
23
- icon,
24
- iconType = 'solid',
25
- color = '',
26
- size = 24,
27
- className,
28
- ...rest
29
- }: {
5
+ export function Icon({ icon, color, size = 24, className, style, ...rest }: {
30
6
  icon: string
31
- iconType?: keyof typeof iconSets
32
7
  color?: string
33
8
  size?: number
34
9
  } & Omit<React.ComponentPropsWithoutRef<'svg'>, 'color'>) {
35
- // Convert kebab-case or snake_case to PascalCase (for Lucide and FA variable names)
36
- const pascalCase = icon
37
- .replace(/[-_](.)/g, (_, group1) => group1.toUpperCase())
38
- .replace(/^(.)/, (_, group1) => group1.toUpperCase())
39
-
40
- // --- Try Lucide First ---
41
- const LucideComponent = LucideIcons[pascalCase as keyof typeof LucideIcons] as any
42
- if (LucideComponent) {
43
- return (
44
- <LucideComponent
45
- className={classNames("inline-block align-[-0.2em] stroke-black dark:stroke-white", className)}
46
- color={color}
47
- size={size}
48
- {...rest}
49
- />
50
- )
51
- }
52
-
53
- // --- Try Font Awesome ---
54
- // Fall back to the brands set: Lucide 1.x removed all brand icons
55
- // (github, twitter, npm, ...), which live only in FA brands.
56
- const iconKey = `fa${pascalCase}`
57
- const setsToTry: Array<keyof typeof iconSets> =
58
- iconType === 'brands' ? ['brands'] : [iconType, 'brands']
59
-
60
- for (const setName of setsToTry) {
61
- const faSet = iconSets[setName] ?? solidIcons
62
- const faIcon = faSet[iconKey as keyof typeof faSet]
63
-
64
- if (faIcon && typeof faIcon === 'object' && 'iconName' in faIcon) {
65
- library.add(faIcon)
66
- return (
67
- <FontAwesomeIcon
68
- icon={[prefixes[setName] || 'fas', faIcon.iconName] as any}
69
- className={classNames("inline-block align-[-0.2em] stroke-black dark:stroke-white", className)}
70
- color={color as any}
71
- size={size as any}
72
- {...rest as any}
73
- />
74
- )
75
- }
76
- }
77
-
78
- console.warn(`⚠️ Icon not found: "${icon}" (converted to ${iconKey}) in ${iconType}`)
79
- return null // or return a default/fallback icon if preferred
10
+ return (
11
+ <CatalogIcon
12
+ name={resolveIconName(icon)}
13
+ style={{ color, ...(typeof style === 'object' && style !== null ? style : {}) }}
14
+ size={size}
15
+ className={classNames('inline-block align-[-0.2em]', className)}
16
+ {...rest}
17
+ />
18
+ )
80
19
  }
package/src/mdx/latex.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
1
+ import { CatalogIcon } from '@clidey/ux'
2
+
2
3
  import { useEffect, useRef, useState } from 'react'
3
4
 
4
5
  function extractText(node: React.ReactNode): string {
@@ -73,7 +74,7 @@ export function Latex({ children, inline = false }: LatexProps) {
73
74
  <div className="my-4">
74
75
  <div className="flex flex-col items-center justify-center py-12 px-6 text-center bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 border-2 border-dashed border-red-300 dark:border-red-700 rounded-lg">
75
76
  <div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
76
- <ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
77
+ <CatalogIcon name="warning" className="w-8 h-8 text-red-500" />
77
78
  </div>
78
79
  <h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
79
80
  LaTeX Rendering Error
package/src/mdx/mdx.tsx CHANGED
@@ -22,7 +22,7 @@ export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
22
22
  export { UnorderedList, OrderedList, ListItem } from './list';
23
23
  export { AsyncAPI } from './async-api';
24
24
  export { Source } from './source';
25
- export { Specimen, SpecimenProps } from './specimen';
25
+ export { Specimen, SpecimenProps, Prop } from './specimen';
26
26
  export { Chip, ChipGroup } from './chip';
27
27
  export { Tabs, Tab } from './tabs';
28
28
  export { IconGrid } from './icon-grid';
@@ -1,4 +1,5 @@
1
- import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
1
+ import { CatalogIcon } from '@clidey/ux'
2
+
2
3
  import classNames from 'classnames'
3
4
  import { useEffect, useMemo, useRef, useState } from 'preact/compat'
4
5
 
@@ -31,7 +32,7 @@ function paletteThemeVariables() {
31
32
  const v = (name: string) => css.getPropertyValue(name).trim()
32
33
  return {
33
34
  fontFamily: v('--font-body') || 'inherit',
34
- fontSize: '13px',
35
+ fontSize: '14px',
35
36
  background: v('--card'),
36
37
  mainBkg: v('--accent'),
37
38
  primaryColor: v('--accent'),
@@ -114,6 +115,9 @@ export function MermaidRenderer({ content }: { content: string }) {
114
115
  let mounted = true
115
116
 
116
117
  const renderMermaid = async () => {
118
+ setError(null)
119
+ setIsValid(null)
120
+ setIsLoaded(false)
117
121
  try {
118
122
  const mermaid = (await import('mermaid')).default
119
123
 
@@ -122,6 +126,7 @@ export function MermaidRenderer({ content }: { content: string }) {
122
126
  mermaid.initialize({
123
127
  startOnLoad: false,
124
128
  theme: 'base',
129
+ look: 'classic',
125
130
  themeVariables: paletteThemeVariables(),
126
131
  flowchart: {
127
132
  nodeSpacing: 50,
@@ -164,7 +169,6 @@ export function MermaidRenderer({ content }: { content: string }) {
164
169
  } catch (err) {
165
170
  console.error('Mermaid rendering error:', err)
166
171
  if (mounted) {
167
- setIsValid(false)
168
172
  setError(err instanceof Error ? err.message : 'Failed to render diagram')
169
173
  }
170
174
  }
@@ -186,13 +190,15 @@ export function MermaidRenderer({ content }: { content: string }) {
186
190
  <div className="mermaid-wrapper my-8">
187
191
  <div className="flex flex-col items-center justify-center py-12 px-6 text-center bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 border-2 border-dashed border-red-300 dark:border-red-700 rounded-lg">
188
192
  <div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
189
- <ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
193
+ <CatalogIcon name="warning" className="w-8 h-8 text-red-500" />
190
194
  </div>
191
195
  <h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
192
- Diagram Syntax Error
196
+ {isValid === false ? 'Diagram Syntax Error' : 'Diagram Load Error'}
193
197
  </h3>
194
198
  <p className="text-red-600 dark:text-red-400 mb-4 max-w-md">
195
- The Mermaid diagram contains invalid syntax and cannot be rendered.
199
+ {isValid === false
200
+ ? 'The Mermaid diagram contains invalid syntax and cannot be rendered.'
201
+ : 'The diagram could not be loaded. Please reload the page.'}
196
202
  </p>
197
203
  <div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md">
198
204
  <p className="text-sm text-red-700 dark:text-red-300 font-mono break-all">
@@ -1,12 +1,13 @@
1
- import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from "@clidey/ux"
1
+ import { CatalogIcon, Button } from "@clidey/ux"
2
2
  import classNames from "classnames"
3
- import { Check, ChevronRight, Copy, Play } from "lucide-react"
3
+
4
4
  import { h } from "preact"
5
5
  import { useState } from "preact/hooks"
6
- import { APIPlayground, type Server } from "./api-playground"
6
+ import type { Server } from "./api-playground"
7
7
  import { CodeFrame, CodeGroup } from "./code"
8
8
  import { Fence, Pre } from "./fence"
9
9
  import { Tag } from "./tag"
10
+ import { TryItPanel, type TryItField } from "./try-it-panel"
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Field list used by the API pages and exposed to MDX as Properties/Property.
@@ -178,6 +179,11 @@ function exampleOf(schema: Schema, depth = 0): unknown {
178
179
  }
179
180
  }
180
181
 
182
+ /** Short type for a form field: `enum` rather than `enum<string>`. */
183
+ function fieldType(schema: Schema): string {
184
+ return schema.enum ? 'enum' : schema.type ?? 'object'
185
+ }
186
+
181
187
  function describeDefault(schema: Schema, description?: string): string | undefined {
182
188
  if (description) return description
183
189
  if (schema.description) return schema.description
@@ -223,17 +229,17 @@ function ChildAttributes({ schema, components }: { schema: Schema; components: C
223
229
  const count = Object.keys(schema.properties ?? {}).length
224
230
  if (count === 0) return null
225
231
  return (
226
- <div className="mt-3">
232
+ <div className="mt-3 overflow-hidden rounded-lg border border-[var(--border)]">
227
233
  <button
228
234
  type="button"
229
235
  onClick={() => setOpen((v) => !v)}
230
236
  aria-expanded={open}
231
- className="flex w-full items-center gap-2 rounded-lg border border-[var(--border)] bg-muted px-4 py-2.5 text-left text-sm font-medium text-foreground"
237
+ className="flex w-full items-center gap-2 bg-muted px-4 py-2.5 text-left text-sm font-medium text-foreground"
232
238
  >
233
- <ChevronRight aria-hidden="true" className={classNames('h-3.5 w-3.5 transition-transform', { 'rotate-90': open })} />
239
+ <CatalogIcon name="chevron-right" aria-hidden="true" className={classNames('h-3.5 w-3.5 transition-transform', { 'rotate-90': open })} />
234
240
  {open ? 'Hide' : 'Show'} {count} child attribute{count === 1 ? '' : 's'}
235
241
  </button>
236
- {open && <div className="pl-4"><SchemaFields schema={schema} components={components} /></div>}
242
+ {open && <div className="px-4"><SchemaFields schema={schema} components={components} /></div>}
237
243
  </div>
238
244
  )
239
245
  }
@@ -247,7 +253,7 @@ function CopyUrl({ value }: { value: string }) {
247
253
  onClick={() => navigator.clipboard.writeText(value).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1000) })}
248
254
  className="rounded-md p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
249
255
  >
250
- {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
256
+ {copied ? <CatalogIcon name="check" className="h-3.5 w-3.5" /> : <CatalogIcon name="copy" className="h-3.5 w-3.5" />}
251
257
  </button>
252
258
  )
253
259
  }
@@ -299,6 +305,32 @@ function ResponseTabs({ responses, components }: { responses: PathMethod['respon
299
305
  )
300
306
  }
301
307
 
308
+ // Longest line of the cURL `-d` body before it continues on the next line.
309
+ const CURL_BODY_WIDTH = 44
310
+
311
+ /** `-d` lines for cURL, breaking a flat JSON object across lines so it reads without wrapping. */
312
+ function curlBody(body: unknown): string[] {
313
+ const json = JSON.stringify(body)
314
+ if (body === null || typeof body !== 'object' || Array.isArray(body) || json.length <= CURL_BODY_WIDTH) {
315
+ return [` -d '${json}'`]
316
+ }
317
+ const lines: string[] = []
318
+ let line = ''
319
+ for (const [key, value] of Object.entries(body)) {
320
+ const entry = `${JSON.stringify(key)}:${JSON.stringify(value)}`
321
+ if (line && line.length + entry.length + 1 > CURL_BODY_WIDTH) {
322
+ lines.push(`${line},`)
323
+ line = entry
324
+ } else {
325
+ line = line ? `${line},${entry}` : entry
326
+ }
327
+ }
328
+ lines.push(line)
329
+ return lines.map((text, index) =>
330
+ `${index === 0 ? " -d '{" : ' '}${text}${index === lines.length - 1 ? "}'" : ''}`
331
+ )
332
+ }
333
+
302
334
  /** cURL / JavaScript / Python samples generated from the operation. */
303
335
  function RequestSamples({ method, url, headers, body }: { method: string; url: string; headers: Record<string, string>; body?: unknown }) {
304
336
  const headerLines = Object.entries(headers)
@@ -306,7 +338,7 @@ function RequestSamples({ method, url, headers, body }: { method: string; url: s
306
338
  const curl = [
307
339
  `curl -X ${method} ${url} \\`,
308
340
  ...headerLines.map(([key, value]) => ` -H "${key}: ${value}" \\`),
309
- ...(json ? [` -d '${json}'`] : []),
341
+ ...(json ? curlBody(body) : []),
310
342
  ].join('\n').replace(/ \\$/, '')
311
343
  const js = [
312
344
  `const response = await fetch("${url}", {`,
@@ -384,20 +416,39 @@ export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPat
384
416
  }
385
417
  if (bodySchema) headers['Content-Type'] = 'application/json'
386
418
 
419
+ const parameterFields: TryItField[] = parameters.flatMap((param) => {
420
+ if (param.in === 'cookie') return []
421
+ const schema = resolveSchema(param.schema, components)
422
+ const value = schema.example ?? schema.default
423
+ return [{ name: param.name, type: fieldType(schema), value: value === undefined ? '' : String(value), in: param.in ?? 'query' }]
424
+ })
425
+ const bodyFields: TryItField[] = Object.entries(bodySchema?.properties ?? {}).map(([name, raw]) => {
426
+ const schema = resolveSchema(raw, components)
427
+ const value = exampleOf(schema)
428
+ return { name, type: fieldType(schema), value: typeof value === 'string' ? value : JSON.stringify(value) }
429
+ })
430
+ const authInputs: TryItField[] = schemes.flatMap((scheme) => {
431
+ const header = authHeader(scheme)
432
+ if (!header) return []
433
+ return [{ name: header[0], type: 'header', value: header[1].replace(/\$\w+$/, '') }]
434
+ })
435
+
387
436
  return (
388
437
  <div className="dory-api max-w-none w-full">
389
- <Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
390
- <SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
391
- <SheetHeader className="p-0">
392
- <SheetTitle>API Playground</SheetTitle>
393
- </SheetHeader>
394
- <APIPlayground method={selectedMethod} url={selectedPath} servers={openAPIJson.servers} />
395
- </SheetContent>
396
- </Sheet>
397
-
398
- {operation.description && <p className="dory-lead -mt-4 mb-6">{operation.description}</p>}
399
-
400
- <div className="not-prose flex items-center gap-3 rounded-lg border border-[var(--border)] bg-card py-2 pl-3 pr-2">
438
+ <TryItPanel
439
+ open={showPlayground}
440
+ onOpenChange={setShowPlayground}
441
+ method={selectedMethod}
442
+ path={selectedPath}
443
+ servers={openAPIJson.servers ?? []}
444
+ parameters={parameterFields}
445
+ body={bodyFields}
446
+ auth={authInputs}
447
+ />
448
+
449
+ {operation.description && <p className="dory-lead -mt-4! mb-6">{operation.description}</p>}
450
+
451
+ <div className="not-prose flex max-w-[45rem] items-center gap-3 rounded-lg border border-[var(--border)] bg-card py-2 pl-3 pr-2">
401
452
  <Tag>{selectedMethod}</Tag>
402
453
  <span className="min-w-0 flex-1 truncate font-mono text-sm">
403
454
  <span className="text-muted-foreground">{serverUrl}</span>
@@ -405,63 +456,68 @@ export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPat
405
456
  </span>
406
457
  <CopyUrl value={fullUrl} />
407
458
  <Button onClick={() => setShowPlayground(true)} className="flex items-center gap-1.5 bg-[var(--primary)] text-white hover:bg-[var(--primary)]/90">
408
- <Play className="h-3 w-3 fill-current" />
459
+ <CatalogIcon name="play" className="h-3 w-3 fill-current" />
409
460
  Try it
410
461
  </Button>
411
462
  </div>
412
463
 
413
- {schemes.length > 0 && (
414
- <>
415
- <SectionTitle>Authorization</SectionTitle>
416
- <Properties>
417
- {schemes.map((scheme, index) => {
418
- const header = authHeader(scheme)
419
- const name = header?.[0] ?? scheme.name ?? scheme.type
420
- const location = scheme.type === 'apiKey' ? scheme.in ?? 'header' : 'header'
421
- return (
422
- <Property key={index} name={name} type={`${location} · string`} required>
423
- {scheme.description ?? (scheme.type === 'http' ? `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} token.` : 'API key.')}
424
- </Property>
425
- )
426
- })}
427
- </Properties>
428
- </>
429
- )}
430
-
431
- {parameters.length > 0 && (
432
- <>
433
- <SectionTitle>Parameters</SectionTitle>
434
- <Properties>
435
- {parameters.map((param) => {
436
- const schema = resolveSchema(param.schema, components)
437
- return (
438
- <Property key={`${param.in}-${param.name}`} name={param.name} type={`${param.in ?? 'query'} · ${typeLabel(schema)}`} required={param.required} defaultValue={schema.default}>
439
- {describeDefault(schema, param.description)}
440
- </Property>
441
- )
442
- })}
443
- </Properties>
444
- </>
445
- )}
446
-
447
- {bodySchema && (
448
- <>
449
- <SectionTitle note="application/json">Body</SectionTitle>
450
- {operation.requestBody?.description && <p className="mt-2 text-sm text-muted-foreground">{operation.requestBody.description}</p>}
451
- <SchemaFields schema={bodySchema} components={components} />
452
- </>
453
- )}
454
-
455
- {responseSchema && (
456
- <>
457
- <SectionTitle>Response</SectionTitle>
458
- <SchemaFields schema={responseSchema} components={components} />
459
- </>
460
- )}
461
-
462
- <div className="mt-8">
463
- <RequestSamples method={selectedMethod} url={fullUrl} headers={headers} body={bodySchema ? exampleOf(bodySchema) : undefined} />
464
- <ResponseTabs responses={operation.responses} components={components} />
464
+ <div className="grid gap-x-12 xl:grid-cols-[minmax(0,1fr)_29rem]">
465
+ <div className="min-w-0">
466
+ {schemes.length > 0 && (
467
+ <>
468
+ <SectionTitle>Authorization</SectionTitle>
469
+ <Properties>
470
+ {schemes.map((scheme, index) => {
471
+ const header = authHeader(scheme)
472
+ const name = header?.[0] ?? scheme.name ?? scheme.type
473
+ const location = scheme.type === 'apiKey' ? scheme.in ?? 'header' : 'header'
474
+ return (
475
+ <Property key={index} name={name} type={`${location} · string`} required>
476
+ {scheme.description ?? (scheme.type === 'http' ? `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} token.` : 'API key.')}
477
+ </Property>
478
+ )
479
+ })}
480
+ </Properties>
481
+ </>
482
+ )}
483
+
484
+ {parameters.length > 0 && (
485
+ <>
486
+ <SectionTitle>Parameters</SectionTitle>
487
+ <Properties>
488
+ {parameters.map((param) => {
489
+ const schema = resolveSchema(param.schema, components)
490
+ return (
491
+ <Property key={`${param.in}-${param.name}`} name={param.name} type={`${param.in ?? 'query'} · ${typeLabel(schema)}`} required={param.required} defaultValue={schema.default}>
492
+ {describeDefault(schema, param.description)}
493
+ </Property>
494
+ )
495
+ })}
496
+ </Properties>
497
+ </>
498
+ )}
499
+
500
+ {bodySchema && (
501
+ <>
502
+ <SectionTitle note="application/json">Body</SectionTitle>
503
+ {operation.requestBody?.description && <p className="mt-2 text-sm text-muted-foreground">{operation.requestBody.description}</p>}
504
+ <SchemaFields schema={bodySchema} components={components} />
505
+ </>
506
+ )}
507
+
508
+ {responseSchema && (
509
+ <>
510
+ <SectionTitle>Response</SectionTitle>
511
+ <SchemaFields schema={responseSchema} components={components} />
512
+ </>
513
+ )}
514
+ </div>
515
+
516
+ {/* Samples sit beside the fields on wide screens and follow the reader down the page. */}
517
+ <aside className="mt-10 min-w-0 xl:sticky xl:top-20 xl:self-start">
518
+ <RequestSamples method={selectedMethod} url={fullUrl} headers={headers} body={bodySchema ? exampleOf(bodySchema) : undefined} />
519
+ <ResponseTabs responses={operation.responses} components={components} />
520
+ </aside>
465
521
  </div>
466
522
  </div>
467
523
  )
@@ -0,0 +1,16 @@
1
+ import { expect, test } from 'vitest';
2
+ import { preprocessMdxSource } from './preprocess';
3
+
4
+ test('page title is rendered by the page frame without repeating in MDX', () => {
5
+ const result = preprocessMdxSource('---\ntitle: "Installation"\n---\n# Installation\n\n## Setup\nInstall it.');
6
+ expect(result).not.toMatch(/^# Installation$/m);
7
+ expect(result).toContain('## Setup');
8
+ expect(result).toContain('Install it.');
9
+ });
10
+
11
+ test('keeps code examples, distinct headings and pages without a frontmatter title', () => {
12
+ const result = preprocessMdxSource('---\ntitle: Guide\n---\n```md\n# Guide\n```\n# A different heading');
13
+ expect(result).toContain('```md\n# Guide\n```');
14
+ expect(result).toContain('# A different heading');
15
+ expect(preprocessMdxSource('# Guide')).toContain('# Guide');
16
+ });