create-eziwiki 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.
Files changed (98) hide show
  1. package/README.md +46 -0
  2. package/bin/create-eziwiki.mjs +83 -0
  3. package/lib/scaffold.mjs +230 -0
  4. package/lib/scaffold.test.mjs +270 -0
  5. package/package.json +38 -0
  6. package/template/README.md +39 -0
  7. package/template/app/[...slug]/page.tsx +162 -0
  8. package/template/app/error.tsx +77 -0
  9. package/template/app/global-error.tsx +76 -0
  10. package/template/app/globals.css +44 -0
  11. package/template/app/graph/page.tsx +62 -0
  12. package/template/app/layout.tsx +164 -0
  13. package/template/app/not-found.tsx +48 -0
  14. package/template/app/page.tsx +34 -0
  15. package/template/app/robots.ts +20 -0
  16. package/template/app/sitemap.ts +48 -0
  17. package/template/components/ThemeToggle.tsx +77 -0
  18. package/template/components/graph/GraphView.tsx +156 -0
  19. package/template/components/layout/Backlinks.tsx +42 -0
  20. package/template/components/layout/Breadcrumb.tsx +88 -0
  21. package/template/components/layout/MobileMenu.tsx +299 -0
  22. package/template/components/layout/NavigationButtons.tsx +87 -0
  23. package/template/components/layout/PageLayout.tsx +89 -0
  24. package/template/components/layout/Sidebar.tsx +376 -0
  25. package/template/components/layout/TabBar.tsx +312 -0
  26. package/template/components/layout/TabBarSkeleton.tsx +12 -0
  27. package/template/components/layout/TabInitializer.tsx +99 -0
  28. package/template/components/layout/TableOfContents.tsx +138 -0
  29. package/template/components/markdown/CodeCopy.tsx +65 -0
  30. package/template/components/markdown/MarkdownContent.tsx +38 -0
  31. package/template/components/markdown/PageTransition.tsx +56 -0
  32. package/template/components/providers/UrlMapProvider.tsx +68 -0
  33. package/template/components/search/SearchDialog.tsx +286 -0
  34. package/template/components/search/SearchTrigger.tsx +41 -0
  35. package/template/content/guides/_meta.json +4 -0
  36. package/template/content/guides/writing.md +82 -0
  37. package/template/content/intro.md +29 -0
  38. package/template/eslintignore +7 -0
  39. package/template/eslintrc.js +40 -0
  40. package/template/gitignore +40 -0
  41. package/template/lib/basePath.test.ts +120 -0
  42. package/template/lib/basePath.ts +108 -0
  43. package/template/lib/cache.ts +36 -0
  44. package/template/lib/content/registry.ts +311 -0
  45. package/template/lib/content/resolver.ts +109 -0
  46. package/template/lib/graph/build.ts +214 -0
  47. package/template/lib/graph/layout.test.ts +189 -0
  48. package/template/lib/graph/layout.ts +247 -0
  49. package/template/lib/markdown/languages.test.ts +85 -0
  50. package/template/lib/markdown/languages.ts +103 -0
  51. package/template/lib/markdown/rehype-plugins.ts +240 -0
  52. package/template/lib/markdown/remark-wikilink.ts +141 -0
  53. package/template/lib/markdown/render.ts +175 -0
  54. package/template/lib/markdown/wikilink.test.ts +91 -0
  55. package/template/lib/markdown/wikilink.ts +85 -0
  56. package/template/lib/navigation/auto.ts +227 -0
  57. package/template/lib/navigation/builder.test.ts +129 -0
  58. package/template/lib/navigation/builder.ts +122 -0
  59. package/template/lib/navigation/hash.ts +32 -0
  60. package/template/lib/navigation/url.test.ts +88 -0
  61. package/template/lib/navigation/url.ts +108 -0
  62. package/template/lib/navigation/urlMap.ts +81 -0
  63. package/template/lib/payload/schema.ts +81 -0
  64. package/template/lib/payload/types.ts +105 -0
  65. package/template/lib/payload/validator.ts +56 -0
  66. package/template/lib/search/build.ts +204 -0
  67. package/template/lib/search/client.ts +190 -0
  68. package/template/lib/search/tokenizer.test.ts +60 -0
  69. package/template/lib/search/tokenizer.ts +83 -0
  70. package/template/lib/search/types.ts +40 -0
  71. package/template/lib/site.ts +86 -0
  72. package/template/lib/store/searchStore.ts +26 -0
  73. package/template/lib/store/tabStore.ts +313 -0
  74. package/template/next-env.d.ts +5 -0
  75. package/template/next.config.js +43 -0
  76. package/template/package-lock.json +9933 -0
  77. package/template/package.json +69 -0
  78. package/template/payload/config.ts +34 -0
  79. package/template/postcss.config.js +6 -0
  80. package/template/prettierignore +7 -0
  81. package/template/prettierrc +8 -0
  82. package/template/public/favicon.svg +10 -0
  83. package/template/public/fonts/Pretandard/Pretendard-Bold.woff2 +0 -0
  84. package/template/public/fonts/Pretandard/Pretendard-Regular.woff2 +0 -0
  85. package/template/public/fonts/Pretandard/Pretendard-SemiBold.woff2 +0 -0
  86. package/template/public/fonts/SUITE/SUITE-Bold.woff2 +0 -0
  87. package/template/public/fonts/SUITE/SUITE-Regular.woff2 +0 -0
  88. package/template/public/fonts/SUITE/SUITE-SemiBold.woff2 +0 -0
  89. package/template/public/images/.gitkeep +0 -0
  90. package/template/scripts/build-search-index.ts +36 -0
  91. package/template/scripts/check-links.ts +40 -0
  92. package/template/scripts/show-urls.ts +48 -0
  93. package/template/scripts/validate-payload.ts +30 -0
  94. package/template/styles/markdown.css +167 -0
  95. package/template/styles/theme.css +65 -0
  96. package/template/tailwind.config.ts +156 -0
  97. package/template/tsconfig.json +32 -0
  98. package/template/vitest.config.ts +30 -0
