@hypoth-ui/docs-renderer-next 0.1.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.
@@ -0,0 +1,477 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Live Example Component
5
+ *
6
+ * Interactive code example wrapper for documentation.
7
+ * Features:
8
+ * - Live preview of component
9
+ * - Syntax-highlighted source code
10
+ * - Copy to clipboard
11
+ * - Toggle between preview and code views
12
+ * - Variant selector for multiple examples
13
+ */
14
+
15
+ import { type ReactNode, useState } from "react";
16
+
17
+ export interface LiveExampleProps {
18
+ /** The live preview content */
19
+ children: ReactNode;
20
+ /** Source code to display */
21
+ code: string;
22
+ /** Language for syntax highlighting */
23
+ language?: string;
24
+ /** Title for the example */
25
+ title?: string;
26
+ /** Description of what the example demonstrates */
27
+ description?: string;
28
+ /** Whether to show code by default */
29
+ defaultShowCode?: boolean;
30
+ /** Available variants/tabs */
31
+ variants?: Array<{
32
+ name: string;
33
+ code: string;
34
+ preview: ReactNode;
35
+ }>;
36
+ /** Custom class name */
37
+ className?: string;
38
+ }
39
+
40
+ export function LiveExample({
41
+ children,
42
+ code,
43
+ language = "tsx",
44
+ title,
45
+ description,
46
+ defaultShowCode = false,
47
+ variants,
48
+ className = "",
49
+ }: LiveExampleProps) {
50
+ const [showCode, setShowCode] = useState(defaultShowCode);
51
+ const [copied, setCopied] = useState(false);
52
+ const [activeVariant, setActiveVariant] = useState(0);
53
+
54
+ const currentCode = variants ? variants[activeVariant]?.code || code : code;
55
+ const currentPreview = variants ? variants[activeVariant]?.preview || children : children;
56
+
57
+ const handleCopy = async () => {
58
+ try {
59
+ await navigator.clipboard.writeText(currentCode);
60
+ setCopied(true);
61
+ setTimeout(() => setCopied(false), 2000);
62
+ } catch (error) {
63
+ console.error("Failed to copy:", error);
64
+ }
65
+ };
66
+
67
+ return (
68
+ <div className={`live-example ${className}`}>
69
+ {title && (
70
+ <div className="live-example__header">
71
+ <h3 className="live-example__title">{title}</h3>
72
+ {description && <p className="live-example__description">{description}</p>}
73
+ </div>
74
+ )}
75
+
76
+ {/* Variant tabs */}
77
+ {variants && variants.length > 1 && (
78
+ <div className="live-example__variants" role="tablist">
79
+ {variants.map((variant, index) => (
80
+ <button
81
+ key={variant.name}
82
+ type="button"
83
+ className={`live-example__variant-tab ${activeVariant === index ? "live-example__variant-tab--active" : ""}`}
84
+ onClick={() => setActiveVariant(index)}
85
+ role="tab"
86
+ aria-selected={activeVariant === index}
87
+ >
88
+ {variant.name}
89
+ </button>
90
+ ))}
91
+ </div>
92
+ )}
93
+
94
+ {/* Preview area */}
95
+ <div className="live-example__preview">
96
+ <div className="live-example__preview-content">{currentPreview}</div>
97
+ </div>
98
+
99
+ {/* Controls */}
100
+ <div className="live-example__controls">
101
+ <button
102
+ type="button"
103
+ className={`live-example__toggle ${showCode ? "live-example__toggle--active" : ""}`}
104
+ onClick={() => setShowCode(!showCode)}
105
+ aria-expanded={showCode}
106
+ aria-controls="live-example-code"
107
+ >
108
+ <svg
109
+ width="16"
110
+ height="16"
111
+ viewBox="0 0 16 16"
112
+ fill="none"
113
+ stroke="currentColor"
114
+ strokeWidth="1.5"
115
+ aria-hidden="true"
116
+ >
117
+ <path d="M10.5 4.5L14 8l-3.5 3.5M5.5 4.5L2 8l3.5 3.5" />
118
+ </svg>
119
+ <span>{showCode ? "Hide code" : "Show code"}</span>
120
+ </button>
121
+
122
+ {showCode && (
123
+ <button type="button" className="live-example__copy" onClick={handleCopy} aria-label="Copy code">
124
+ {copied ? (
125
+ <>
126
+ <svg
127
+ width="16"
128
+ height="16"
129
+ viewBox="0 0 16 16"
130
+ fill="none"
131
+ stroke="currentColor"
132
+ strokeWidth="2"
133
+ aria-hidden="true"
134
+ >
135
+ <path d="M3.5 8l3 3 6-6" />
136
+ </svg>
137
+ <span>Copied!</span>
138
+ </>
139
+ ) : (
140
+ <>
141
+ <svg
142
+ width="16"
143
+ height="16"
144
+ viewBox="0 0 16 16"
145
+ fill="none"
146
+ stroke="currentColor"
147
+ strokeWidth="1.5"
148
+ aria-hidden="true"
149
+ >
150
+ <rect x="5" y="5" width="9" height="9" rx="1" />
151
+ <path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2" />
152
+ </svg>
153
+ <span>Copy</span>
154
+ </>
155
+ )}
156
+ </button>
157
+ )}
158
+ </div>
159
+
160
+ {/* Code block */}
161
+ {showCode && (
162
+ <div id="live-example-code" className="live-example__code">
163
+ <pre className={`language-${language}`}>
164
+ <code>{currentCode}</code>
165
+ </pre>
166
+ </div>
167
+ )}
168
+
169
+ <style jsx>{`
170
+ .live-example {
171
+ border: 1px solid var(--ds-color-border-default, #e5e5e5);
172
+ border-radius: 8px;
173
+ overflow: hidden;
174
+ margin: 1.5rem 0;
175
+ }
176
+
177
+ .live-example__header {
178
+ padding: 1rem;
179
+ border-bottom: 1px solid var(--ds-color-border-default, #e5e5e5);
180
+ background: var(--ds-color-background-subtle, #f9f9f9);
181
+ }
182
+
183
+ .live-example__title {
184
+ font-size: 0.875rem;
185
+ font-weight: 600;
186
+ margin: 0;
187
+ color: var(--ds-color-foreground-default, #1a1a1a);
188
+ }
189
+
190
+ .live-example__description {
191
+ font-size: 0.75rem;
192
+ margin: 0.25rem 0 0;
193
+ color: var(--ds-color-foreground-muted, #666);
194
+ }
195
+
196
+ .live-example__variants {
197
+ display: flex;
198
+ gap: 0;
199
+ border-bottom: 1px solid var(--ds-color-border-default, #e5e5e5);
200
+ background: var(--ds-color-background-subtle, #f9f9f9);
201
+ }
202
+
203
+ .live-example__variant-tab {
204
+ padding: 0.5rem 1rem;
205
+ font-size: 0.75rem;
206
+ font-weight: 500;
207
+ color: var(--ds-color-foreground-muted, #666);
208
+ background: transparent;
209
+ border: none;
210
+ border-bottom: 2px solid transparent;
211
+ cursor: pointer;
212
+ transition: color 0.15s, border-color 0.15s;
213
+ }
214
+
215
+ .live-example__variant-tab:hover {
216
+ color: var(--ds-color-foreground-default, #1a1a1a);
217
+ }
218
+
219
+ .live-example__variant-tab--active {
220
+ color: var(--ds-brand-primary, #0066cc);
221
+ border-bottom-color: var(--ds-brand-primary, #0066cc);
222
+ }
223
+
224
+ .live-example__preview {
225
+ padding: 1.5rem;
226
+ background: var(--ds-color-background-surface, #fff);
227
+ min-height: 80px;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ }
232
+
233
+ .live-example__preview-content {
234
+ width: 100%;
235
+ }
236
+
237
+ .live-example__controls {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.5rem;
241
+ padding: 0.5rem 1rem;
242
+ border-top: 1px solid var(--ds-color-border-default, #e5e5e5);
243
+ background: var(--ds-color-background-subtle, #f9f9f9);
244
+ }
245
+
246
+ .live-example__toggle,
247
+ .live-example__copy {
248
+ display: inline-flex;
249
+ align-items: center;
250
+ gap: 0.375rem;
251
+ padding: 0.375rem 0.75rem;
252
+ font-size: 0.75rem;
253
+ font-weight: 500;
254
+ color: var(--ds-color-foreground-muted, #666);
255
+ background: var(--ds-color-background-surface, #fff);
256
+ border: 1px solid var(--ds-color-border-default, #e5e5e5);
257
+ border-radius: 4px;
258
+ cursor: pointer;
259
+ transition: color 0.15s, border-color 0.15s;
260
+ }
261
+
262
+ .live-example__toggle:hover,
263
+ .live-example__copy:hover {
264
+ color: var(--ds-color-foreground-default, #1a1a1a);
265
+ border-color: var(--ds-color-border-strong, #ccc);
266
+ }
267
+
268
+ .live-example__toggle--active {
269
+ color: var(--ds-brand-primary, #0066cc);
270
+ border-color: var(--ds-brand-primary, #0066cc);
271
+ background: rgba(0, 102, 204, 0.05);
272
+ }
273
+
274
+ .live-example__copy {
275
+ margin-left: auto;
276
+ }
277
+
278
+ .live-example__code {
279
+ border-top: 1px solid var(--ds-color-border-default, #e5e5e5);
280
+ background: var(--ds-color-background-code, #1e1e1e);
281
+ overflow-x: auto;
282
+ }
283
+
284
+ .live-example__code pre {
285
+ margin: 0;
286
+ padding: 1rem;
287
+ font-size: 0.8125rem;
288
+ line-height: 1.6;
289
+ font-family: "SF Mono", Menlo, Monaco, "Courier New", monospace;
290
+ }
291
+
292
+ .live-example__code code {
293
+ color: var(--ds-color-code-text, #d4d4d4);
294
+ white-space: pre;
295
+ }
296
+
297
+ /* Dark mode adjustments */
298
+ :global([data-theme="dark"]) .live-example__preview {
299
+ background: var(--ds-color-background-surface-dark, #1a1a1a);
300
+ }
301
+ `}</style>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ /**
307
+ * Simple code block without live preview
308
+ */
309
+ export interface CodeBlockProps {
310
+ /** Source code to display */
311
+ code: string;
312
+ /** Language for syntax highlighting */
313
+ language?: string;
314
+ /** Filename to display */
315
+ filename?: string;
316
+ /** Whether to show line numbers */
317
+ showLineNumbers?: boolean;
318
+ /** Lines to highlight */
319
+ highlightLines?: number[];
320
+ /** Custom class name */
321
+ className?: string;
322
+ }
323
+
324
+ export function CodeBlock({
325
+ code,
326
+ language = "tsx",
327
+ filename,
328
+ showLineNumbers = false,
329
+ highlightLines = [],
330
+ className = "",
331
+ }: CodeBlockProps) {
332
+ const [copied, setCopied] = useState(false);
333
+
334
+ const handleCopy = async () => {
335
+ try {
336
+ await navigator.clipboard.writeText(code);
337
+ setCopied(true);
338
+ setTimeout(() => setCopied(false), 2000);
339
+ } catch (error) {
340
+ console.error("Failed to copy:", error);
341
+ }
342
+ };
343
+
344
+ const lines = code.split("\n");
345
+
346
+ return (
347
+ <div className={`code-block ${className}`}>
348
+ {filename && (
349
+ <div className="code-block__header">
350
+ <span className="code-block__filename">{filename}</span>
351
+ <button type="button" className="code-block__copy" onClick={handleCopy} aria-label="Copy code">
352
+ {copied ? (
353
+ <svg
354
+ width="14"
355
+ height="14"
356
+ viewBox="0 0 16 16"
357
+ fill="none"
358
+ stroke="currentColor"
359
+ strokeWidth="2"
360
+ aria-hidden="true"
361
+ >
362
+ <path d="M3.5 8l3 3 6-6" />
363
+ </svg>
364
+ ) : (
365
+ <svg
366
+ width="14"
367
+ height="14"
368
+ viewBox="0 0 16 16"
369
+ fill="none"
370
+ stroke="currentColor"
371
+ strokeWidth="1.5"
372
+ aria-hidden="true"
373
+ >
374
+ <rect x="5" y="5" width="9" height="9" rx="1" />
375
+ <path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2" />
376
+ </svg>
377
+ )}
378
+ </button>
379
+ </div>
380
+ )}
381
+ <pre className={`language-${language}`}>
382
+ <code>
383
+ {showLineNumbers
384
+ ? lines.map((line, i) => (
385
+ <span
386
+ key={i}
387
+ className={`code-block__line ${highlightLines.includes(i + 1) ? "code-block__line--highlighted" : ""}`}
388
+ >
389
+ <span className="code-block__line-number">{i + 1}</span>
390
+ <span className="code-block__line-content">{line}</span>
391
+ {"\n"}
392
+ </span>
393
+ ))
394
+ : code}
395
+ </code>
396
+ </pre>
397
+
398
+ <style jsx>{`
399
+ .code-block {
400
+ border-radius: 8px;
401
+ overflow: hidden;
402
+ margin: 1rem 0;
403
+ background: var(--ds-color-background-code, #1e1e1e);
404
+ }
405
+
406
+ .code-block__header {
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
+ padding: 0.5rem 1rem;
411
+ background: rgba(255, 255, 255, 0.05);
412
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
413
+ }
414
+
415
+ .code-block__filename {
416
+ font-size: 0.75rem;
417
+ font-family: "SF Mono", Menlo, Monaco, "Courier New", monospace;
418
+ color: var(--ds-color-code-text, #d4d4d4);
419
+ opacity: 0.7;
420
+ }
421
+
422
+ .code-block__copy {
423
+ padding: 0.25rem;
424
+ background: transparent;
425
+ border: none;
426
+ color: var(--ds-color-code-text, #d4d4d4);
427
+ opacity: 0.5;
428
+ cursor: pointer;
429
+ transition: opacity 0.15s;
430
+ }
431
+
432
+ .code-block__copy:hover {
433
+ opacity: 1;
434
+ }
435
+
436
+ .code-block pre {
437
+ margin: 0;
438
+ padding: 1rem;
439
+ font-size: 0.8125rem;
440
+ line-height: 1.6;
441
+ font-family: "SF Mono", Menlo, Monaco, "Courier New", monospace;
442
+ overflow-x: auto;
443
+ }
444
+
445
+ .code-block code {
446
+ color: var(--ds-color-code-text, #d4d4d4);
447
+ }
448
+
449
+ .code-block__line {
450
+ display: flex;
451
+ }
452
+
453
+ .code-block__line--highlighted {
454
+ background: rgba(255, 255, 255, 0.1);
455
+ margin: 0 -1rem;
456
+ padding: 0 1rem;
457
+ }
458
+
459
+ .code-block__line-number {
460
+ display: inline-block;
461
+ width: 2.5rem;
462
+ flex-shrink: 0;
463
+ text-align: right;
464
+ padding-right: 1rem;
465
+ color: var(--ds-color-code-text, #d4d4d4);
466
+ opacity: 0.3;
467
+ user-select: none;
468
+ }
469
+
470
+ .code-block__line-content {
471
+ flex: 1;
472
+ white-space: pre;
473
+ }
474
+ `}</style>
475
+ </div>
476
+ );
477
+ }
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import type { Edition as EditionType } from "@hypoth-ui/docs-core";
4
+ import { type ReactNode, createContext, useContext } from "react";
5
+
6
+ /**
7
+ * Context for the current edition
8
+ */
9
+ const EditionContext = createContext<EditionType>("enterprise");
10
+
11
+ /**
12
+ * Provider for the current edition
13
+ */
14
+ export function EditionProvider({
15
+ edition,
16
+ children,
17
+ }: {
18
+ edition: EditionType;
19
+ children: ReactNode;
20
+ }) {
21
+ return <EditionContext.Provider value={edition}>{children}</EditionContext.Provider>;
22
+ }
23
+
24
+ /**
25
+ * Hook to get the current edition
26
+ */
27
+ export function useEdition(): EditionType {
28
+ return useContext(EditionContext);
29
+ }
30
+
31
+ /**
32
+ * Edition hierarchy for checking availability
33
+ */
34
+ const EDITION_INCLUDES: Record<EditionType, EditionType[]> = {
35
+ core: [],
36
+ pro: ["core"],
37
+ enterprise: ["core", "pro"],
38
+ };
39
+
40
+ /**
41
+ * Check if content for a specific edition should be visible
42
+ */
43
+ function isEditionVisible(contentEdition: EditionType, currentEdition: EditionType): boolean {
44
+ // Content is visible if:
45
+ // 1. The current edition matches the content edition
46
+ // 2. The current edition includes the content edition in its hierarchy
47
+ if (contentEdition === currentEdition) return true;
48
+ return EDITION_INCLUDES[currentEdition].includes(contentEdition);
49
+ }
50
+
51
+ interface EditionProps {
52
+ /**
53
+ * The edition(s) this content is for.
54
+ * Can be a single edition or an array of editions.
55
+ */
56
+ for: EditionType | EditionType[];
57
+ /**
58
+ * The content to render if the edition matches
59
+ */
60
+ children: ReactNode;
61
+ /**
62
+ * Optional fallback content for other editions
63
+ */
64
+ fallback?: ReactNode;
65
+ }
66
+
67
+ /**
68
+ * Edition component for conditional content in MDX
69
+ *
70
+ * @example
71
+ * ```mdx
72
+ * <Edition for="enterprise">
73
+ * This content is only visible to enterprise users.
74
+ * </Edition>
75
+ *
76
+ * <Edition for={["pro", "enterprise"]} fallback={<p>Upgrade to access this feature.</p>}>
77
+ * This content is visible to pro and enterprise users.
78
+ * </Edition>
79
+ * ```
80
+ */
81
+ export function Edition({ for: targetEdition, children, fallback }: EditionProps) {
82
+ const currentEdition = useEdition();
83
+
84
+ // Normalize to array
85
+ const editions = Array.isArray(targetEdition) ? targetEdition : [targetEdition];
86
+
87
+ // Check if current edition can see this content
88
+ const isVisible = editions.some((e) => isEditionVisible(e, currentEdition));
89
+
90
+ if (isVisible) {
91
+ return <>{children}</>;
92
+ }
93
+
94
+ // Show fallback if provided
95
+ if (fallback) {
96
+ return <>{fallback}</>;
97
+ }
98
+
99
+ // Hide content
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Badge component to show which editions content is available for
105
+ */
106
+ interface EditionBadgeProps {
107
+ edition: EditionType | EditionType[];
108
+ className?: string;
109
+ }
110
+
111
+ export function EditionBadge({ edition, className = "" }: EditionBadgeProps) {
112
+ const editions = Array.isArray(edition) ? edition : [edition];
113
+
114
+ return (
115
+ <span className={`edition-badge-group ${className}`}>
116
+ {editions.map((e) => (
117
+ <span key={e} className={`edition-badge edition-badge--${e}`}>
118
+ {e}
119
+ </span>
120
+ ))}
121
+ <style jsx>{`
122
+ .edition-badge-group {
123
+ display: inline-flex;
124
+ gap: 0.25rem;
125
+ }
126
+ .edition-badge {
127
+ display: inline-block;
128
+ padding: 0.125rem 0.5rem;
129
+ font-size: 0.75rem;
130
+ font-weight: 600;
131
+ border-radius: 9999px;
132
+ text-transform: capitalize;
133
+ }
134
+ .edition-badge--core {
135
+ background-color: #e0f2fe;
136
+ color: #0369a1;
137
+ }
138
+ .edition-badge--pro {
139
+ background-color: #f0fdf4;
140
+ color: #15803d;
141
+ }
142
+ .edition-badge--enterprise {
143
+ background-color: #faf5ff;
144
+ color: #7e22ce;
145
+ }
146
+ `}</style>
147
+ </span>
148
+ );
149
+ }
@@ -0,0 +1,90 @@
1
+ import { compile, run } from "@mdx-js/mdx";
2
+ import type { ComponentType, ReactNode } from "react";
3
+ import * as jsxDevRuntime from "react/jsx-dev-runtime";
4
+ import * as jsxRuntime from "react/jsx-runtime";
5
+ import { Edition, EditionBadge } from "./mdx/edition";
6
+
7
+ // biome-ignore lint/suspicious/noExplicitAny: MDX components have varying prop signatures
8
+ type MdxComponent = ComponentType<any>;
9
+
10
+ interface MdxRendererProps {
11
+ /** MDX source string */
12
+ source: string;
13
+ /** Custom components to use in MDX */
14
+ components?: Record<string, MdxComponent>;
15
+ }
16
+
17
+ // Default MDX components
18
+ const defaultComponents: Record<string, MdxComponent> = {
19
+ // Edition-specific content components
20
+ Edition,
21
+ EditionBadge,
22
+ // Code blocks with syntax highlighting placeholder
23
+ pre: ({ children, ...props }: { children: ReactNode }) => (
24
+ <pre className="code-block" {...props}>
25
+ {children}
26
+ </pre>
27
+ ),
28
+ code: ({ children, className, ...props }: { children: ReactNode; className?: string }) => {
29
+ const isInline = !className;
30
+ return (
31
+ <code className={isInline ? "code-inline" : className} {...props}>
32
+ {children}
33
+ </code>
34
+ );
35
+ },
36
+ // Table styling
37
+ table: ({ children, ...props }: { children: ReactNode }) => (
38
+ <div className="table-wrapper">
39
+ <table {...props}>{children}</table>
40
+ </div>
41
+ ),
42
+ // Heading anchors
43
+ h1: ({ children, ...props }: { children: ReactNode }) => <h1 {...props}>{children}</h1>,
44
+ h2: ({ children, ...props }: { children: ReactNode }) => {
45
+ const id =
46
+ typeof children === "string" ? children.toLowerCase().replace(/\s+/g, "-") : undefined;
47
+ return (
48
+ <h2 id={id} {...props}>
49
+ {children}
50
+ </h2>
51
+ );
52
+ },
53
+ h3: ({ children, ...props }: { children: ReactNode }) => {
54
+ const id =
55
+ typeof children === "string" ? children.toLowerCase().replace(/\s+/g, "-") : undefined;
56
+ return (
57
+ <h3 id={id} {...props}>
58
+ {children}
59
+ </h3>
60
+ );
61
+ },
62
+ };
63
+
64
+ export async function MdxRenderer({ source, components = {} }: MdxRendererProps) {
65
+ const isDev = process.env.NODE_ENV === "development";
66
+
67
+ // Compile MDX to JavaScript
68
+ const code = await compile(source, {
69
+ outputFormat: "function-body",
70
+ development: isDev,
71
+ });
72
+
73
+ // Run the compiled code with the appropriate runtime
74
+ const { default: MdxContent } = await run(code, {
75
+ ...(isDev ? jsxDevRuntime : jsxRuntime),
76
+ baseUrl: import.meta.url,
77
+ });
78
+
79
+ // Merge default and custom components
80
+ const mergedComponents = {
81
+ ...defaultComponents,
82
+ ...components,
83
+ };
84
+
85
+ return (
86
+ <div className="mdx-content">
87
+ <MdxContent components={mergedComponents} />
88
+ </div>
89
+ );
90
+ }