@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
@@ -1,6 +1,6 @@
1
- import { compile } from '@mdx-js/mdx';
2
- import { preprocessMdxTags } from '../plugins/sanitize';
3
- import { getMdxConfig } from '../config/mdx';
1
+ import { compile, createProcessor } from '@mdx-js/mdx';
2
+ import { preprocessMdxSource } from './preprocess.ts';
3
+ import { getMdxConfig } from '../config/mdx.ts';
4
4
 
5
5
  export interface ProcessMdxOptions {
6
6
  isDevelopment?: boolean;
@@ -25,101 +25,63 @@ export async function preprocessMdxContent(
25
25
  error?: Error;
26
26
  }> {
27
27
  try {
28
- // Step 0: Strip YAML frontmatter before any processing.
29
- // Frontmatter is parsed separately by the frontmatter-generator plugin.
30
- let processedContent = content;
31
- const frontMatterMatch = processedContent.match(/^---\n([\s\S]*?)\n---\n?/);
32
- if (frontMatterMatch) {
33
- processedContent = processedContent.slice(frontMatterMatch[0].length);
34
- }
35
-
36
- // Step 0b: Strip stray ```yaml metadata blocks that follow frontmatter.
37
- // Some content generators produce a second YAML block (```yaml...---) after
38
- // the frontmatter. This is a broken code fence (closed with --- instead of ```)
39
- // that leaks metadata into the rendered page.
40
- const yamlBlockMatch = processedContent.match(/^```yaml\n([\s\S]*?)\n---\n?/);
41
- if (yamlBlockMatch) {
42
- processedContent = processedContent.slice(yamlBlockMatch[0].length);
43
- }
44
-
45
- // Step 1: Apply preprocessor (handles MDX tags, code blocks, etc.)
46
- const preprocessor = preprocessMdxTags();
47
-
48
- if (preprocessor.transform) {
49
- const result = preprocessor.transform(processedContent, fileName);
50
- if (result && typeof result === 'object' && result.code) {
51
- processedContent = result.code;
52
- } else if (typeof result === 'string') {
53
- processedContent = result;
54
- }
55
- }
56
-
57
- // Step 2: Convert URLs in angle brackets to markdown links
58
- // Handles cases like <https://example.com> which MDX would interpret as JSX
59
- // But preserves them inside code blocks
60
-
61
- // Process line by line to handle code blocks correctly
62
- const lines = processedContent.split('\n');
63
- let inCodeBlock = false;
64
-
65
- processedContent = lines.map(line => {
66
- // Check if we're entering or leaving a code block (with optional indentation)
67
- if (line.trim().startsWith('```')) {
68
- inCodeBlock = !inCodeBlock;
69
- return line;
28
+ const processed = preprocessMdxSource(content, fileName);
29
+ // Only real top-level ESM declarations register static JSX components.
30
+ // The browser's embedded runtime does not call this trusted build processor.
31
+ const importedNames = (source: string) => {
32
+ const names: string[] = [];
33
+ let declarations = 0;
34
+ for (const node of createProcessor().parse(source).children) {
35
+ if (node.type !== 'mdxjsEsm') continue;
36
+ for (const statement of node.data?.estree?.body ?? []) {
37
+ if (statement.type !== 'ImportDeclaration') continue;
38
+ declarations++;
39
+ for (const specifier of statement.specifiers) names.push(specifier.local.name);
40
+ }
70
41
  }
71
-
72
- // Don't process lines inside code blocks
73
- if (inCodeBlock) {
74
- return line;
75
- }
76
-
77
- // For lines outside code blocks, preserve inline code
78
- // More robust regex that handles inline code with potential spaces
79
- // Matches: `...` including multi-line content
80
- let result = '';
81
- let lastIndex = 0;
82
-
83
- // Find all inline code spans in the line
84
- const inlineCodeRegex = /`([^`]+)`/g;
85
- let match;
86
-
87
- while ((match = inlineCodeRegex.exec(line)) !== null) {
88
- // Process text before the inline code
89
- const beforeCode = line.substring(lastIndex, match.index);
90
- result += beforeCode.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
91
-
92
- // Keep inline code as-is
93
- result += match[0];
94
-
95
- lastIndex = match.index + match[0].length;
42
+ return { names, declarations };
43
+ };
44
+ let importedComponents: string[] = [];
45
+ if (/^import(?:\s|$)/m.test(processed)) {
46
+ try {
47
+ importedComponents = importedNames(processed).names;
48
+ } catch {
49
+ // Escaped closing tags can temporarily make imported JSX with props
50
+ // malformed. Gather provisional names from isolated import declarations.
51
+ // They are never trusted until the complete document's AST verifies them.
52
+ const candidates = new Set<string>();
53
+ const lines = processed.split('\n');
54
+ let fence = '';
55
+ for (let index = 0; index < lines.length; index++) {
56
+ const marker = lines[index].match(/^\s*(`{3,}|~{3,})(.*)$/);
57
+ if (marker) {
58
+ if (!fence) fence = marker[1];
59
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = '';
60
+ continue;
61
+ }
62
+ if (fence || !/^import(?:\s|$)/.test(lines[index])) continue;
63
+ for (let end = index; end < lines.length; end++) {
64
+ try {
65
+ const result = importedNames(lines.slice(index, end + 1).join('\n'));
66
+ if (!result.declarations) continue;
67
+ for (const name of result.names) candidates.add(name);
68
+ index = end;
69
+ break;
70
+ } catch { /* The declaration may span more lines. */ }
71
+ }
72
+ }
73
+ const provisional = preprocessMdxSource(content, fileName, [...candidates]);
74
+ importedComponents = importedNames(provisional).names;
96
75
  }
