@clidey/dory 1.0.8 → 1.10.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 (210) hide show
  1. package/README.md +59 -164
  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/dependency-overlay.d.ts +7 -0
  20. package/bin/dist/dependency-overlay.d.ts.map +1 -0
  21. package/bin/dist/dependency-overlay.js +137 -0
  22. package/bin/dist/dependency-overlay.js.map +1 -0
  23. package/bin/dist/dory.js +254 -402
  24. package/bin/dist/dory.js.map +1 -1
  25. package/bin/dist/i18n-init.d.ts +6 -0
  26. package/bin/dist/i18n-init.d.ts.map +1 -0
  27. package/bin/dist/i18n-init.js +56 -0
  28. package/bin/dist/i18n-init.js.map +1 -0
  29. package/bin/dist/mcp-http.d.ts +8 -0
  30. package/bin/dist/mcp-http.d.ts.map +1 -0
  31. package/bin/dist/mcp-http.js +72 -0
  32. package/bin/dist/mcp-http.js.map +1 -0
  33. package/bin/dist/mcp.d.ts +2 -0
  34. package/bin/dist/mcp.d.ts.map +1 -0
  35. package/bin/dist/mcp.js +81 -0
  36. package/bin/dist/mcp.js.map +1 -0
  37. package/bin/dist/preview-entry.d.ts +2 -0
  38. package/bin/dist/preview-entry.d.ts.map +1 -0
  39. package/bin/dist/preview-entry.js +6 -0
  40. package/bin/dist/preview-entry.js.map +1 -0
  41. package/bin/dist/preview-server.d.ts +5 -0
  42. package/bin/dist/preview-server.d.ts.map +1 -0
  43. package/bin/dist/preview-server.js +146 -0
  44. package/bin/dist/preview-server.js.map +1 -0
  45. package/bin/dist/preview-static.d.ts +7 -0
  46. package/bin/dist/preview-static.d.ts.map +1 -0
  47. package/bin/dist/preview-static.js +70 -0
  48. package/bin/dist/preview-static.js.map +1 -0
  49. package/bin/dist/project-config.d.ts +22 -0
  50. package/bin/dist/project-config.d.ts.map +1 -0
  51. package/bin/dist/project-config.js +44 -0
  52. package/bin/dist/project-config.js.map +1 -0
  53. package/dist/index.html +57 -0
  54. package/dist/runtime/dory-runtime.css +2 -0
  55. package/dist/runtime/dory-runtime.html +14 -0
  56. package/dist/runtime/dory-runtime.js +5490 -0
  57. package/docs/api-playground-demo.mdx +119 -0
  58. package/docs/api-reference/asyncapi.json +324 -0
  59. package/docs/api-reference/openapi.json +366 -0
  60. package/docs/api-reference/operations/create.mdx +5 -0
  61. package/docs/api-reference/operations/delete.mdx +5 -0
  62. package/docs/api-reference/operations/get.mdx +5 -0
  63. package/docs/api-reference/operations/update.mdx +5 -0
  64. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  65. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  66. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  67. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  68. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  69. package/docs/assets/dory.jpeg +0 -0
  70. package/docs/components/accordion.mdx +145 -0
  71. package/docs/components/callouts.mdx +134 -0
  72. package/docs/components/cards.mdx +153 -0
  73. package/docs/components/code-blocks.mdx +281 -0
  74. package/docs/components/expandable.mdx +189 -0
  75. package/docs/components/icons.mdx +71 -0
  76. package/docs/components/images.mdx +82 -0
  77. package/docs/components/latex.mdx +148 -0
  78. package/docs/components/layout.mdx +178 -0
  79. package/docs/components/lists.mdx +134 -0
  80. package/docs/components/mermaid.mdx +149 -0
  81. package/docs/components/overview.mdx +81 -0
  82. package/docs/components/source.mdx +130 -0
  83. package/docs/components/steps.mdx +122 -0
  84. package/docs/components/tables.mdx +114 -0
  85. package/docs/components/tags.mdx +179 -0
  86. package/docs/components/typography.mdx +185 -0
  87. package/docs/configuration/analytics.mdx +45 -0
  88. package/docs/configuration/chat.mdx +59 -0
  89. package/docs/configuration/dory-json.mdx +278 -0
  90. package/docs/configuration/embed.mdx +73 -0
  91. package/docs/configuration/fonts.mdx +86 -0
  92. package/docs/configuration/internationalization.mdx +61 -0
  93. package/docs/configuration/navigation.mdx +217 -0
  94. package/docs/configuration/theming.mdx +166 -0
  95. package/docs/contributing.mdx +47 -0
  96. package/docs/dory.json +136 -0
  97. package/docs/embed-example.html +176 -0
  98. package/docs/favicon.svg +18 -0
  99. package/docs/features.mdx +440 -0
  100. package/docs/getting-started/automation.mdx +76 -0
  101. package/docs/getting-started/configuration.mdx +95 -0
  102. package/docs/getting-started/custom-components.mdx +60 -0
  103. package/docs/getting-started/installation.mdx +88 -0
  104. package/docs/getting-started/quick-start.mdx +53 -0
  105. package/docs/introduction.mdx +50 -0
  106. package/docs/logo/logo.svg +18 -0
  107. package/docs/playgrounds/api-playground.mdx +122 -0
  108. package/docs/playgrounds/websocket-playground.mdx +98 -0
  109. package/docs/social-card.png +0 -0
  110. package/docs/social-card.svg +17 -0
  111. package/docs/websocket-playground-demo.mdx +135 -0
  112. package/package.json +23 -17
  113. package/scripts/verify-component-examples.mjs +126 -0
  114. package/scripts/verify-runtime-parity.mjs +147 -0
  115. package/src/app.css +22 -1
  116. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  117. package/src/assets/fonts/OFL.txt +94 -0
  118. package/src/components/chat-assistant.tsx +229 -0
  119. package/src/components/dropdown.tsx +3 -3
  120. package/src/components/error-boundary.tsx +2 -2
  121. package/src/components/hooks.tsx +20 -1
  122. package/src/components/navigation.tsx +82 -51
  123. package/src/components/notification.tsx +2 -3
  124. package/src/components/routes.tsx +17 -19
  125. package/src/components/search-index.test.ts +15 -0
  126. package/src/components/search-index.ts +49 -48
  127. package/src/components/search.tsx +208 -104
  128. package/src/components/store.ts +27 -9
  129. package/src/config/mdx-common.ts +11 -0
  130. package/src/config/mdx.ts +10 -11
  131. package/src/embed/loader.ts +0 -3
  132. package/src/embed/types.ts +0 -2
  133. package/src/embed/widget-iframe.ts +19 -21
  134. package/src/embedded-runtime/config.ts +138 -0
  135. package/src/embedded-runtime/index.tsx +680 -0
  136. package/src/embedded-runtime/runtime.html +14 -0
  137. package/src/entry-server.tsx +46 -124
  138. package/src/main.tsx +17 -5
  139. package/src/mdx/accordion.tsx +3 -3
  140. package/src/mdx/api-playground.tsx +18 -16
  141. package/src/mdx/async-api.tsx +2 -2
  142. package/src/mdx/card.tsx +3 -2
  143. package/src/mdx/code.tsx +11 -9
  144. package/src/mdx/fence.tsx +6 -5
  145. package/src/mdx/icon-grid.tsx +4 -1
  146. package/src/mdx/icon-names.test.ts +15 -0
  147. package/src/mdx/icon-names.ts +60 -0
  148. package/src/mdx/icon-source-variants.ts +102 -0
  149. package/src/mdx/icon.tsx +12 -73
  150. package/src/mdx/latex.test.ts +38 -0
  151. package/src/mdx/latex.tsx +42 -64
  152. package/src/mdx/mdx.tsx +1 -1
  153. package/src/mdx/mermaid.tsx +12 -6
  154. package/src/mdx/open-api.tsx +130 -74
  155. package/src/mdx/preprocess.test.ts +16 -0
  156. package/src/mdx/preprocess.ts +61 -0
  157. package/src/mdx/processor.test.ts +135 -37
  158. package/src/mdx/processor.ts +54 -92
  159. package/src/mdx/source.tsx +5 -4
  160. package/src/mdx/specimen.tsx +57 -43
  161. package/src/mdx/tabs.tsx +32 -21
  162. package/src/mdx/text.tsx +11 -11
  163. package/src/mdx/try-it-panel.tsx +284 -0
  164. package/src/mdx/type-scale.tsx +7 -7
  165. package/src/mdx/websocket-playground.tsx +21 -14
  166. package/src/plugins/analytics-injector.ts +8 -0
  167. package/src/plugins/chat-dev-server.ts +32 -0
  168. package/src/plugins/docs-assets.test.ts +25 -0
  169. package/src/plugins/docs-assets.ts +6 -2
  170. package/src/plugins/frontmatter-dev-server.ts +2 -2
  171. package/src/plugins/frontmatter-generator.ts +1 -1
  172. package/src/plugins/html-files-middleware.test.ts +21 -1
  173. package/src/plugins/html-files-middleware.ts +26 -6
  174. package/src/plugins/html-metadata-injector.ts +24 -4
  175. package/src/plugins/llm-txt-dev-server.ts +2 -2
  176. package/src/plugins/llm-txt-generator.ts +21 -4
  177. package/src/plugins/prerender.ts +47 -20
  178. package/src/plugins/robots-generator.ts +1 -1
  179. package/src/plugins/sanitize.ts +6 -5
  180. package/src/plugins/shared.ts +6 -18
  181. package/src/plugins/sitemap-generator.ts +1 -1
  182. package/src/plugins/social-preview.test.ts +71 -0
  183. package/src/plugins/social-preview.ts +87 -0
  184. package/src/plugins/unified-mdx-plugin.ts +1 -1
  185. package/src/theme.scss +630 -207
  186. package/src/types/config.ts +53 -1
  187. package/src/ui/breadcrumb.tsx +17 -0
  188. package/src/ui/docs-header-view.tsx +32 -0
  189. package/src/ui/docs-navigation-view.tsx +69 -0
  190. package/src/ui/docs-page-frame.tsx +50 -0
  191. package/src/ui/dory-mark.tsx +9 -0
  192. package/src/ui/header.tsx +33 -90
  193. package/src/ui/layout.tsx +99 -153
  194. package/src/ui/mobile-navigation.tsx +10 -8
  195. package/src/ui/page-actions.tsx +67 -0
  196. package/src/ui/page-feedback.tsx +50 -0
  197. package/src/ui/page-footer.tsx +23 -0
  198. package/src/ui/prev-next-link.tsx +15 -69
  199. package/src/ui/prev-next-view.tsx +27 -0
  200. package/src/ui/table-of-content-view.tsx +167 -0
  201. package/src/ui/table-of-content.tsx +25 -183
  202. package/src/utils/analytics.test.ts +36 -0
  203. package/src/utils/analytics.ts +35 -0
  204. package/src/utils/colors.ts +7 -0
  205. package/src/utils/content-scope.test.ts +26 -0
  206. package/src/utils/content-scope.ts +39 -0
  207. package/src/utils/search-content.ts +12 -0
  208. package/src/utils/theme.ts +1 -7
  209. package/vite.config.ssr.ts +4 -4
  210. package/vite.config.ts +22 -16