@@ -0,0 +1,299 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { usePathname, useRouter } from 'next/navigation';
6
+ import { NavigationItem } from '@/lib/payload/types';
7
+ import { useTabStore } from '@/lib/store/tabStore';
8
+ import { useUrlMap } from '@/components/providers/UrlMapProvider';
9
+ import { filterHiddenItems } from '@/lib/navigation/builder';
10
+
11
+ /**
12
+ * Props for the MobileMenu component
13
+ */
14
+ interface MobileMenuProps {
15
+ /** Array of top-level navigation items */
16
+ navigation: NavigationItem[];
17
+ /** Whether the mobile menu is currently open */
18
+ isOpen: boolean;
19
+ /** Callback function to close the menu */
20
+ onClose: () => void;
21
+ }
22
+
23
+ /**
24
+ * Props for the MobileNavigationItem component
25
+ */
26
+ interface MobileNavigationItemProps {
27
+ /** Navigation item to render */
28
+ item: NavigationItem;
29
+ /** Current page path for highlighting active item */
30
+ currentPath: string;
31
+ /** Nesting level for indentation (0 = top level) */
32
+ level: number;
33
+ /** Callback function when navigation occurs */
34
+ onNavigate: () => void;
35
+ /** Background color inherited from parent */
36
+ backgroundColor?: string;
37
+ }
38
+
39
+ /**
40
+ * Calculate luminance of a color to determine if it's light or dark
41
+ */
42
+ function isLightColor(hexColor: string): boolean {
43
+ const hex = hexColor.replace('#', '');
44
+ const r = parseInt(hex.substring(0, 2), 16);
45
+ const g = parseInt(hex.substring(2, 4), 16);
46
+ const b = parseInt(hex.substring(4, 6), 16);
47
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
48
+ return luminance > 0.5;
49
+ }
50
+
51
+ /**
52
+ * Renders a single navigation item optimized for mobile with touch-friendly
53
+ * targets and automatic menu closing on navigation.
54
+ *
55
+ * @param props - Component props
56
+ */
57
+ function MobileNavigationItem({
58
+ item,
59
+ currentPath,
60
+ level,
61
+ onNavigate,
62
+ backgroundColor,
63
+ }: MobileNavigationItemProps) {
64
+ const [isExpanded, setIsExpanded] = useState(false);
65
+ const router = useRouter();
66
+ const { href: urlFor } = useUrlMap();
67
+ const { activeTabId, tabs, addTab } = useTabStore();
68
+ const hasChildren = item.children && item.children.length > 0;
69
+ const isActive = item.path === currentPath;
70
+
71
+ // Calculate left margin based on level using inline styles
72
+ const getLeftMarginStyle = () => {
73
+ if (level === 0) return {};
74
+ const baseMargin = level * 24; // 24px per level
75
+ const extraMargin = hasChildren ? 0 : 4; // Extra 4px for documents
76
+ return { marginLeft: `${baseMargin + extraMargin}px` };
77
+ };
78
+
79
+ // Use item's color if defined, otherwise inherit from parent
80
+ const bgColor = item.color || backgroundColor;
81
+
82
+ // Determine if background is light or dark
83
+ const isLight = bgColor ? isLightColor(bgColor) : true;
84
+
85
+ // Get text colors based on background
86
+ const textColor = isLight ? 'rgb(55, 65, 81)' : 'rgb(243, 244, 246)';
87
+
88
+ // Get background style
89
+ const getBgStyle = (isTopLevel: boolean) => {
90
+ if (!bgColor) return undefined;
91
+ return {
92
+ backgroundColor: bgColor,
93
+ ...(isTopLevel && { padding: '4px', borderRadius: '6px' }),
94
+ };
95
+ };
96
+
97
+ const handleToggle = () => {
98
+ if (hasChildren) {
99
+ setIsExpanded(!isExpanded);
100
+ }
101
+ };
102
+
103
+ const handleLinkClick = (e: React.MouseEvent) => {
104
+ if (item.path) {
105
+ e.preventDefault();
106
+ const { navigateInHistory } = useTabStore.getState();
107
+
108
+ if (activeTabId) {
109
+ // Add to history and update the active tab's path
110
+ navigateInHistory(activeTabId, item.path, item.name);
111
+ } else if (tabs.length === 0) {
112
+ // No tabs at all, create a new one
113
+ addTab({ title: item.name, path: item.path });
114
+ }
115
+
116
+ router.replace(urlFor(item.path));
117
+ onNavigate();
118
+ }
119
+ };
120
+
121
+ return (
122
+ <div className={level === 0 ? 'mb-0.5' : ''} style={level === 0 ? getBgStyle(true) : undefined}>
123
+ <div className="flex items-center" style={level > 0 ? getBgStyle(false) : undefined}>
124
+ {hasChildren ? (
125
+ <button
126
+ onClick={handleToggle}
127
+ className={`flex items-center flex-1 px-2 py-1 rounded-md text-sm transition-colors touch-manipulation ${
128
+ !bgColor
129
+ ? 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700'
130
+ : ''
131
+ }`}
132
+ style={bgColor ? { color: textColor, ...getLeftMarginStyle() } : getLeftMarginStyle()}
133
+ // No aria-label: this button already contains the section name, and
134
+ // labelling it "Expand" replaced that name rather than adding to it,
135
+ // leaving every section announced identically. `aria-expanded`
136
+ // carries the state on its own.
137
+ aria-expanded={isExpanded}
138
+ >
139
+ <svg
140
+ className={`w-4 h-4 mr-2 -ml-1 flex-shrink-0 transition-transform duration-[120ms] ${isExpanded ? 'rotate-90' : ''}`}
141
+ style={{ transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)' }}
142
+ fill="none"
143
+ stroke="currentColor"
144
+ viewBox="0 0 24 24"
145
+ >
146
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
147
+ </svg>
148
+ <span className="font-semibold">
149
+ {item.icon && <span className="mr-2">{item.icon}</span>}
150
+ {item.name}
151
+ </span>
152
+ </button>
153
+ ) : item.path ? (
154
+ <Link
155
+ href={urlFor(item.path)}
156
+ onClick={handleLinkClick}
157
+ className={`flex-1 px-2 py-1 rounded-md text-sm transition-colors touch-manipulation ${
158
+ isActive
159
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100 font-medium'
160
+ : !bgColor
161
+ ? 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700'
162
+ : ''
163
+ }`}
164
+ style={
165
+ bgColor && !isActive
166
+ ? { color: textColor, ...getLeftMarginStyle() }
167
+ : getLeftMarginStyle()
168
+ }
169
+ >
170
+ {item.icon && <span className="mr-2">{item.icon}</span>}
171
+ {item.name}
172
+ </Link>
173
+ ) : (
174
+ <div
175
+ className={`flex-1 px-2 py-1 text-sm font-semibold ${
176
+ !bgColor ? 'text-gray-900 dark:text-gray-100' : ''
177
+ }`}
178
+ style={bgColor ? { color: textColor, ...getLeftMarginStyle() } : getLeftMarginStyle()}
179
+ >
180
+ {item.icon && <span className="mr-2">{item.icon}</span>}
181
+ {item.name}
182
+ </div>
183
+ )}
184
+ </div>
185
+ {hasChildren && (
186
+ <div
187
+ className="overflow-hidden transition-all duration-[120ms]"
188
+ style={{
189
+ maxHeight: isExpanded ? '1000px' : '0',
190
+ transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
191
+ ...getBgStyle(false),
192
+ }}
193
+ >
194
+ {item.children!.map((child, index) => (
195
+ <MobileNavigationItem
196
+ key={`${child.name}-${index}`}
197
+ item={child}
198
+ currentPath={currentPath}
199
+ level={level + 1}
200
+ onNavigate={onNavigate}
201
+ backgroundColor={bgColor}
202
+ />
203
+ ))}
204
+ </div>
205
+ )}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Displays a full-screen overlay menu for mobile devices. The menu slides
212
+ * in from the left and includes a backdrop overlay. Automatically prevents
213
+ * body scrolling when open and closes when a navigation link is clicked.
214
+ *
215
+ * Visible only on mobile viewports (< 768px). On desktop, the Sidebar
216
+ * component is used instead.
217
+ *
218
+ * @param props - Component props
219
+ * @param props.navigation - Array of top-level navigation items to display
220
+ * @param props.isOpen - Whether the menu is currently open
221
+ * @param props.onClose - Callback function to close the menu
222
+ *
223
+ */
224
+ export function MobileMenu({ navigation, isOpen, onClose }: MobileMenuProps) {
225
+ const pathname = usePathname();
226
+ const { toPath } = useUrlMap();
227
+
228
+ // Resolve through the URL map rather than trimming the pathname: the URL is
229
+ // a hash under one strategy and carries a trailing slash under the other, and
230
+ // neither form would ever equal a content path.
231
+ const currentPath = toPath(pathname) ?? '';
232
+
233
+ // Filter out hidden items
234
+ const visibleNavigation = filterHiddenItems(navigation);
235
+
236
+ // Prevent body scroll when menu is open
237
+ useEffect(() => {
238
+ if (isOpen) {
239
+ document.body.style.overflow = 'hidden';
240
+ } else {
241
+ document.body.style.overflow = 'unset';
242
+ }
243
+ return () => {
244
+ document.body.style.overflow = 'unset';
245
+ };
246
+ }, [isOpen]);
247
+
248
+ return (
249
+ <>
250
+ {isOpen && (
251
+ <div
252
+ className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden touch-manipulation"
253
+ onClick={onClose}
254
+ onTouchEnd={onClose}
255
+ aria-hidden="true"
256
+ role="button"
257
+ tabIndex={-1}
258
+ />
259
+ )}
260
+
261
+ <div
262
+ className={`fixed top-0 left-0 h-full w-80 max-w-[85vw] bg-white dark:bg-gray-900 z-50 transform transition-transform duration-300 ease-in-out md:hidden ${
263
+ isOpen ? 'translate-x-0' : '-translate-x-full'
264
+ } overflow-y-auto`}
265
+ >
266
+ <div className="p-4">
267
+ <nav>
268
+ <div className="flex items-center justify-between mb-1">
269
+ <div className="flex-1" />
270
+ <button
271
+ onClick={onClose}
272
+ className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 active:bg-gray-100 dark:active:bg-gray-800 rounded-md transition-colors touch-manipulation"
273
+ aria-label="Close menu"
274
+ >
275
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
276
+ <path
277
+ strokeLinecap="round"
278
+ strokeLinejoin="round"
279
+ strokeWidth={2}
280
+ d="M6 18L18 6M6 6l12 12"
281
+ />
282
+ </svg>
283
+ </button>
284
+ </div>
285
+ {visibleNavigation.map((item, index) => (
286
+ <MobileNavigationItem
287
+ key={`${item.name}-${index}`}
288
+ item={item}
289
+ currentPath={currentPath}
290
+ level={0}
291
+ onNavigate={onClose}
292
+ />
293
+ ))}
294
+ </nav>
295
+ </div>
296
+ </div>
297
+ </>
298
+ );
299
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
6
+ import { useTabStore } from '@/lib/store/tabStore';
7
+ import { NavigationItem } from '@/lib/payload/types';
8
+ import { Breadcrumb } from './Breadcrumb';
9
+ import { useUrlMap } from '@/components/providers/UrlMapProvider';
10
+
11
+ interface NavigationButtonsProps {
12
+ navigation: NavigationItem[];
13
+ }
14
+
15
+ /**
16
+ * Navigation buttons for back/forward history within tabs, combined with breadcrumb
17
+ */
18
+ export function NavigationButtons({ navigation }: NavigationButtonsProps) {
19
+ const router = useRouter();
20
+ const { href: urlFor } = useUrlMap();
21
+ const { activeTabId, goBack, goForward, canGoBack, canGoForward } = useTabStore();
22
+
23
+ const canNavigateBack = activeTabId ? canGoBack(activeTabId) : false;
24
+ const canNavigateForward = activeTabId ? canGoForward(activeTabId) : false;
25
+
26
+ const handleBack = () => {
27
+ if (!activeTabId) return;
28
+
29
+ const result = goBack(activeTabId);
30
+ if (result) {
31
+ router.replace(urlFor(result.path));
32
+ }
33
+ };
34
+
35
+ const handleForward = () => {
36
+ if (!activeTabId) return;
37
+
38
+ const result = goForward(activeTabId);
39
+ if (result) {
40
+ router.replace(urlFor(result.path));
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="flex items-center gap-2 px-2 py-1.5 border-b border-gray-200 dark:border-gray-800 h-9 md:h-auto bg-white dark:bg-gray-950">
46
+ <div className="flex items-center gap-1">
47
+ <button
48
+ onClick={handleBack}
49
+ disabled={!canNavigateBack}
50
+ className={`
51
+ p-1 rounded-md transition-colors
52
+ ${
53
+ canNavigateBack
54
+ ? 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
55
+ : 'text-gray-300 dark:text-gray-700 cursor-not-allowed'
56
+ }
57
+ `}
58
+ aria-label="Go back"
59
+ title="Go back"
60
+ >
61
+ <ArrowLeft className="w-4 h-4" />
62
+ </button>
63
+ <button
64
+ onClick={handleForward}
65
+ disabled={!canNavigateForward}
66
+ className={`
67
+ p-1 rounded-md transition-colors
68
+ ${
69
+ canNavigateForward
70
+ ? 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
71
+ : 'text-gray-300 dark:text-gray-700 cursor-not-allowed'
72
+ }
73
+ `}
74
+ aria-label="Go forward"
75
+ title="Go forward"
76
+ >
77
+ <ArrowRight className="w-4 h-4" />
78
+ </button>
79
+ </div>
80
+
81
+ {/* Breadcrumb - center */}
82
+ <div className="flex-1 flex justify-center min-w-0">
83
+ <Breadcrumb navigation={navigation} />
84
+ </div>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { NavigationItem } from '@/lib/payload/types';
5
+ import { Sidebar } from './Sidebar';
6
+ import { MobileMenu } from './MobileMenu';
7
+ import { TabBar } from './TabBar';
8
+ import { NavigationButtons } from './NavigationButtons';
9
+ import { ThemeToggle } from '@/components/ThemeToggle';
10
+ import { SearchTrigger } from '@/components/search/SearchTrigger';
11
+
12
+ /**
13
+ * Props for the PageLayout component
14
+ */
15
+ interface PageLayoutProps {
16
+ /** Array of top-level navigation items */
17
+ navigation: NavigationItem[];
18
+ /** Page content to render in the main area */
19
+ children: React.ReactNode;
20
+ }
21
+
22
+ /**
23
+ * Main page layout wrapper with responsive navigation
24
+ * Manages the mobile menu open/close state internally.
25
+ *
26
+ * @param props - Component props
27
+ * @param props.navigation - Array of navigation items to display in sidebar/menu
28
+ * @param props.children - Page content to render in the main content area
29
+ *
30
+ */
31
+ export function PageLayout({ navigation, children }: PageLayoutProps) {
32
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
33
+
34
+ const toggleMobileMenu = () => {
35
+ setIsMobileMenuOpen(!isMobileMenuOpen);
36
+ };
37
+
38
+ const closeMobileMenu = () => {
39
+ setIsMobileMenuOpen(false);
40
+ };
41
+
42
+ return (
43
+ <div className="min-h-screen bg-white dark:bg-gray-950">
44
+ <header className="md:hidden sticky top-0 z-30 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-4 py-3">
45
+ <div className="flex items-center justify-between">
46
+ <button
47
+ onClick={toggleMobileMenu}
48
+ className="p-2 -ml-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors active:bg-gray-200 dark:active:bg-gray-700 touch-manipulation"
49
+ aria-label="Toggle menu"
50
+ aria-expanded={isMobileMenuOpen}
51
+ >
52
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ strokeWidth={2}
57
+ d="M4 6h16M4 12h16M4 18h16"
58
+ />
59
+ </svg>
60
+ </button>
61
+ <SearchTrigger className="mx-3 min-w-0 flex-1" />
62
+ <ThemeToggle />
63
+ </div>
64
+ </header>
65
+
66
+ <MobileMenu navigation={navigation} isOpen={isMobileMenuOpen} onClose={closeMobileMenu} />
67
+
68
+ <div className="flex">
69
+ <Sidebar navigation={navigation} />
70
+
71
+ <main id="main-content" tabIndex={-1} className="flex-1 min-w-0 flex flex-col">
72
+ <div className="sticky top-0 z-20 bg-white dark:bg-gray-950">
73
+ <TabBar />
74
+ <NavigationButtons navigation={navigation} />
75
+ </div>
76
+
77
+ <div className="flex-1 overflow-y-auto">
78
+ {/* Wide enough to seat the table-of-contents rail beside the
79
+ article; the article itself stays measured by its own prose
80
+ width. */}
81
+ <div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-12">
82
+ {children}
83
+ </div>
84
+ </div>
85
+ </main>
86
+ </div>
87
+ </div>
88
+ );
89
+ }