97
-
98
- // Process remaining text after last inline code (or entire line if no inline code)
99
- const remainingText = line.substring(lastIndex);
100
- result += remainingText.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
101
-
102
- return result;
103
- }).join('\n');
104
-
105
- // Step 3: Add any future preprocessing steps here
106
- // Example: processedContent = await yourNewPreprocessor(processedContent);
107
- // Example: processedContent = removeHTMLComments(processedContent);
108
- // Example: processedContent = expandCustomShortcodes(processedContent);
109
- // ALL preprocessing added here will automatically apply to:
110
- // - CLI commands (verify:content)
111
- // - Vite build (pnpm run build)
112
- // - Vite dev server (pnpm run dev)
113
-
76
+ }
114
77
  return {
115
78
  success: true,
116
- processedContent
79
+ processedContent: importedComponents.length
80
+ ? preprocessMdxSource(content, fileName, importedComponents)
81
+ : processed,
117
82
  };
118
83
  } catch (error) {
119
- return {
120
- success: false,
121
- error: error as Error
122
- };
84
+ return { success: false, error: error as Error };
123
85
  }
124
86
  }
125
87
 
@@ -1,5 +1,6 @@
1
+ import { CatalogIcon } from '@clidey/ux'
1
2
  import classNames from 'classnames'
2
- import { ExternalLink, File } from 'lucide-react'
3
+
3
4
  import { useEffect, useState } from 'preact/hooks'
4
5
  import { Fence } from './fence'
5
6
 
@@ -121,7 +122,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
121
122
  : 'border-[var(--border)] bg-muted text-foreground hover:border-[var(--primary)]',
122
123
  )}
123
124
  >
124
- <File aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
125
+ <CatalogIcon name="file" aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
125
126
  {path}
126
127
  {range && <span className="text-muted-foreground">{range}</span>}
127
128
  </button>
@@ -129,7 +130,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
129
130
  </div>
130
131
  <div className="dory-code mt-3 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
131
132
  <div className="flex min-h-10 items-center gap-2 border-b border-[var(--border)] bg-muted px-4">
132
- <File aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
133
+ <CatalogIcon name="file" aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
133
134
  <p className="mr-auto truncate font-mono text-xs text-foreground">
134
135
  {repo} · {current.path}
135
136
  {current.range && <span className="ml-2 text-muted-foreground">{current.range}</span>}
@@ -141,7 +142,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
141
142
  className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-[var(--primary)] hover:underline"
142
143
  >
143
144
  {openLabel}
144
- <ExternalLink aria-hidden="true" className="h-3 w-3" />
145
+ <CatalogIcon name="external" aria-hidden="true" className="h-3 w-3" />
145
146
  </a>
146
147
  </div>