@@ -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
  }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createElement } from 'preact';
3
+ import { renderToString } from 'preact-render-to-string';
4
+ import katex from 'katex';
5
+ import { Latex, renderLatexExpression } from './latex';
6
+
7
+ describe('LaTeX rendering', () => {
8
+ it('server-renders a readable display fallback with the same client shell', () => {
9
+ const html = renderToString(createElement(Latex, { children: 'E = mc^2' }));
10
+ expect(html).toContain('dory-latex not-prose');
11
+ expect(html).toContain('aria-busy="true"');
12
+ expect(html).toContain('data-latex-fallback');
13
+ expect(html).toContain('E = mc^2');
14
+ expect(html).not.toContain('animate-spin');
15
+ });
16
+
17
+ it('keeps the inline fallback inline and escapes source text', () => {
18
+ const html = renderToString(createElement(Latex, { inline: true, children: '<img src=x onerror=alert(1)>' }));
19
+ expect(html).toMatch(/^<span /);
20
+ expect(html).not.toContain('<div');
21
+ expect(html).not.toContain('<img');
22
+ expect(html).toContain('&lt;img');
23
+ });
24
+
25
+ it('renders complex math with KaTeX and the requested mode', () => {
26
+ const expression = String.raw`\frac{a}{b}`;
27
+ expect(renderLatexExpression(katex, expression, false)).toContain('katex-display');
28
+ const inline = renderLatexExpression(katex, expression, true);
29
+ expect(inline).toContain('mfrac');
30
+ expect(inline).not.toContain('katex-display');
31
+ });
32
+
33
+ it('does not allow formulas to create trusted links, HTML classes, or image requests', () => {
34
+ expect(renderLatexExpression(katex, String.raw`\href{javascript:alert(1)}{click}`, true)).not.toMatch(/href=["']javascript:/);
35
+ expect(renderLatexExpression(katex, String.raw`\htmlClass{injected}{x}`, false)).not.toMatch(/class=["']injected/);
36
+ expect(renderLatexExpression(katex, String.raw`\includegraphics{https://example.test/tracker.png}`, false)).not.toContain('<img');
37
+ });
38
+ });
package/src/mdx/latex.tsx CHANGED
@@ -1,12 +1,13 @@
1
- import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
2
- import { useEffect, useRef, useState } from 'react'
1
+ import { useEffect, useState } from 'preact/hooks'
3
2
 
4
3
  function extractText(node: React.ReactNode): string {
5
4
  if (typeof node === 'string') return node;
6
5
  if (typeof node === 'number') return String(node);
7
6
  if (!node) return '';
8
7
  if (Array.isArray(node)) return node.map(extractText).join('');
9
- if (typeof node === 'object' && 'props' in node) return extractText((node as any).props.children);
8
+ if (typeof node === 'object' && 'props' in node) {
9
+ return extractText((node as { props: { children?: React.ReactNode } }).props.children);
10
+ }
10
11
  return '';
11
12
  }
12
13
 
@@ -16,90 +17,67 @@ interface LatexProps {
16
17
  inline?: boolean
17
18
  }
18
19
 
19
- export function Latex({ children, inline = false }: LatexProps) {
20
- const expression = extractText(children).trim();
20
+ type RenderResult = {
21
+ expression: string
22
+ inline: boolean
23
+ html?: string
24
+ error?: string
25
+ }
21
26
 
22
- if (typeof window === 'undefined') {
23
- return inline ? <code>{expression}</code> : <div className="my-4"><code>{expression}</code></div>
24
- }
27
+ /** Only KaTeX produces markup; TeX commands cannot opt into trusted HTML or URLs. */
28
+ export function renderLatexExpression(katex: Pick<typeof import('katex').default, 'renderToString'>, expression: string, inline: boolean): string {
29
+ return katex.renderToString(expression, {
30
+ displayMode: !inline,
31
+ throwOnError: false,
32
+ trust: false,
33
+ maxExpand: 1000,
34
+ })
35
+ }
25
36
 
26
- const containerRef = useRef<HTMLDivElement>(null)
27
- const [error, setError] = useState<string | null>(null)
28
- const [isLoading, setIsLoading] = useState(true)
37
+ export function Latex({ children, inline = false }: LatexProps) {
38
+ const expression = extractText(children).trim()
39
+ const [result, setResult] = useState<RenderResult | null>(null)
40
+ // A changed expression immediately shows its own fallback while it loads.
41
+ const current = result?.expression === expression && result.inline === inline ? result : null
42
+ const isLoading = current === null
29
43
 
30
44
  useEffect(() => {
31
45
  let mounted = true
32
-
33
46
  const renderLatex = async () => {
34
47
  try {
35
- setError(null)
36
- setIsLoading(true)
37
-
38
48
  const [katexModule] = await Promise.all([
39
49
  import('katex'),
40
50
  import('katex/dist/katex.min.css'),
41
51
  ])
42
- const katex = katexModule.default
43
-
44
- if (!mounted || !containerRef.current) return
45
-
46
- katex.render(expression, containerRef.current, {
47
- displayMode: !inline,
48
- throwOnError: false,
49
- })
50
- setIsLoading(false)
51
- } catch (error) {
52
- if (mounted) {
53
- console.error('Error rendering LaTeX:', error)
54
- setError(error instanceof Error ? error.message : 'Failed to render LaTeX')
55
- setIsLoading(false)
56
- }
52
+ const html = renderLatexExpression(katexModule.default, expression, inline)
53
+ if (mounted) setResult({ expression, inline, html })
54
+ } catch {
55
+ if (mounted) setResult({ expression, inline, error: 'The equation could not be rendered.' })
57
56
  }
58
57
  }
59
-
60
58
  renderLatex()
61
-
62
- return () => {
63
- mounted = false
64
- }
59
+ return () => { mounted = false }
65
60
  }, [expression, inline])
66
61
 
62
+ // Server rendering and the first browser render use the same readable fallback.
63
+ // Keep KaTeX lazy, and let Preact own the markup instead of mutating a ref's DOM.
67
64
  if (inline) {
68
- return <span ref={containerRef as unknown as preact.RefObject<HTMLSpanElement>} className="dory-latex-inline">{isLoading && expression}</span>
69
- }
70
-
71
- if (error) {
72
65
  return (
73
- <div className="my-4">
74
- <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
- <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
- </div>
78
- <h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
79
- LaTeX Rendering Error
80
- </h3>
81
- <p className="text-red-600 dark:text-red-400 mb-4 max-w-md">
82
- The LaTeX expression could not be rendered.
83
- </p>
84
- <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">
85
- <p className="text-sm text-red-700 dark:text-red-300 font-mono break-all">
86
- {error}
87
- </p>
88
- </div>
89
- </div>
90
- </div>
66
+ <span className="dory-latex-inline" aria-busy={isLoading} title={current?.error}>
67
+ {current?.html !== undefined
68
+ ? <span dangerouslySetInnerHTML={{ __html: current.html }} />
69
+ : <span data-latex-fallback>{expression}</span>}
70
+ </span>
91
71
  )
92
72
  }
93
73
 
94
74
  return (
95
- <div className="dory-latex not-prose my-5 rounded-lg border border-[var(--border)] bg-muted/50 px-5 pb-5 pt-3">
75
+ <div className="dory-latex not-prose my-5 rounded-lg border border-[var(--border)] bg-muted/50 px-5 pb-5 pt-3" aria-busy={isLoading}>
96
76
  <span className="block font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">Display</span>
97
- {isLoading && (
98
- <div className="flex items-center justify-center py-4">
99
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[var(--primary)]" />
100
- </div>
101
- )}
102
- <div ref={containerRef} className={isLoading ? 'hidden' : 'mt-3 text-center'} />
77
+ {current?.html !== undefined
78
+ ? <div className="mt-3 overflow-x-auto text-center" dangerouslySetInnerHTML={{ __html: current.html }} />
79
+ : <span data-latex-fallback className="block overflow-x-auto px-2 py-3 text-center font-mono text-sm text-muted-foreground">{expression}</span>}
80
+ {current?.error && <p role="alert" className="mt-2 text-center text-sm text-destructive">{current.error}</p>}
103
81
  </div>
104
82
  )
105
83
  }
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">