@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,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CategoryInfo, ConformanceData, ConformanceStatus } from "@hypoth-ui/docs-core";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { CategoryFilter } from "./CategoryFilter";
|
|
6
|
+
import { ConformanceTable } from "./ConformanceTable";
|
|
7
|
+
|
|
8
|
+
// Placeholder data - in production, this would be loaded from the conformance report
|
|
9
|
+
const PLACEHOLDER_DATA: ConformanceData = {
|
|
10
|
+
version: "0.0.0",
|
|
11
|
+
generatedAt: new Date().toISOString(),
|
|
12
|
+
wcagVersion: "2.1",
|
|
13
|
+
targetLevel: "AA",
|
|
14
|
+
components: [
|
|
15
|
+
{
|
|
16
|
+
id: "ds-button",
|
|
17
|
+
name: "Button",
|
|
18
|
+
category: "form-controls",
|
|
19
|
+
status: "conformant",
|
|
20
|
+
wcagLevel: "AA",
|
|
21
|
+
automatedPassed: true,
|
|
22
|
+
manualAuditComplete: true,
|
|
23
|
+
passCount: 10,
|
|
24
|
+
failCount: 0,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "ds-input",
|
|
28
|
+
name: "Input",
|
|
29
|
+
category: "form-controls",
|
|
30
|
+
status: "conformant",
|
|
31
|
+
wcagLevel: "AA",
|
|
32
|
+
automatedPassed: true,
|
|
33
|
+
manualAuditComplete: true,
|
|
34
|
+
passCount: 10,
|
|
35
|
+
failCount: 0,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "ds-checkbox",
|
|
39
|
+
name: "Checkbox",
|
|
40
|
+
category: "form-controls",
|
|
41
|
+
status: "pending",
|
|
42
|
+
wcagLevel: "AA",
|
|
43
|
+
automatedPassed: true,
|
|
44
|
+
manualAuditComplete: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "ds-dialog",
|
|
48
|
+
name: "Dialog",
|
|
49
|
+
category: "overlays",
|
|
50
|
+
status: "conformant",
|
|
51
|
+
wcagLevel: "AA",
|
|
52
|
+
automatedPassed: true,
|
|
53
|
+
manualAuditComplete: true,
|
|
54
|
+
passCount: 12,
|
|
55
|
+
failCount: 0,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "ds-tooltip",
|
|
59
|
+
name: "Tooltip",
|
|
60
|
+
category: "overlays",
|
|
61
|
+
status: "partial",
|
|
62
|
+
wcagLevel: "AA",
|
|
63
|
+
automatedPassed: true,
|
|
64
|
+
manualAuditComplete: true,
|
|
65
|
+
passCount: 8,
|
|
66
|
+
failCount: 2,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
summary: {
|
|
70
|
+
total: 5,
|
|
71
|
+
conformant: 3,
|
|
72
|
+
partial: 1,
|
|
73
|
+
nonConformant: 0,
|
|
74
|
+
pending: 1,
|
|
75
|
+
conformancePercentage: 60,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default function AccessibilityPage() {
|
|
80
|
+
const [data] = useState<ConformanceData>(PLACEHOLDER_DATA);
|
|
81
|
+
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
82
|
+
const [selectedStatus, setSelectedStatus] = useState<ConformanceStatus | null>(null);
|
|
83
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
84
|
+
|
|
85
|
+
// Get categories
|
|
86
|
+
const categories = useMemo<CategoryInfo[]>(() => {
|
|
87
|
+
const counts = new Map<string, number>();
|
|
88
|
+
for (const c of data.components) {
|
|
89
|
+
counts.set(c.category, (counts.get(c.category) ?? 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
return Array.from(counts.entries()).map(([id, count]) => ({
|
|
92
|
+
id,
|
|
93
|
+
name: id.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
94
|
+
count,
|
|
95
|
+
}));
|
|
96
|
+
}, [data.components]);
|
|
97
|
+
|
|
98
|
+
// Filter components
|
|
99
|
+
const filteredComponents = useMemo(() => {
|
|
100
|
+
let result = data.components;
|
|
101
|
+
|
|
102
|
+
if (selectedCategory) {
|
|
103
|
+
result = result.filter((c) => c.category === selectedCategory);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (selectedStatus) {
|
|
107
|
+
result = result.filter((c) => c.status === selectedStatus);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (searchQuery) {
|
|
111
|
+
const query = searchQuery.toLowerCase();
|
|
112
|
+
result = result.filter(
|
|
113
|
+
(c) => c.id.toLowerCase().includes(query) || c.name.toLowerCase().includes(query)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}, [data.components, selectedCategory, selectedStatus, searchQuery]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="max-w-6xl mx-auto px-4 py-8">
|
|
122
|
+
{/* Header */}
|
|
123
|
+
<header className="mb-8">
|
|
124
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
|
125
|
+
Accessibility Conformance
|
|
126
|
+
</h1>
|
|
127
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
128
|
+
WCAG {data.wcagVersion} Level {data.targetLevel} conformance status for all components in
|
|
129
|
+
the design system.
|
|
130
|
+
</p>
|
|
131
|
+
</header>
|
|
132
|
+
|
|
133
|
+
{/* Summary cards */}
|
|
134
|
+
<section className="mb-8 grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
135
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
|
136
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
|
137
|
+
{data.summary.total}
|
|
138
|
+
</div>
|
|
139
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">Total Components</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="rounded-lg border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-900/20">
|
|
142
|
+
<div className="text-2xl font-bold text-green-700 dark:text-green-400">
|
|
143
|
+
{data.summary.conformant}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="text-sm text-green-600 dark:text-green-400">Conformant</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-900/20">
|
|
148
|
+
<div className="text-2xl font-bold text-yellow-700 dark:text-yellow-400">
|
|
149
|
+
{data.summary.partial}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="text-sm text-yellow-600 dark:text-yellow-400">Partial</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
|
|
154
|
+
<div className="text-2xl font-bold text-red-700 dark:text-red-400">
|
|
155
|
+
{data.summary.nonConformant}
|
|
156
|
+
</div>
|
|
157
|
+
<div className="text-sm text-red-600 dark:text-red-400">Non-Conformant</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
|
|
160
|
+
<div className="text-2xl font-bold text-gray-700 dark:text-gray-400">
|
|
161
|
+
{data.summary.pending}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">Pending Audit</div>
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
|
|
167
|
+
{/* Progress bar */}
|
|
168
|
+
<section className="mb-8">
|
|
169
|
+
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-400 mb-2">
|
|
170
|
+
<span>Conformance Progress</span>
|
|
171
|
+
<span>{data.summary.conformancePercentage}%</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
|
|
174
|
+
<div
|
|
175
|
+
className="h-full bg-green-500 transition-all duration-300"
|
|
176
|
+
style={{ width: `${data.summary.conformancePercentage}%` }}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
{/* Filters */}
|
|
182
|
+
<CategoryFilter
|
|
183
|
+
categories={categories}
|
|
184
|
+
selectedCategory={selectedCategory}
|
|
185
|
+
selectedStatus={selectedStatus}
|
|
186
|
+
onCategoryChange={setSelectedCategory}
|
|
187
|
+
onStatusChange={setSelectedStatus}
|
|
188
|
+
onSearchChange={setSearchQuery}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{/* Results count */}
|
|
192
|
+
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
|
193
|
+
Showing {filteredComponents.length} of {data.components.length} components
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Component table */}
|
|
197
|
+
<ConformanceTable components={filteredComponents} />
|
|
198
|
+
|
|
199
|
+
{/* Footer info */}
|
|
200
|
+
<footer className="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
|
201
|
+
<p>
|
|
202
|
+
Last updated: {new Date(data.generatedAt).toLocaleDateString()} • Version: {data.version}
|
|
203
|
+
</p>
|
|
204
|
+
</footer>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search API Route
|
|
3
|
+
*
|
|
4
|
+
* Provides search functionality for the documentation site.
|
|
5
|
+
* Loads the pre-built search index and performs client-side filtering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
9
|
+
import type { SearchEntry, SearchIndex } from "@hypoth-ui/docs-core";
|
|
10
|
+
|
|
11
|
+
// Cache the search index in memory
|
|
12
|
+
let searchIndex: SearchIndex | null = null;
|
|
13
|
+
let indexLoadPromise: Promise<SearchIndex | null> | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load the search index from the file system or CDN
|
|
17
|
+
*/
|
|
18
|
+
async function loadSearchIndex(): Promise<SearchIndex | null> {
|
|
19
|
+
if (searchIndex) {
|
|
20
|
+
return searchIndex;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (indexLoadPromise) {
|
|
24
|
+
return indexLoadPromise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
indexLoadPromise = (async () => {
|
|
28
|
+
try {
|
|
29
|
+
// In production, this would load from a CDN or static file
|
|
30
|
+
// For now, we'll use a bundled index or return mock data
|
|
31
|
+
const indexPath = process.env.SEARCH_INDEX_PATH || "./dist/search-index.json";
|
|
32
|
+
|
|
33
|
+
// Try to load from file system in development
|
|
34
|
+
if (process.env.NODE_ENV === "development") {
|
|
35
|
+
const fs = await import("node:fs/promises");
|
|
36
|
+
const path = await import("node:path");
|
|
37
|
+
const fullPath = path.join(process.cwd(), indexPath);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
41
|
+
searchIndex = JSON.parse(content) as SearchIndex;
|
|
42
|
+
return searchIndex;
|
|
43
|
+
} catch {
|
|
44
|
+
// Index not found, return empty
|
|
45
|
+
console.warn(`Search index not found at ${fullPath}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Return a minimal fallback index
|
|
50
|
+
searchIndex = {
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
generatedAt: new Date().toISOString(),
|
|
53
|
+
edition: "enterprise",
|
|
54
|
+
entries: [],
|
|
55
|
+
facets: {
|
|
56
|
+
categories: [],
|
|
57
|
+
types: [],
|
|
58
|
+
tags: [],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return searchIndex;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to load search index:", error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
return indexLoadPromise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Search entries by query
|
|
74
|
+
*/
|
|
75
|
+
function searchEntries(
|
|
76
|
+
entries: SearchEntry[],
|
|
77
|
+
query: string,
|
|
78
|
+
options: {
|
|
79
|
+
type?: "component" | "guide";
|
|
80
|
+
category?: string;
|
|
81
|
+
limit?: number;
|
|
82
|
+
} = {}
|
|
83
|
+
): SearchEntry[] {
|
|
84
|
+
const { type, category, limit = 20 } = options;
|
|
85
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
86
|
+
|
|
87
|
+
if (!normalizedQuery) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Filter and score entries
|
|
92
|
+
const scored = entries
|
|
93
|
+
.filter((entry) => {
|
|
94
|
+
// Apply type filter
|
|
95
|
+
if (type && entry.type !== type) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Apply category filter
|
|
100
|
+
if (category && entry.category !== category) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
})
|
|
106
|
+
.map((entry) => {
|
|
107
|
+
let score = 0;
|
|
108
|
+
|
|
109
|
+
// Title match (highest weight)
|
|
110
|
+
const titleLower = entry.title.toLowerCase();
|
|
111
|
+
if (titleLower === normalizedQuery) {
|
|
112
|
+
score += 100;
|
|
113
|
+
} else if (titleLower.startsWith(normalizedQuery)) {
|
|
114
|
+
score += 50;
|
|
115
|
+
} else if (titleLower.includes(normalizedQuery)) {
|
|
116
|
+
score += 25;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Description match
|
|
120
|
+
const descLower = entry.description?.toLowerCase() || "";
|
|
121
|
+
if (descLower.includes(normalizedQuery)) {
|
|
122
|
+
score += 15;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Excerpt match
|
|
126
|
+
const excerptLower = entry.excerpt?.toLowerCase() || "";
|
|
127
|
+
if (excerptLower.includes(normalizedQuery)) {
|
|
128
|
+
score += 10;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Tag match
|
|
132
|
+
const tagMatch = entry.tags?.some(
|
|
133
|
+
(tag) => tag.toLowerCase().includes(normalizedQuery) || normalizedQuery.includes(tag.toLowerCase())
|
|
134
|
+
);
|
|
135
|
+
if (tagMatch) {
|
|
136
|
+
score += 20;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { entry, score };
|
|
140
|
+
})
|
|
141
|
+
.filter(({ score }) => score > 0)
|
|
142
|
+
.sort((a, b) => b.score - a.score)
|
|
143
|
+
.slice(0, limit)
|
|
144
|
+
.map(({ entry }) => entry);
|
|
145
|
+
|
|
146
|
+
return scored;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* GET /api/search
|
|
151
|
+
*
|
|
152
|
+
* Query parameters:
|
|
153
|
+
* - q: Search query (required)
|
|
154
|
+
* - type: Filter by type (component | guide)
|
|
155
|
+
* - category: Filter by category
|
|
156
|
+
* - limit: Maximum results (default: 20)
|
|
157
|
+
*/
|
|
158
|
+
export async function GET(request: NextRequest) {
|
|
159
|
+
const searchParams = request.nextUrl.searchParams;
|
|
160
|
+
const query = searchParams.get("q") || "";
|
|
161
|
+
const type = searchParams.get("type") as "component" | "guide" | null;
|
|
162
|
+
const category = searchParams.get("category");
|
|
163
|
+
const limit = Number.parseInt(searchParams.get("limit") || "20", 10);
|
|
164
|
+
|
|
165
|
+
if (!query.trim()) {
|
|
166
|
+
return NextResponse.json({
|
|
167
|
+
results: [],
|
|
168
|
+
query: "",
|
|
169
|
+
total: 0,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const index = await loadSearchIndex();
|
|
174
|
+
|
|
175
|
+
if (!index) {
|
|
176
|
+
return NextResponse.json(
|
|
177
|
+
{ error: "Search index not available" },
|
|
178
|
+
{ status: 503 }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const results = searchEntries(index.entries, query, {
|
|
183
|
+
type: type || undefined,
|
|
184
|
+
category: category || undefined,
|
|
185
|
+
limit,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return NextResponse.json({
|
|
189
|
+
results,
|
|
190
|
+
query,
|
|
191
|
+
total: results.length,
|
|
192
|
+
facets: index.facets,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* POST /api/search
|
|
198
|
+
*
|
|
199
|
+
* Alternative endpoint for more complex queries
|
|
200
|
+
*/
|
|
201
|
+
export async function POST(request: NextRequest) {
|
|
202
|
+
try {
|
|
203
|
+
const body = await request.json();
|
|
204
|
+
const { query = "", type, category, limit = 20 } = body;
|
|
205
|
+
|
|
206
|
+
if (!query.trim()) {
|
|
207
|
+
return NextResponse.json({
|
|
208
|
+
results: [],
|
|
209
|
+
query: "",
|
|
210
|
+
total: 0,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const index = await loadSearchIndex();
|
|
215
|
+
|
|
216
|
+
if (!index) {
|
|
217
|
+
return NextResponse.json(
|
|
218
|
+
{ error: "Search index not available" },
|
|
219
|
+
{ status: 503 }
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const results = searchEntries(index.entries, query, {
|
|
224
|
+
type,
|
|
225
|
+
category,
|
|
226
|
+
limit,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return NextResponse.json({
|
|
230
|
+
results,
|
|
231
|
+
query,
|
|
232
|
+
total: results.length,
|
|
233
|
+
facets: index.facets,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
return NextResponse.json(
|
|
237
|
+
{ error: "Invalid request body" },
|
|
238
|
+
{ status: 400 }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|