147
148
  {ready ? (
@@ -1,18 +1,39 @@
1
- import classNames from 'classnames'
2
1
  import { Children, isValidElement } from 'preact/compat'
3
- import { useState } from 'preact/hooks'
4
- import { fencePropsOf } from './code'
5
- import { Pre } from './fence'
2
+ import { Tabs as UxTabs, TabsList, TabsTrigger, TabsContent, cn } from '@clidey/ux'
3
+ import { handleTabKeyDown } from './tabs'
4
+ import { CopyButton, fencePropsOf } from './code'
5
+ import { Fence, Pre } from './fence'
6
6
 
7
- /** Marks the props table inside a Specimen; its children render in the Props tab. */
7
+ /** Props tab body: a PROP / DESCRIPTION header followed by <Prop> rows. */
8
8
  export function SpecimenProps({ children }: { children: preact.ComponentChildren }) {
9
- return <>{children}</>
9
+ return (
10
+ <div className="dory-props">
11
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.7fr)] gap-6 bg-muted px-5 py-2.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
12
+ <span>Prop</span>
13
+ <span>Description</span>
14
+ </div>
15
+ {children}
16
+ </div>
17
+ )
18
+ }
19
+
20
+ /** One row of a Specimen's Props tab: mono name with its type underneath, and a description. */
21
+ export function Prop({ name, type, children }: { name: string; type?: string; children: preact.ComponentChildren }) {
22
+ return (
23
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.7fr)] gap-6 border-t border-[var(--border)] px-5 py-3.5">
24
+ <div className="min-w-0">
25
+ <p className="font-mono text-[13px] font-semibold text-foreground">{name}</p>
26
+ {type && <p className="mt-0.5 font-mono text-xs text-[var(--primary)]">{type}</p>}
27
+ </div>
28
+ <div className="text-sm leading-relaxed text-muted-foreground [&_p]:mb-0">{children}</div>
29
+ </div>
30
+ )
10
31
  }
11
32
 
12
33
  type SpecimenTab = 'Preview' | 'Code' | 'Props'
13
34
 
