@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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/app/accessibility/CategoryFilter.tsx +123 -0
- package/app/accessibility/ConformanceTable.tsx +109 -0
- package/app/accessibility/StatusBadge.tsx +47 -0
- package/app/accessibility/[component]/page.tsx +166 -0
- package/app/accessibility/page.tsx +207 -0
- package/app/api/search/route.ts +241 -0
- package/app/components/[id]/page.tsx +316 -0
- package/app/edition-upgrade/page.tsx +76 -0
- package/app/guides/[id]/page.tsx +67 -0
- package/app/layout.tsx +93 -0
- package/app/page.tsx +29 -0
- package/components/branding/header.tsx +82 -0
- package/components/branding/logo.tsx +54 -0
- package/components/feedback/feedback-widget.tsx +263 -0
- package/components/live-example.tsx +477 -0
- package/components/mdx/edition.tsx +149 -0
- package/components/mdx-renderer.tsx +90 -0
- package/components/nav-sidebar.tsx +269 -0
- package/components/search/search-input.tsx +508 -0
- package/components/theme-init-script.tsx +35 -0
- package/components/theme-switcher.tsx +166 -0
- package/components/tokens-used.tsx +135 -0
- package/components/upgrade/upgrade-prompt.tsx +141 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +751 -0
- package/package.json +66 -0
- package/styles/globals.css +613 -0
|
@@ -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
|
+
}
|