@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,316 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
type ComponentAccessibility,
|
|
4
|
+
type ComponentStatus,
|
|
5
|
+
type ContractManifest,
|
|
6
|
+
type Edition,
|
|
7
|
+
getMinimumEdition,
|
|
8
|
+
isComponentAvailable,
|
|
9
|
+
} from "@hypoth-ui/docs-core";
|
|
10
|
+
import {
|
|
11
|
+
getEditionConfig,
|
|
12
|
+
loadManifestByIdFromPacks,
|
|
13
|
+
loadManifestsFromPacks,
|
|
14
|
+
resolveContentFile,
|
|
15
|
+
} from "../../../lib/content-resolver";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Display manifest shape used for rendering component pages
|
|
19
|
+
* Normalizes both ContractManifest and legacy ComponentManifest formats
|
|
20
|
+
*/
|
|
21
|
+
interface DisplayManifest {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
status: ComponentStatus;
|
|
25
|
+
props: Array<{ name: string; type: string; default?: string; description: string }>;
|
|
26
|
+
events: Array<{ name: string; description: string }>;
|
|
27
|
+
examples: Array<{ title: string; code: string }>;
|
|
28
|
+
tokensUsed: string[];
|
|
29
|
+
accessibility?: ComponentAccessibility;
|
|
30
|
+
}
|
|
31
|
+
import { notFound, redirect } from "next/navigation";
|
|
32
|
+
import { MdxRenderer } from "../../../components/mdx-renderer";
|
|
33
|
+
import { EditionProvider } from "../../../components/mdx/edition";
|
|
34
|
+
import { TokensUsed } from "../../../components/tokens-used";
|
|
35
|
+
|
|
36
|
+
interface ComponentPageProps {
|
|
37
|
+
params: Promise<{
|
|
38
|
+
id: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the current edition from environment or config
|
|
44
|
+
*/
|
|
45
|
+
async function getCurrentEdition(): Promise<Edition> {
|
|
46
|
+
// Check environment variable first
|
|
47
|
+
const envEdition = process.env.DS_EDITION;
|
|
48
|
+
if (envEdition && ["core", "pro", "enterprise"].includes(envEdition)) {
|
|
49
|
+
return envEdition as Edition;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fall back to edition config from content resolver
|
|
53
|
+
try {
|
|
54
|
+
const config = await getEditionConfig();
|
|
55
|
+
return config.edition;
|
|
56
|
+
} catch {
|
|
57
|
+
return "enterprise"; // Default to enterprise (shows all)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load contract manifests from content packs with overlay resolution
|
|
63
|
+
*/
|
|
64
|
+
async function loadContractManifests(): Promise<ContractManifest[]> {
|
|
65
|
+
return loadManifestsFromPacks();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate static params for all components available in the current edition
|
|
70
|
+
*/
|
|
71
|
+
export async function generateStaticParams() {
|
|
72
|
+
const edition = await getCurrentEdition();
|
|
73
|
+
const manifests = await loadContractManifests();
|
|
74
|
+
|
|
75
|
+
// Filter by edition
|
|
76
|
+
const availableComponents = manifests.filter((manifest) =>
|
|
77
|
+
isComponentAvailable(manifest.editions, edition)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return availableComponents.map((manifest) => ({
|
|
81
|
+
id: manifest.id,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function generateMetadata({ params }: ComponentPageProps) {
|
|
86
|
+
const { id } = await params;
|
|
87
|
+
|
|
88
|
+
// Load manifest with overlay resolution
|
|
89
|
+
const manifest = await loadManifestByIdFromPacks(id);
|
|
90
|
+
|
|
91
|
+
if (!manifest) {
|
|
92
|
+
return { title: "Component Not Found" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
title: manifest.name,
|
|
97
|
+
description: manifest.description,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default async function ComponentPage({ params }: ComponentPageProps) {
|
|
102
|
+
const { id } = await params;
|
|
103
|
+
const edition = await getCurrentEdition();
|
|
104
|
+
|
|
105
|
+
// Try to load from contract manifests
|
|
106
|
+
const manifests = await loadContractManifests();
|
|
107
|
+
const contractManifest = manifests.find((m) => m.id === id);
|
|
108
|
+
|
|
109
|
+
// Check edition access
|
|
110
|
+
if (contractManifest) {
|
|
111
|
+
const isAvailable = isComponentAvailable(contractManifest.editions, edition);
|
|
112
|
+
|
|
113
|
+
if (!isAvailable) {
|
|
114
|
+
// Redirect to upgrade page
|
|
115
|
+
const requiredEdition = getMinimumEdition(contractManifest.editions);
|
|
116
|
+
redirect(`/edition-upgrade?component=${id}&from=${edition}&to=${requiredEdition}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use overlay-resolved manifest or fall back to direct lookup
|
|
121
|
+
const manifest = contractManifest ?? (await loadManifestByIdFromPacks(id));
|
|
122
|
+
|
|
123
|
+
if (!manifest) {
|
|
124
|
+
notFound();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Load MDX content with overlay resolution
|
|
128
|
+
let mdxContent: string | null = null;
|
|
129
|
+
try {
|
|
130
|
+
const resolved = await resolveContentFile(`components/${id}.mdx`);
|
|
131
|
+
if (resolved) {
|
|
132
|
+
mdxContent = await readFile(resolved.resolvedPath, "utf-8");
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// No MDX file, use auto-generated content from manifest
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Normalize to DisplayManifest format for rendering
|
|
139
|
+
// Contract manifests don't have props/events/examples (those come from MDX)
|
|
140
|
+
const displayManifest: DisplayManifest = {
|
|
141
|
+
name: manifest.name,
|
|
142
|
+
description: manifest.description,
|
|
143
|
+
status: manifest.status,
|
|
144
|
+
props: [],
|
|
145
|
+
events: [],
|
|
146
|
+
examples: [],
|
|
147
|
+
tokensUsed: manifest.tokensUsed ?? [],
|
|
148
|
+
accessibility: manifest.accessibility,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<EditionProvider edition={edition}>
|
|
153
|
+
<article className="component-page">
|
|
154
|
+
<header className="component-header">
|
|
155
|
+
<div className="component-status" data-status={displayManifest.status}>
|
|
156
|
+
{displayManifest.status}
|
|
157
|
+
</div>
|
|
158
|
+
<h1>{displayManifest.name}</h1>
|
|
159
|
+
{displayManifest.description && (
|
|
160
|
+
<p className="component-description">{displayManifest.description}</p>
|
|
161
|
+
)}
|
|
162
|
+
</header>
|
|
163
|
+
|
|
164
|
+
{displayManifest.tokensUsed.length > 0 && (
|
|
165
|
+
<TokensUsed tokens={displayManifest.tokensUsed} />
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{displayManifest.accessibility && (
|
|
169
|
+
<section className="component-accessibility">
|
|
170
|
+
<h2>Accessibility</h2>
|
|
171
|
+
<dl className="accessibility-details">
|
|
172
|
+
<div className="accessibility-item">
|
|
173
|
+
<dt>APG Pattern</dt>
|
|
174
|
+
<dd>
|
|
175
|
+
<a
|
|
176
|
+
href={`https://www.w3.org/WAI/ARIA/apg/patterns/${displayManifest.accessibility.apgPattern.toLowerCase().replace(/\s+/g, "-")}/`}
|
|
177
|
+
target="_blank"
|
|
178
|
+
rel="noopener noreferrer"
|
|
179
|
+
>
|
|
180
|
+
{displayManifest.accessibility.apgPattern}
|
|
181
|
+
</a>
|
|
182
|
+
</dd>
|
|
183
|
+
</div>
|
|
184
|
+
{displayManifest.accessibility.keyboard.length > 0 && (
|
|
185
|
+
<div className="accessibility-item">
|
|
186
|
+
<dt>Keyboard Interactions</dt>
|
|
187
|
+
<dd>
|
|
188
|
+
<ul className="keyboard-list">
|
|
189
|
+
{displayManifest.accessibility.keyboard.map((interaction) => (
|
|
190
|
+
<li key={interaction}>{interaction}</li>
|
|
191
|
+
))}
|
|
192
|
+
</ul>
|
|
193
|
+
</dd>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
<div className="accessibility-item">
|
|
197
|
+
<dt>Screen Reader</dt>
|
|
198
|
+
<dd>{displayManifest.accessibility.screenReader}</dd>
|
|
199
|
+
</div>
|
|
200
|
+
{displayManifest.accessibility.ariaPatterns &&
|
|
201
|
+
displayManifest.accessibility.ariaPatterns.length > 0 && (
|
|
202
|
+
<div className="accessibility-item">
|
|
203
|
+
<dt>ARIA Patterns</dt>
|
|
204
|
+
<dd>
|
|
205
|
+
<ul className="aria-list">
|
|
206
|
+
{displayManifest.accessibility.ariaPatterns.map((pattern) => (
|
|
207
|
+
<li key={pattern}>
|
|
208
|
+
<code>{pattern}</code>
|
|
209
|
+
</li>
|
|
210
|
+
))}
|
|
211
|
+
</ul>
|
|
212
|
+
</dd>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
{displayManifest.accessibility.knownLimitations &&
|
|
216
|
+
displayManifest.accessibility.knownLimitations.length > 0 && (
|
|
217
|
+
<div className="accessibility-item accessibility-item--warning">
|
|
218
|
+
<dt>Known Limitations</dt>
|
|
219
|
+
<dd>
|
|
220
|
+
<ul className="limitations-list">
|
|
221
|
+
{displayManifest.accessibility.knownLimitations.map((limitation) => (
|
|
222
|
+
<li key={limitation}>{limitation}</li>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
</dd>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</dl>
|
|
229
|
+
</section>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{mdxContent ? (
|
|
233
|
+
<MdxRenderer source={mdxContent} />
|
|
234
|
+
) : (
|
|
235
|
+
<div className="component-auto-docs">
|
|
236
|
+
{/* Auto-generated documentation from manifest */}
|
|
237
|
+
<section>
|
|
238
|
+
<h2>Usage</h2>
|
|
239
|
+
<pre>
|
|
240
|
+
<code>{`<ds-${id}></ds-${id}>`}</code>
|
|
241
|
+
</pre>
|
|
242
|
+
</section>
|
|
243
|
+
|
|
244
|
+
{displayManifest.props && displayManifest.props.length > 0 && (
|
|
245
|
+
<section>
|
|
246
|
+
<h2>Properties</h2>
|
|
247
|
+
<table className="props-table">
|
|
248
|
+
<thead>
|
|
249
|
+
<tr>
|
|
250
|
+
<th>Name</th>
|
|
251
|
+
<th>Type</th>
|
|
252
|
+
<th>Default</th>
|
|
253
|
+
<th>Description</th>
|
|
254
|
+
</tr>
|
|
255
|
+
</thead>
|
|
256
|
+
<tbody>
|
|
257
|
+
{displayManifest.props.map((prop) => (
|
|
258
|
+
<tr key={prop.name}>
|
|
259
|
+
<td>
|
|
260
|
+
<code>{prop.name}</code>
|
|
261
|
+
</td>
|
|
262
|
+
<td>
|
|
263
|
+
<code>{prop.type}</code>
|
|
264
|
+
</td>
|
|
265
|
+
<td>{prop.default ? <code>{prop.default}</code> : "—"}</td>
|
|
266
|
+
<td>{prop.description}</td>
|
|
267
|
+
</tr>
|
|
268
|
+
))}
|
|
269
|
+
</tbody>
|
|
270
|
+
</table>
|
|
271
|
+
</section>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{displayManifest.events && displayManifest.events.length > 0 && (
|
|
275
|
+
<section>
|
|
276
|
+
<h2>Events</h2>
|
|
277
|
+
<table className="events-table">
|
|
278
|
+
<thead>
|
|
279
|
+
<tr>
|
|
280
|
+
<th>Name</th>
|
|
281
|
+
<th>Description</th>
|
|
282
|
+
</tr>
|
|
283
|
+
</thead>
|
|
284
|
+
<tbody>
|
|
285
|
+
{displayManifest.events.map((event) => (
|
|
286
|
+
<tr key={event.name}>
|
|
287
|
+
<td>
|
|
288
|
+
<code>{event.name}</code>
|
|
289
|
+
</td>
|
|
290
|
+
<td>{event.description}</td>
|
|
291
|
+
</tr>
|
|
292
|
+
))}
|
|
293
|
+
</tbody>
|
|
294
|
+
</table>
|
|
295
|
+
</section>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{displayManifest.examples && displayManifest.examples.length > 0 && (
|
|
299
|
+
<section>
|
|
300
|
+
<h2>Examples</h2>
|
|
301
|
+
{displayManifest.examples.map((example) => (
|
|
302
|
+
<div key={example.title} className="example">
|
|
303
|
+
<h3>{example.title}</h3>
|
|
304
|
+
<pre>
|
|
305
|
+
<code>{example.code}</code>
|
|
306
|
+
</pre>
|
|
307
|
+
</div>
|
|
308
|
+
))}
|
|
309
|
+
</section>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</article>
|
|
314
|
+
</EditionProvider>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Edition } from "@hypoth-ui/docs-core";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { UpgradePrompt } from "../../components/upgrade/upgrade-prompt";
|
|
4
|
+
import { getEditionConfig } from "../../lib/content-resolver";
|
|
5
|
+
|
|
6
|
+
interface UpgradePageProps {
|
|
7
|
+
searchParams: Promise<{
|
|
8
|
+
component?: string;
|
|
9
|
+
from?: string;
|
|
10
|
+
to?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function EditionUpgradePage({ searchParams }: UpgradePageProps) {
|
|
15
|
+
const params = await searchParams;
|
|
16
|
+
const componentId = params.component;
|
|
17
|
+
const fromEdition = (params.from ?? "core") as Edition;
|
|
18
|
+
const toEdition = (params.to ?? "enterprise") as Edition;
|
|
19
|
+
|
|
20
|
+
// Load upgrade config from edition config
|
|
21
|
+
const config = await getEditionConfig();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="upgrade-page">
|
|
25
|
+
<UpgradePrompt
|
|
26
|
+
currentEdition={fromEdition}
|
|
27
|
+
requiredEdition={toEdition}
|
|
28
|
+
upgradeConfig={config.upgrade}
|
|
29
|
+
itemName={componentId}
|
|
30
|
+
componentId={componentId}
|
|
31
|
+
variant="full-page"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<div className="upgrade-actions">
|
|
35
|
+
<Link href="/" className="btn btn-secondary">
|
|
36
|
+
Back to Documentation
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style jsx>{`
|
|
41
|
+
.upgrade-page {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
align-items: center;
|
|
46
|
+
min-height: 80vh;
|
|
47
|
+
padding: 2rem;
|
|
48
|
+
gap: 2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.upgrade-actions {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 1rem;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.btn {
|
|
58
|
+
padding: 0.75rem 1.5rem;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
transition: all 0.2s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.btn-secondary {
|
|
66
|
+
background: var(--ds-surface-tertiary, #e9ecef);
|
|
67
|
+
color: var(--ds-text-primary, #333);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.btn-secondary:hover {
|
|
71
|
+
background: var(--ds-surface-hover, #dee2e6);
|
|
72
|
+
}
|
|
73
|
+
`}</style>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parseFrontmatter } from "@hypoth-ui/docs-core";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import { MdxRenderer } from "../../../components/mdx-renderer";
|
|
5
|
+
import { discoverGuides, resolveContentFile } from "../../../lib/content-resolver";
|
|
6
|
+
|
|
7
|
+
interface GuidePageProps {
|
|
8
|
+
params: Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Generate static params for all guides from content packs
|
|
14
|
+
export async function generateStaticParams() {
|
|
15
|
+
try {
|
|
16
|
+
const guides = await discoverGuides();
|
|
17
|
+
return guides.map(({ id }) => ({ id }));
|
|
18
|
+
} catch {
|
|
19
|
+
return [{ id: "getting-started" }, { id: "theming" }];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function generateMetadata({ params }: GuidePageProps) {
|
|
24
|
+
const { id } = await params;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const resolved = await resolveContentFile(`guides/${id}.mdx`);
|
|
28
|
+
if (!resolved) {
|
|
29
|
+
return { title: "Guide Not Found" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = await readFile(resolved.resolvedPath, "utf-8");
|
|
33
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
title: frontmatter.title,
|
|
37
|
+
description: frontmatter.description,
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { title: "Guide Not Found" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async function GuidePage({ params }: GuidePageProps) {
|
|
45
|
+
const { id } = await params;
|
|
46
|
+
|
|
47
|
+
// Resolve guide content through overlay chain
|
|
48
|
+
const resolved = await resolveContentFile(`guides/${id}.mdx`);
|
|
49
|
+
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
notFound();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let mdxContent: string;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
mdxContent = await readFile(resolved.resolvedPath, "utf-8");
|
|
58
|
+
} catch {
|
|
59
|
+
notFound();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<article className="guide-page">
|
|
64
|
+
<MdxRenderer source={mdxContent} />
|
|
65
|
+
</article>
|
|
66
|
+
);
|
|
67
|
+
}
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { applyDefaultFeatures } from "@hypoth-ui/docs-core";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { BrandedHeader } from "../components/branding/header";
|
|
5
|
+
import { FeedbackWidget } from "../components/feedback/feedback-widget";
|
|
6
|
+
import { NavSidebar } from "../components/nav-sidebar";
|
|
7
|
+
import { SearchInput } from "../components/search/search-input";
|
|
8
|
+
import { ThemeInitScript } from "../components/theme-init-script";
|
|
9
|
+
import { ThemeSwitcher } from "../components/theme-switcher";
|
|
10
|
+
import { BrandingProvider } from "../lib/branding-context";
|
|
11
|
+
import { getEditionConfig } from "../lib/content-resolver";
|
|
12
|
+
import "../styles/globals.css";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate metadata dynamically based on branding config
|
|
16
|
+
*/
|
|
17
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
18
|
+
const config = await getEditionConfig();
|
|
19
|
+
const siteName = config.branding?.name ?? "Design System";
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
title: {
|
|
23
|
+
template: `%s | ${siteName}`,
|
|
24
|
+
default: siteName,
|
|
25
|
+
},
|
|
26
|
+
description: `Documentation for ${siteName}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RootLayoutProps {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function RootLayout({ children }: RootLayoutProps) {
|
|
35
|
+
// Load edition config for branding
|
|
36
|
+
const config = await getEditionConfig();
|
|
37
|
+
|
|
38
|
+
// Apply default features to ensure all flags are present
|
|
39
|
+
const features = applyDefaultFeatures(config.features);
|
|
40
|
+
|
|
41
|
+
// Generate CSS custom properties for branding
|
|
42
|
+
const brandingStyles = `
|
|
43
|
+
:root {
|
|
44
|
+
--ds-brand-primary: ${config.branding?.primaryColor ?? "#0066cc"};
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<html lang="en" data-mode="light">
|
|
50
|
+
<head>
|
|
51
|
+
<ThemeInitScript />
|
|
52
|
+
{/* Inject branding CSS custom properties - safe as brandingStyles is server-generated */}
|
|
53
|
+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: CSS is server-generated from config */}
|
|
54
|
+
<style dangerouslySetInnerHTML={{ __html: brandingStyles }} />
|
|
55
|
+
{config.branding?.favicon && <link rel="icon" href={config.branding.favicon} />}
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
{/* T064: Skip link for keyboard accessibility */}
|
|
59
|
+
<a href="#main-content" className="skip-link">
|
|
60
|
+
Skip to main content
|
|
61
|
+
</a>
|
|
62
|
+
<BrandingProvider
|
|
63
|
+
branding={config.branding}
|
|
64
|
+
features={config.features}
|
|
65
|
+
upgrade={config.upgrade}
|
|
66
|
+
edition={config.edition}
|
|
67
|
+
editionId={config.id}
|
|
68
|
+
editionName={config.name}
|
|
69
|
+
>
|
|
70
|
+
<div className="docs-layout">
|
|
71
|
+
{/* T065: Proper landmark roles - header contains navigation */}
|
|
72
|
+
<BrandedHeader editionName={config.name}>
|
|
73
|
+
{/* T056: Conditionally render SearchInput based on features.search */}
|
|
74
|
+
{features.search && <SearchInput />}
|
|
75
|
+
{/* T055: Conditionally render ThemeSwitcher based on features.darkMode */}
|
|
76
|
+
{features.darkMode && <ThemeSwitcher />}
|
|
77
|
+
</BrandedHeader>
|
|
78
|
+
{/* T065: Proper landmark roles - nav element for navigation */}
|
|
79
|
+
<nav className="docs-sidebar" aria-label="Documentation navigation">
|
|
80
|
+
<NavSidebar edition={config.edition} />
|
|
81
|
+
</nav>
|
|
82
|
+
{/* T065: Proper landmark roles - main content area */}
|
|
83
|
+
<main id="main-content" className="docs-main">
|
|
84
|
+
<div className="docs-content">{children}</div>
|
|
85
|
+
</main>
|
|
86
|
+
{/* T057: Conditionally render FeedbackWidget based on features.feedback */}
|
|
87
|
+
{features.feedback && <FeedbackWidget />}
|
|
88
|
+
</div>
|
|
89
|
+
</BrandingProvider>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
);
|
|
93
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="docs-home">
|
|
6
|
+
<h1>Design System Documentation</h1>
|
|
7
|
+
<p>Welcome to the design system documentation.</p>
|
|
8
|
+
|
|
9
|
+
<nav className="docs-home-nav">
|
|
10
|
+
<h2>Get Started</h2>
|
|
11
|
+
<ul>
|
|
12
|
+
<li>
|
|
13
|
+
<Link href="/guides/getting-started">Getting Started</Link>
|
|
14
|
+
</li>
|
|
15
|
+
<li>
|
|
16
|
+
<Link href="/guides/theming">Theming</Link>
|
|
17
|
+
</li>
|
|
18
|
+
</ul>
|
|
19
|
+
|
|
20
|
+
<h2>Components</h2>
|
|
21
|
+
<ul>
|
|
22
|
+
<li>
|
|
23
|
+
<Link href="/components/button">Button</Link>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</nav>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Branded Header Component
|
|
5
|
+
*
|
|
6
|
+
* Site header with branding, navigation, and optional features.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Link from "next/link";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import { useBranding } from "../../lib/branding-context";
|
|
12
|
+
import { Logo } from "./logo";
|
|
13
|
+
|
|
14
|
+
export interface BrandedHeaderProps {
|
|
15
|
+
/** Additional content in the header (e.g., search, theme toggle) */
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
/** Custom class name */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Edition name to display (passed from server) */
|
|
20
|
+
editionName?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function BrandedHeader({ children, className = "", editionName }: BrandedHeaderProps) {
|
|
24
|
+
const { primaryColor } = useBranding();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<header className={`branded-header ${className}`}>
|
|
28
|
+
<div className="branded-header__left">
|
|
29
|
+
<Link href="/" className="branded-header__logo-link">
|
|
30
|
+
<Logo size="medium" />
|
|
31
|
+
</Link>
|
|
32
|
+
{editionName && editionName !== "Default" && (
|
|
33
|
+
<span className="branded-header__edition">{editionName}</span>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="branded-header__right">{children}</div>
|
|
38
|
+
|
|
39
|
+
<style jsx>{`
|
|
40
|
+
.branded-header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
padding: 0.75rem 1.5rem;
|
|
45
|
+
background: var(--ds-surface-primary, #fff);
|
|
46
|
+
border-bottom: 1px solid var(--ds-border, #e5e7eb);
|
|
47
|
+
position: sticky;
|
|
48
|
+
top: 0;
|
|
49
|
+
z-index: 100;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.branded-header__left {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.75rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.branded-header__logo-link {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
color: inherit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.branded-header__edition {
|
|
66
|
+
font-size: 0.75rem;
|
|
67
|
+
padding: 0.125rem 0.5rem;
|
|
68
|
+
border-radius: 9999px;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
background-color: var(--ds-brand-primary, ${primaryColor});
|
|
71
|
+
color: white;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.branded-header__right {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 1rem;
|
|
78
|
+
}
|
|
79
|
+
`}</style>
|
|
80
|
+
</header>
|
|
81
|
+
);
|
|
82
|
+
}
|