14
- function isMdxSource(child: preact.ComponentChild): boolean {
15
- return isValidElement(child) && (child as preact.VNode).type === Pre && fencePropsOf(child as preact.JSX.Element).language === 'mdx'
35
+ function isSourceFence(child: preact.ComponentChild): boolean {
36
+ return isValidElement(child) && (child as preact.VNode).type === Pre && ['mdx', 'tsx', 'jsx'].includes(fencePropsOf(child as preact.JSX.Element).language)
16
37
  }
17
38
 
18
39
  function isPropsBlock(child: preact.ComponentChild): boolean {
@@ -21,49 +42,42 @@ function isPropsBlock(child: preact.ComponentChild): boolean {
21
42
 
22
43
  /**
23
44
  * Framed component example with Preview / Code / Props tabs.
24
- * An ```mdx fence becomes the Code tab, a <SpecimenProps> block becomes the
45
+ * An MDX, TSX, or JSX fence becomes the Code tab, a <SpecimenProps> block becomes the
25
46
  * Props tab, and everything else is the rendered preview.
26
47
  */
27
- export function Specimen({ label, children }: { label?: string; children: preact.ComponentChildren }) {
48
+ export function Specimen({ label, children, padding = 'default' }: {
49
+ label?: string
50
+ children: preact.ComponentChildren
51
+ /** Use none only when the example supplies its own preview spacing. */
52
+ padding?: 'default' | 'none'
53
+ }) {
54
+ const previewClassName = cn('m-0 min-w-0 [&>:first-child]:mt-0 [&>:last-child]:mb-0', padding === 'none' ? 'p-0' : 'p-5 sm:p-6')
28
55
  const items = Children.toArray(children)
29
- const code = items.filter(isMdxSource)
56
+ const code = items.filter(isSourceFence)
30
57
  const props = items.filter(isPropsBlock)
31
- const preview = items.filter((child) => !isMdxSource(child) && !isPropsBlock(child))
58
+ const preview = items.filter((child) => !isSourceFence(child) && !isPropsBlock(child))
32
59
 
33
60
  const tabs: SpecimenTab[] = ['Preview']
34
61
  if (code.length > 0) tabs.push('Code')
35
62
  if (props.length > 0) tabs.push('Props')
36
63
 
37
- const [active, setActive] = useState<SpecimenTab>('Preview')
38
- const panels: Record<SpecimenTab, preact.ComponentChildren[]> = { Preview: preview, Code: code, Props: props }
39
-
40
- return (
41
- <div className="dory-specimen not-prose my-5 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
42
- <div className="flex min-h-10 items-center justify-between gap-3 border-b border-[var(--border)] py-1.5 pl-4 pr-1.5">
43
- <p className="truncate font-mono text-xs text-[var(--primary)]">{label}</p>
44
- {tabs.length > 1 && (
45
- <div role="tablist" className="flex shrink-0 rounded-md bg-muted p-0.5">
46
- {tabs.map((tab) => (
47
- <button
48
- key={tab}
49
- type="button"
50
- role="tab"
51
- aria-selected={active === tab}
52
- onClick={() => setActive(tab)}
53
- className={classNames('rounded-[5px] px-2.5 py-1 text-xs font-medium transition', {
54
- 'bg-card text-foreground shadow-[0_1px_2px_rgb(14_34_64/0.08)] dark:bg-[var(--border)]': active === tab,
55
- 'text-muted-foreground hover:text-foreground': active !== tab,
56
- })}
57
- >
58
- {tab}
59
- </button>
60
- ))}
61
- </div>
62
- )}
63
- </div>
64
- <div role="tabpanel" className="p-5 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
65
- {panels[active]}
66
- </div>
64
+ const renderCode = () => code.map((child, index) => {
65
+ const fence = fencePropsOf(child as preact.JSX.Element)
66
+ return <div key={index} className="relative max-h-[32rem] overflow-auto px-5 py-4 font-mono text-[12.5px] leading-[1.7]">
67
+ <Fence language={fence.language} meta={{ ...fence.meta, title: undefined }}>{fence.code}</Fence>
68
+ <CopyButton code={fence.code} />
67
69
  </div>
68
- )
70
+ })
71
+ if (tabs.length === 1) return <div className="dory-specimen not-prose my-5 overflow-hidden rounded-lg border border-[var(--border)] bg-card"><div className="px-4 py-3 font-mono text-xs text-[var(--primary)]">{label}</div><div data-dory-preview data-padding={padding} className={previewClassName}>{preview}</div></div>
72
+ return <UxTabs defaultValue="Preview" className="dory-specimen not-prose my-5 gap-0 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
73
+ <div className="flex min-h-12 flex-wrap items-center justify-between gap-3 border-b border-[var(--border)] py-2 pl-4 pr-2">
74
+ <p className="truncate font-mono text-xs text-[var(--primary)]">{label}</p>
75
+ <TabsList aria-label={`${label ?? 'Component'} example views`} onKeyDown={handleTabKeyDown}>
76
+ {tabs.map(tab => <TabsTrigger key={tab} value={tab}>{tab}</TabsTrigger>)}
77
+ </TabsList>
78
+ </div>
79
+ <TabsContent value="Preview" data-dory-preview data-padding={padding} className={previewClassName}>{preview}</TabsContent>
80
+ {code.length > 0 && <TabsContent value="Code" className="m-0 p-0">{renderCode()}</TabsContent>}
81
+ {props.length > 0 && <TabsContent value="Props" className="m-0 p-0">{props}</TabsContent>}
82
+ </UxTabs>
69
83
  }
package/src/mdx/tabs.tsx CHANGED
@@ -1,39 +1,50 @@
1
- import classNames from 'classnames'
1
+ import { Tabs as UxTabs, TabsContent, TabsList, TabsTrigger } from '@clidey/ux'
2
2
  import { Children, isValidElement } from 'preact/compat'
3
- import { useState } from 'preact/hooks'
3
+
4
+ /** Keyboard fallback for UX tabs rendered through Preact compatibility. */
5
+ export function handleTabKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
6
+ // Radix roving focus does not handle arrow keys through Preact compat.
7
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
8
+ const tabs = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="tab"]:not([disabled])'))
9
+ const current = tabs.indexOf(event.target as HTMLButtonElement)
10
+ if (current < 0 || tabs.length === 0) return
11
+ event.preventDefault()
12
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1
13
+ : (current + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length
14
+ tabs[next].focus()
15
+ tabs[next].click()
16
+ }
4
17
 
5
18
  /** One tab panel inside <Tabs>. Renders its children when active. */
6
19
  export function Tab({ children }: { title: string; children: preact.ComponentChildren }) {
7
20
  return <>{children}</>
8
21
  }
9
22
 
10
- /** Underlined tab strip for any MDX content, e.g. several diagrams of one thing. */
23
+ /** UX tab strip for any MDX content, e.g. several diagrams of one thing. */
11
24
  export function Tabs({ children }: { children: preact.ComponentChildren }) {
12
25
  const panels = Children.toArray(children).filter(isValidElement) as preact.VNode<{ title: string }>[]
13
- const [active, setActive] = useState(0)
14
26
 
15
27
  return (
16
- <div className="dory-tabs not-prose my-5">
17
- <div role="tablist" className="flex gap-5 border-b border-[var(--border)]">
28
+ <UxTabs defaultValue="0" className="dory-tabs not-prose my-5">
29
+ <TabsList
30
+ className="max-w-full justify-start overflow-x-auto"
31
+ onKeyDown={handleTabKeyDown}
32
+ >
18
33
  {panels.map((panel, index) => (
19
- <button
34
+ <TabsTrigger
20
35
  key={index}
21
- type="button"
22
- role="tab"
23
- aria-selected={active === index}
24
- onClick={() => setActive(index)}
25
- className={classNames('-mb-px border-b-2 pb-2.5 pt-1 text-sm font-medium transition', {
26
- 'border-[var(--primary)] text-foreground': active === index,
27
- 'border-transparent text-muted-foreground hover:text-foreground': active !== index,
28
- })}
36
+ value={String(index)}
37
+ className="whitespace-nowrap"
29
38
  >
30
39
  {panel.props.title}
31
- </button>
40
+ </TabsTrigger>
32
41
  ))}
33
- </div>
34
- <div role="tabpanel" className="pt-4 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
35
- {panels[active]}
36
- </div>
37
- </div>
42
+ </TabsList>
43
+ {panels.map((panel, index) => (
44
+ <TabsContent key={index} value={String(index)} className="pt-4 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
45
+ {panel}
46
+ </TabsContent>
47
+ ))}
48
+ </UxTabs>
38
49
  )
39
50
  }
package/src/mdx/text.tsx CHANGED
@@ -4,42 +4,42 @@ import classNames from "classnames"
4
4
  import { useState } from "preact/hooks"
5
5
 
6
6
 
7
- export const h1 = function H1(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
7
+ export const h1 = function H1(props: preact.ComponentProps<'h1'>) {
8
8
  return <h1 className="dory-h1 font-heading mb-3 mt-10" {...props} />
9
9
  }
10
10
 
11
- export const h2 = function H2({ children, ...props }: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
11
+ export const h2 = function H2({ children, ...props }: preact.ComponentProps<'h2'>) {
12
12
  return (
13
- <h2 className="dory-h2 font-heading mb-2 mt-10" {...props}>
13
+ <h2 className="dory-h2 font-heading mb-4 mt-16" {...props}>
14
14
  <span>{children}</span>
15
15
  {props.id && <a href={`#${props.id}`} className="dory-anchor" aria-label="Link to this section">#</a>}
16
16
  </h2>
17
17
  )
18
18
  }
19
19
 
20
- export const h3 = function H3(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
21
- return <h3 className="dory-h3 font-heading mb-2 mt-7" {...props} />
20
+ export const h3 = function H3(props: preact.ComponentProps<'h3'>) {
21
+ return <h3 className="dory-h3 font-heading mb-3 mt-10" {...props} />
22
22
  }
23
23
 
24
- export const h4 = function H4(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
24
+ export const h4 = function H4(props: preact.ComponentProps<'h4'>) {
25
25
  return <h4 className="dory-h4 font-heading mb-1.5 mt-5" {...props} />
26
26
  }
27
27
 
28
- export const h5 = function H5(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
28
+ export const h5 = function H5(props: preact.ComponentProps<'h5'>) {
29
29
  return <h5 className="text-lg font-bold mb-2" {...props} />
30
30
  }
31
31
 
32
- export const h6 = function H6(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
32
+ export const h6 = function H6(props: preact.ComponentProps<'h6'>) {
33
33
  return <h6 className="text-base font-bold mb-2" {...props} />
34
34
  }
35
35
 
36
- export const p = function P(props: preact.JSX.HTMLAttributes<HTMLParagraphElement>) {
36
+ export const p = function P(props: preact.ComponentProps<'p'>) {
37
37
  return <p className="mb-2 inline-block" {...props} />
38
38
  }
39
39
 
40
40
  export function Row({ children, cols = 1, className }: { children: preact.ComponentChildren, cols?: number, className?: string }) {
41
41
  return (
42
- <div className={cn(`py-6 grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none`, className)} style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
42
+ <div className={cn(`dory-row py-6 grid min-w-0 grid-cols-1 items-start gap-6 sm:grid-cols-[repeat(var(--dory-columns),minmax(0,1fr))] xl:max-w-none`, className)} style={{ '--dory-columns': cols }}>
43
43
  {children}
44
44
  </div>
45
45
  )
@@ -58,7 +58,7 @@ export function Col({
58
58
  }) {
59
59
  return (
60
60
  <div
61
- className={classNames("[&>:first-child]:mt-0 [&>:last-child]:mb-0", {
61
+ className={classNames("min-w-0 [&>:first-child]:mt-0 [&>:last-child]:mb-0", {
62
62
  'xl:sticky xl:top-24': sticky,
63
63
  })}
64
64
  style={{