@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hypoth-org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @hypoth-ui/docs-renderer-next
2
+
3
+ ![Alpha](https://img.shields.io/badge/status-alpha-orange)
4
+
5
+ Next.js-based documentation site renderer for the hypoth-ui design system. Renders MDX content packs with edition filtering, tenant branding, search, and live component previews.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @hypoth-ui/docs-renderer-next
11
+ ```
12
+
13
+ > **Note:** This is primarily an internal package used to build the documentation site.
14
+
15
+ ## Usage
16
+
17
+ ### Import Components
18
+
19
+ ```typescript
20
+ import { NavSidebar } from '@hypoth-ui/docs-renderer-next/components/nav-sidebar';
21
+ import { MdxRenderer } from '@hypoth-ui/docs-renderer-next/components/mdx-renderer';
22
+ ```
23
+
24
+ ### Import Styles
25
+
26
+ ```typescript
27
+ import '@hypoth-ui/docs-renderer-next/styles/globals.css';
28
+ ```
29
+
30
+ ### Run the Docs Site
31
+
32
+ ```bash
33
+ pnpm --filter @ds/docs-app dev:core # Core edition
34
+ pnpm --filter @ds/docs-app dev:pro # Pro edition
35
+ pnpm --filter @ds/docs-app dev:enterprise # Enterprise edition
36
+ ```
37
+
38
+ ## Documentation
39
+
40
+ See the [main README](../../README.md) for full documentation and architecture overview.
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import type { CategoryInfo, ConformanceStatus } from "@hypoth-ui/docs-core";
4
+ import { useState } from "react";
5
+
6
+ interface CategoryFilterProps {
7
+ categories: CategoryInfo[];
8
+ selectedCategory: string | null;
9
+ selectedStatus: ConformanceStatus | null;
10
+ onCategoryChange: (category: string | null) => void;
11
+ onStatusChange: (status: ConformanceStatus | null) => void;
12
+ onSearchChange: (query: string) => void;
13
+ }
14
+
15
+ const STATUSES: Array<{ value: ConformanceStatus; label: string }> = [
16
+ { value: "conformant", label: "Conformant" },
17
+ { value: "partial", label: "Partial" },
18
+ { value: "non-conformant", label: "Non-Conformant" },
19
+ { value: "pending", label: "Pending" },
20
+ ];
21
+
22
+ export function CategoryFilter({
23
+ categories,
24
+ selectedCategory,
25
+ selectedStatus,
26
+ onCategoryChange,
27
+ onStatusChange,
28
+ onSearchChange,
29
+ }: CategoryFilterProps) {
30
+ const [searchValue, setSearchValue] = useState("");
31
+
32
+ const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
33
+ const value = e.target.value;
34
+ setSearchValue(value);
35
+ onSearchChange(value);
36
+ };
37
+
38
+ return (
39
+ <div className="mb-6 space-y-4">
40
+ {/* Search */}
41
+ <div>
42
+ <label htmlFor="search" className="sr-only">
43
+ Search components
44
+ </label>
45
+ <input
46
+ type="search"
47
+ id="search"
48
+ placeholder="Search components..."
49
+ value={searchValue}
50
+ onChange={handleSearchChange}
51
+ className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800"
52
+ />
53
+ </div>
54
+
55
+ {/* Filters */}
56
+ <div className="flex flex-wrap gap-4">
57
+ {/* Category filter */}
58
+ <div>
59
+ <label
60
+ htmlFor="category"
61
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
62
+ >
63
+ Category
64
+ </label>
65
+ <select
66
+ id="category"
67
+ value={selectedCategory ?? ""}
68
+ onChange={(e) => onCategoryChange(e.target.value || null)}
69
+ className="rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800"
70
+ >
71
+ <option value="">All Categories</option>
72
+ {categories.map((cat) => (
73
+ <option key={cat.id} value={cat.id}>
74
+ {cat.name} ({cat.count})
75
+ </option>
76
+ ))}
77
+ </select>
78
+ </div>
79
+
80
+ {/* Status filter */}
81
+ <div>
82
+ <label
83
+ htmlFor="status"
84
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
85
+ >
86
+ Status
87
+ </label>
88
+ <select
89
+ id="status"
90
+ value={selectedStatus ?? ""}
91
+ onChange={(e) => onStatusChange((e.target.value as ConformanceStatus) || null)}
92
+ className="rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800"
93
+ >
94
+ <option value="">All Statuses</option>
95
+ {STATUSES.map((status) => (
96
+ <option key={status.value} value={status.value}>
97
+ {status.label}
98
+ </option>
99
+ ))}
100
+ </select>
101
+ </div>
102
+
103
+ {/* Clear filters */}
104
+ {(selectedCategory || selectedStatus || searchValue) && (
105
+ <div className="flex items-end">
106
+ <button
107
+ type="button"
108
+ onClick={() => {
109
+ onCategoryChange(null);
110
+ onStatusChange(null);
111
+ setSearchValue("");
112
+ onSearchChange("");
113
+ }}
114
+ className="rounded-lg px-3 py-2 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
115
+ >
116
+ Clear filters
117
+ </button>
118
+ </div>
119
+ )}
120
+ </div>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import type { ComponentConformance } from "@hypoth-ui/docs-core";
4
+ import Link from "next/link";
5
+ import { StatusBadge } from "./StatusBadge";
6
+
7
+ interface ConformanceTableProps {
8
+ components: ComponentConformance[];
9
+ }
10
+
11
+ export function ConformanceTable({ components }: ConformanceTableProps) {
12
+ if (components.length === 0) {
13
+ return (
14
+ <div className="rounded-lg border border-gray-200 bg-gray-50 p-8 text-center dark:border-gray-700 dark:bg-gray-800">
15
+ <p className="text-gray-600 dark:text-gray-400">No components match the current filters.</p>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return (
21
+ <div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
22
+ <table className="w-full">
23
+ <thead className="bg-gray-50 dark:bg-gray-800">
24
+ <tr>
25
+ <th
26
+ scope="col"
27
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
28
+ >
29
+ Component
30
+ </th>
31
+ <th
32
+ scope="col"
33
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
34
+ >
35
+ Category
36
+ </th>
37
+ <th
38
+ scope="col"
39
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
40
+ >
41
+ Status
42
+ </th>
43
+ <th
44
+ scope="col"
45
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
46
+ >
47
+ Automated
48
+ </th>
49
+ <th
50
+ scope="col"
51
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
52
+ >
53
+ Manual Audit
54
+ </th>
55
+ <th
56
+ scope="col"
57
+ className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
58
+ >
59
+ Last Updated
60
+ </th>
61
+ </tr>
62
+ </thead>
63
+ <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
64
+ {components.map((component) => (
65
+ <tr key={component.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
66
+ <td className="px-4 py-3">
67
+ <Link
68
+ href={`/accessibility/${component.id}`}
69
+ className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
70
+ >
71
+ {component.name}
72
+ </Link>
73
+ <div className="text-xs text-gray-500">{component.id}</div>
74
+ </td>
75
+ <td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
76
+ {component.category}
77
+ </td>
78
+ <td className="px-4 py-3">
79
+ <StatusBadge status={component.status} size="sm" />
80
+ </td>
81
+ <td className="px-4 py-3 text-sm">
82
+ {component.automatedPassed ? (
83
+ <span className="text-green-600 dark:text-green-400">✓ Passed</span>
84
+ ) : (
85
+ <span className="text-red-600 dark:text-red-400">✗ Failed</span>
86
+ )}
87
+ </td>
88
+ <td className="px-4 py-3 text-sm">
89
+ {component.manualAuditComplete ? (
90
+ <span className="text-green-600 dark:text-green-400">
91
+ {component.passCount}/{(component.passCount ?? 0) + (component.failCount ?? 0)}{" "}
92
+ passed
93
+ </span>
94
+ ) : (
95
+ <span className="text-gray-500 dark:text-gray-400">Not audited</span>
96
+ )}
97
+ </td>
98
+ <td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
99
+ {component.lastAuditDate
100
+ ? new Date(component.lastAuditDate).toLocaleDateString()
101
+ : "—"}
102
+ </td>
103
+ </tr>
104
+ ))}
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import type { ConformanceStatus } from "@hypoth-ui/docs-core";
4
+
5
+ interface StatusBadgeProps {
6
+ status: ConformanceStatus;
7
+ size?: "sm" | "md";
8
+ }
9
+
10
+ const STATUS_CONFIG: Record<ConformanceStatus, { icon: string; label: string; className: string }> =
11
+ {
12
+ conformant: {
13
+ icon: "✅",
14
+ label: "Conformant",
15
+ className: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
16
+ },
17
+ partial: {
18
+ icon: "⚠️",
19
+ label: "Partial",
20
+ className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
21
+ },
22
+ "non-conformant": {
23
+ icon: "❌",
24
+ label: "Non-Conformant",
25
+ className: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
26
+ },
27
+ pending: {
28
+ icon: "⏳",
29
+ label: "Pending Audit",
30
+ className: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200",
31
+ },
32
+ };
33
+
34
+ export function StatusBadge({ status, size = "md" }: StatusBadgeProps) {
35
+ const config = STATUS_CONFIG[status] ?? STATUS_CONFIG.pending;
36
+
37
+ const sizeClasses = size === "sm" ? "px-2 py-0.5 text-xs" : "px-3 py-1 text-sm";
38
+
39
+ return (
40
+ <span
41
+ className={`inline-flex items-center gap-1.5 rounded-full font-medium ${config.className} ${sizeClasses}`}
42
+ >
43
+ <span aria-hidden="true">{config.icon}</span>
44
+ <span>{config.label}</span>
45
+ </span>
46
+ );
47
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import type { ComponentConformance } from "@hypoth-ui/docs-core";
4
+ import Link from "next/link";
5
+ import { use } from "react";
6
+ import { StatusBadge } from "../StatusBadge";
7
+
8
+ // Placeholder component data
9
+ const COMPONENT_DATA: Record<string, ComponentConformance> = {
10
+ "ds-button": {
11
+ id: "ds-button",
12
+ name: "Button",
13
+ category: "form-controls",
14
+ status: "conformant",
15
+ wcagLevel: "AA",
16
+ lastAuditDate: "2026-01-04T10:00:00Z",
17
+ lastAuditor: "auditor@example.com",
18
+ automatedPassed: true,
19
+ manualAuditComplete: true,
20
+ passCount: 10,
21
+ failCount: 0,
22
+ exceptionCount: 0,
23
+ },
24
+ };
25
+
26
+ interface ComponentPageProps {
27
+ params: Promise<{
28
+ component: string;
29
+ }>;
30
+ }
31
+
32
+ export default function ComponentDetailPage({ params }: ComponentPageProps) {
33
+ const { component: componentId } = use(params);
34
+ const component = COMPONENT_DATA[componentId];
35
+
36
+ if (!component) {
37
+ return (
38
+ <div className="max-w-4xl mx-auto px-4 py-8">
39
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">
40
+ Component Not Found
41
+ </h1>
42
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
43
+ No accessibility data found for component: {componentId}
44
+ </p>
45
+ <Link
46
+ href="/accessibility"
47
+ className="text-blue-600 hover:text-blue-800 dark:text-blue-400"
48
+ >
49
+ ← Back to Accessibility Conformance
50
+ </Link>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div className="max-w-4xl mx-auto px-4 py-8">
57
+ {/* Breadcrumb */}
58
+ <nav className="mb-6 text-sm">
59
+ <Link
60
+ href="/accessibility"
61
+ className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
62
+ >
63
+ Accessibility Conformance
64
+ </Link>
65
+ <span className="mx-2 text-gray-400">/</span>
66
+ <span className="text-gray-900 dark:text-gray-100">{component.name}</span>
67
+ </nav>
68
+
69
+ {/* Header */}
70
+ <header className="mb-8">
71
+ <div className="flex items-center gap-4 mb-2">
72
+ <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{component.name}</h1>
73
+ <StatusBadge status={component.status} />
74
+ </div>
75
+ <p className="text-gray-600 dark:text-gray-400">
76
+ {component.id} • {component.category}
77
+ </p>
78
+ </header>
79
+
80
+ {/* Status summary */}
81
+ <section className="mb-8 grid grid-cols-2 md:grid-cols-4 gap-4">
82
+ <div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
83
+ <div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
84
+ {component.automatedPassed ? "✓ Passed" : "✗ Failed"}
85
+ </div>
86
+ <div className="text-sm text-gray-600 dark:text-gray-400">Automated Tests</div>
87
+ </div>
88
+ <div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
89
+ <div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
90
+ {component.manualAuditComplete ? "Complete" : "Pending"}
91
+ </div>
92
+ <div className="text-sm text-gray-600 dark:text-gray-400">Manual Audit</div>
93
+ </div>
94
+ <div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
95
+ <div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
96
+ WCAG {component.wcagLevel}
97
+ </div>
98
+ <div className="text-sm text-gray-600 dark:text-gray-400">Target Level</div>
99
+ </div>
100
+ <div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
101
+ <div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
102
+ {component.lastAuditDate ? new Date(component.lastAuditDate).toLocaleDateString() : "—"}
103
+ </div>
104
+ <div className="text-sm text-gray-600 dark:text-gray-400">Last Audit</div>
105
+ </div>
106
+ </section>
107
+
108
+ {/* Manual audit results */}
109
+ {component.manualAuditComplete && (
110
+ <section className="mb-8">
111
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
112
+ Manual Audit Results
113
+ </h2>
114
+ <div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800">
115
+ <div className="grid grid-cols-3 gap-4 text-center">
116
+ <div>
117
+ <div className="text-2xl font-bold text-green-600 dark:text-green-400">
118
+ {component.passCount ?? 0}
119
+ </div>
120
+ <div className="text-sm text-gray-600 dark:text-gray-400">Passed</div>
121
+ </div>
122
+ <div>
123
+ <div className="text-2xl font-bold text-red-600 dark:text-red-400">
124
+ {component.failCount ?? 0}
125
+ </div>
126
+ <div className="text-sm text-gray-600 dark:text-gray-400">Failed</div>
127
+ </div>
128
+ <div>
129
+ <div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
130
+ {component.exceptionCount ?? 0}
131
+ </div>
132
+ <div className="text-sm text-gray-600 dark:text-gray-400">Exceptions</div>
133
+ </div>
134
+ </div>
135
+ {component.lastAuditor && (
136
+ <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-600 dark:text-gray-400">
137
+ Audited by: {component.lastAuditor}
138
+ </div>
139
+ )}
140
+ </div>
141
+ </section>
142
+ )}
143
+
144
+ {/* Component documentation link */}
145
+ <section className="mb-8">
146
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
147
+ Documentation
148
+ </h2>
149
+ <Link
150
+ href={`/components/${component.id.replace("ds-", "")}`}
151
+ className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 dark:text-blue-400"
152
+ >
153
+ View {component.name} component documentation →
154
+ </Link>
155
+ </section>
156
+
157
+ {/* Back link */}
158
+ <Link
159
+ href="/accessibility"
160
+ className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
161
+ >
162
+ ← Back to Accessibility Conformance
163
+ </Link>
164
+ </div>
165
+ );
166
+ }