@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,269 @@
1
+ "use client";
2
+
3
+ import type { Edition, NavItem } from "@hypoth-ui/docs-core";
4
+ import Link from "next/link";
5
+ import { usePathname } from "next/navigation";
6
+
7
+ interface NavSidebarProps {
8
+ /** Custom navigation items (optional, uses defaults if not provided) */
9
+ navigation?: {
10
+ components: NavItem[];
11
+ guides: NavItem[];
12
+ };
13
+ /** Current edition for filtering */
14
+ edition?: Edition;
15
+ /** Edition map for checking component availability */
16
+ editionMap?: {
17
+ components: Record<string, { editions: Edition[]; status: string; name: string }>;
18
+ };
19
+ }
20
+
21
+ // Default navigation structure
22
+ const defaultNavigation = {
23
+ guides: [
24
+ {
25
+ id: "introduction",
26
+ label: "Introduction",
27
+ href: "",
28
+ type: "category" as const,
29
+ order: 0,
30
+ children: [
31
+ {
32
+ id: "getting-started",
33
+ label: "Getting Started",
34
+ href: "/guides/getting-started",
35
+ type: "guide" as const,
36
+ order: 1,
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ id: "customization",
42
+ label: "Customization",
43
+ href: "",
44
+ type: "category" as const,
45
+ order: 1,
46
+ children: [
47
+ {
48
+ id: "theming",
49
+ label: "Theming",
50
+ href: "/guides/theming",
51
+ type: "guide" as const,
52
+ order: 1,
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ components: [
58
+ {
59
+ id: "actions",
60
+ label: "Actions",
61
+ href: "",
62
+ type: "category" as const,
63
+ order: 0,
64
+ children: [
65
+ {
66
+ id: "button",
67
+ label: "Button",
68
+ href: "/components/button",
69
+ type: "component" as const,
70
+ order: 0,
71
+ status: "stable" as const,
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ id: "forms",
77
+ label: "Forms",
78
+ href: "",
79
+ type: "category" as const,
80
+ order: 1,
81
+ children: [
82
+ {
83
+ id: "input",
84
+ label: "Input",
85
+ href: "/components/input",
86
+ type: "component" as const,
87
+ order: 0,
88
+ status: "stable" as const,
89
+ },
90
+ ],
91
+ },
92
+ ],
93
+ };
94
+
95
+ /**
96
+ * Edition hierarchy for checking availability
97
+ */
98
+ const EDITION_INCLUDES: Record<Edition, Edition[]> = {
99
+ core: [],
100
+ pro: ["core"],
101
+ enterprise: ["core", "pro"],
102
+ };
103
+
104
+ /**
105
+ * Check if a component is available for the current edition
106
+ */
107
+ function isComponentAvailable(
108
+ componentId: string,
109
+ edition: Edition,
110
+ editionMap?: NavSidebarProps["editionMap"]
111
+ ): boolean {
112
+ if (!editionMap) return true; // No filtering if no edition map
113
+
114
+ const component = editionMap.components[componentId];
115
+ if (!component) return true; // Unknown components are shown
116
+
117
+ // Check if any of the component's editions are available
118
+ const includedEditions = [edition, ...EDITION_INCLUDES[edition]];
119
+ return component.editions.some((e) => includedEditions.includes(e));
120
+ }
121
+
122
+ interface NavLinkProps {
123
+ item: NavItem;
124
+ disabled?: boolean;
125
+ requiredEdition?: Edition;
126
+ }
127
+
128
+ function NavLink({ item, disabled, requiredEdition }: NavLinkProps) {
129
+ const pathname = usePathname();
130
+ const isActive = pathname === item.href;
131
+
132
+ if (disabled) {
133
+ return (
134
+ <span className="nav-link nav-link--disabled" title={`Requires ${requiredEdition} edition`}>
135
+ <span className="nav-link-label">{item.label}</span>
136
+ <span className="nav-link-lock">🔒</span>
137
+ </span>
138
+ );
139
+ }
140
+
141
+ return (
142
+ <Link
143
+ href={item.href}
144
+ className={`nav-link ${isActive ? "nav-link--active" : ""}`}
145
+ aria-current={isActive ? "page" : undefined}
146
+ >
147
+ <span className="nav-link-label">{item.label}</span>
148
+ {item.status && (
149
+ <span className={`nav-link-status nav-link-status--${item.status}`}>{item.status}</span>
150
+ )}
151
+ </Link>
152
+ );
153
+ }
154
+
155
+ interface NavCategoryProps {
156
+ item: NavItem;
157
+ edition?: Edition;
158
+ editionMap?: NavSidebarProps["editionMap"];
159
+ }
160
+
161
+ function NavCategory({ item, edition, editionMap }: NavCategoryProps) {
162
+ // Filter children by edition
163
+ const visibleChildren = item.children?.filter((child) => {
164
+ if (child.type !== "component") return true;
165
+ if (!edition || !editionMap) return true;
166
+ return isComponentAvailable(child.id, edition, editionMap);
167
+ });
168
+
169
+ // Get disabled children (for showing with lock icon)
170
+ const disabledChildren = item.children?.filter((child) => {
171
+ if (child.type !== "component") return false;
172
+ if (!edition || !editionMap) return false;
173
+ return !isComponentAvailable(child.id, edition, editionMap);
174
+ });
175
+
176
+ // Don't render empty categories
177
+ if (!visibleChildren?.length && !disabledChildren?.length) {
178
+ return null;
179
+ }
180
+
181
+ return (
182
+ <div className="nav-category">
183
+ <h3 className="nav-category-title">{item.label}</h3>
184
+ <ul className="nav-category-list">
185
+ {visibleChildren?.map((child) => (
186
+ <li key={child.id}>
187
+ <NavLink item={child} />
188
+ </li>
189
+ ))}
190
+ {disabledChildren?.map((child) => {
191
+ const component = editionMap?.components[child.id];
192
+ const requiredEdition = component?.editions[0] as Edition | undefined;
193
+ return (
194
+ <li key={child.id}>
195
+ <NavLink item={child} disabled requiredEdition={requiredEdition} />
196
+ </li>
197
+ );
198
+ })}
199
+ </ul>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export function NavSidebar({
205
+ navigation = defaultNavigation,
206
+ edition,
207
+ editionMap,
208
+ }: NavSidebarProps) {
209
+ return (
210
+ <nav className="nav-sidebar" aria-label="Documentation navigation">
211
+ <div className="nav-sidebar-header">
212
+ <Link href="/" className="nav-sidebar-logo">
213
+ Design System
214
+ </Link>
215
+ {edition && (
216
+ <span className={`nav-sidebar-edition nav-sidebar-edition--${edition}`}>{edition}</span>
217
+ )}
218
+ </div>
219
+
220
+ <div className="nav-sidebar-content">
221
+ <section className="nav-section">
222
+ <h2 className="nav-section-title">Guides</h2>
223
+ {navigation.guides.map((item) => (
224
+ <NavCategory key={item.id} item={item} edition={edition} editionMap={editionMap} />
225
+ ))}
226
+ </section>
227
+
228
+ <section className="nav-section">
229
+ <h2 className="nav-section-title">Components</h2>
230
+ {navigation.components.map((item) => (
231
+ <NavCategory key={item.id} item={item} edition={edition} editionMap={editionMap} />
232
+ ))}
233
+ </section>
234
+ </div>
235
+
236
+ <style jsx>{`
237
+ .nav-link--disabled {
238
+ opacity: 0.5;
239
+ cursor: not-allowed;
240
+ display: flex;
241
+ justify-content: space-between;
242
+ align-items: center;
243
+ }
244
+ .nav-link-lock {
245
+ font-size: 0.75rem;
246
+ }
247
+ .nav-sidebar-edition {
248
+ font-size: 0.75rem;
249
+ padding: 0.125rem 0.5rem;
250
+ border-radius: 9999px;
251
+ text-transform: capitalize;
252
+ font-weight: 600;
253
+ }
254
+ .nav-sidebar-edition--core {
255
+ background-color: #e0f2fe;
256
+ color: #0369a1;
257
+ }
258
+ .nav-sidebar-edition--pro {
259
+ background-color: #f0fdf4;
260
+ color: #15803d;
261
+ }
262
+ .nav-sidebar-edition--enterprise {
263
+ background-color: #faf5ff;
264
+ color: #7e22ce;
265
+ }
266
+ `}</style>
267
+ </nav>
268
+ );
269
+ }