@leanspec/ui 0.2.5-dev.20251120015656 → 0.2.5-dev.20251120022746
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/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/ui/.next/build-manifest.json +2 -2
- package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -3
- package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
- package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
- package/.next/{static/chunks/3fc323b284db714f.js → standalone/packages/ui/.next/static/chunks/c3d4d07de959ecf1.js} +1 -1
- package/.next/standalone/packages/ui/package.json +1 -1
- package/.next/standalone/packages/ui/src/app/globals.css +27 -0
- package/.next/standalone/packages/ui/src/components/spec-detail-client.tsx +71 -27
- package/.next/standalone/packages/ui/src/components/table-of-contents.tsx +56 -37
- package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
- package/.next/{standalone/packages/ui/.next/static/chunks/3fc323b284db714f.js → static/chunks/c3d4d07de959ecf1.js} +1 -1
- package/package.json +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/cff894805981b496.css +0 -1
- package/.next/static/chunks/cff894805981b496.css +0 -1
- /package/.next/standalone/packages/ui/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
- /package/.next/standalone/packages/ui/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/packages/ui/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
- /package/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
- /package/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{aJb7D9JtV_zRcRPZ0yHNE → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
|
@@ -538,4 +538,31 @@ html.changing-theme * {
|
|
|
538
538
|
writing-mode: vertical-rl;
|
|
539
539
|
transform: rotate(180deg);
|
|
540
540
|
}
|
|
541
|
+
|
|
542
|
+
/* Auto-hiding scrollbar - visible only on hover */
|
|
543
|
+
.scrollbar-auto-hide {
|
|
544
|
+
scrollbar-width: thin;
|
|
545
|
+
scrollbar-color: transparent transparent;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.scrollbar-auto-hide:hover {
|
|
549
|
+
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.scrollbar-auto-hide::-webkit-scrollbar {
|
|
553
|
+
width: 4px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.scrollbar-auto-hide::-webkit-scrollbar-track {
|
|
557
|
+
background: transparent;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.scrollbar-auto-hide::-webkit-scrollbar-thumb {
|
|
561
|
+
background-color: transparent;
|
|
562
|
+
border-radius: 20px;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
|
|
566
|
+
background-color: hsl(var(--muted-foreground) / 0.3);
|
|
567
|
+
}
|
|
541
568
|
}
|
|
@@ -18,7 +18,7 @@ import { SpecTimeline } from '@/components/spec-timeline';
|
|
|
18
18
|
import { StatusBadge } from '@/components/status-badge';
|
|
19
19
|
import { PriorityBadge } from '@/components/priority-badge';
|
|
20
20
|
import { MarkdownLink } from '@/components/markdown-link';
|
|
21
|
-
import { TableOfContents } from '@/components/table-of-contents';
|
|
21
|
+
import { TableOfContents, TableOfContentsSidebar } from '@/components/table-of-contents';
|
|
22
22
|
import { BackToTop } from '@/components/back-to-top';
|
|
23
23
|
import { SpecDependencyGraph } from '@/components/spec-dependency-graph';
|
|
24
24
|
import {
|
|
@@ -104,10 +104,10 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
|
|
|
104
104
|
|
|
105
105
|
// Fetch complete dependency graph when dialog opens
|
|
106
106
|
const { data: dependencyGraphData } = useSWR<{
|
|
107
|
-
current:
|
|
108
|
-
dependsOn:
|
|
109
|
-
requiredBy:
|
|
110
|
-
related:
|
|
107
|
+
current: { specName: string; specNumber?: number };
|
|
108
|
+
dependsOn: { specName: string; specNumber?: number }[];
|
|
109
|
+
requiredBy: { specName: string; specNumber?: number }[];
|
|
110
|
+
related: { specName: string; specNumber?: number }[];
|
|
111
111
|
}>(
|
|
112
112
|
dependenciesDialogOpen ? `/api/specs/${initialSpec.specNumber || initialSpec.id}/dependency-graph` : null,
|
|
113
113
|
fetcher,
|
|
@@ -118,7 +118,7 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
|
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
const spec = specData?.spec || initialSpec;
|
|
121
|
-
const tags = spec.tags || [];
|
|
121
|
+
const tags = React.useMemo(() => spec.tags || [], [spec.tags]);
|
|
122
122
|
const updatedRelative = spec.updatedAt ? formatRelativeTime(spec.updatedAt) : 'N/A';
|
|
123
123
|
const relationships = spec.relationships;
|
|
124
124
|
|
|
@@ -175,10 +175,45 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
|
|
|
175
175
|
router.push(newUrl, { scroll: false });
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
const headerRef = React.useRef<HTMLElement>(null);
|
|
179
|
+
|
|
180
|
+
// Handle scroll padding for sticky header
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
const updateScrollPadding = () => {
|
|
183
|
+
const navbarHeight = 56; // 3.5rem / top-14
|
|
184
|
+
let offset = navbarHeight;
|
|
185
|
+
|
|
186
|
+
// On large screens, the spec header is also sticky
|
|
187
|
+
if (window.innerWidth >= 1024 && headerRef.current) {
|
|
188
|
+
offset += headerRef.current.offsetHeight - navbarHeight;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
document.documentElement.style.scrollPaddingTop = `${offset}px`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Initial update
|
|
195
|
+
updateScrollPadding();
|
|
196
|
+
|
|
197
|
+
// Update on resize
|
|
198
|
+
window.addEventListener('resize', updateScrollPadding);
|
|
199
|
+
|
|
200
|
+
// Update when content changes (might affect header height if tags wrap)
|
|
201
|
+
const observer = new ResizeObserver(updateScrollPadding);
|
|
202
|
+
if (headerRef.current) {
|
|
203
|
+
observer.observe(headerRef.current);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return () => {
|
|
207
|
+
window.removeEventListener('resize', updateScrollPadding);
|
|
208
|
+
observer.disconnect();
|
|
209
|
+
document.documentElement.style.scrollPaddingTop = '';
|
|
210
|
+
};
|
|
211
|
+
}, [spec, tags]); // Re-run if spec metadata changes
|
|
212
|
+
|
|
178
213
|
return (
|
|
179
214
|
<>
|
|
180
215
|
{/* Compact Header - sticky on desktop, static on mobile */}
|
|
181
|
-
<header className="lg:sticky lg:top-14 lg:z-20 border-b bg-card">
|
|
216
|
+
<header ref={headerRef} className="lg:sticky lg:top-14 lg:z-20 border-b bg-card">
|
|
182
217
|
<div className="px-3 sm:px-6 py-3 sm:py-4">
|
|
183
218
|
{/* Line 1: Spec number + H1 Title */}
|
|
184
219
|
<h1 className="text-xl sm:text-2xl font-bold tracking-tight mb-2 sm:mb-3">
|
|
@@ -345,28 +380,37 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
|
|
|
345
380
|
)}
|
|
346
381
|
</header>
|
|
347
382
|
|
|
348
|
-
{/* Main content
|
|
349
|
-
<
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
383
|
+
{/* Main content with Sidebar */}
|
|
384
|
+
<div className="flex flex-col xl:flex-row xl:items-start">
|
|
385
|
+
<main className="flex-1 px-3 sm:px-6 py-4 sm:py-8 min-w-0">
|
|
386
|
+
<div className="space-y-6">
|
|
387
|
+
{isLoading && <div className="text-sm text-muted-foreground">Loading...</div>}
|
|
388
|
+
{error && <div className="text-sm text-destructive">Error loading spec</div>}
|
|
353
389
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
390
|
+
<article className="prose prose-slate dark:prose-invert max-w-none prose-sm sm:prose-base">
|
|
391
|
+
<ReactMarkdown
|
|
392
|
+
remarkPlugins={[remarkGfm, remarkStripHtmlComments]}
|
|
393
|
+
rehypePlugins={[rehypeHighlight, rehypeSlug]}
|
|
394
|
+
components={{
|
|
395
|
+
a: (props) => <MarkdownLink {...props} currentSpecNumber={spec.specNumber || undefined} />,
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
{displayContent}
|
|
399
|
+
</ReactMarkdown>
|
|
400
|
+
</article>
|
|
401
|
+
</div>
|
|
402
|
+
</main>
|
|
403
|
+
|
|
404
|
+
{/* Right Sidebar for TOC (Desktop only) */}
|
|
405
|
+
<aside className="hidden xl:block w-72 shrink-0 px-6 py-8 sticky top-40 h-[calc(100vh-10rem)] overflow-y-auto scrollbar-auto-hide">
|
|
406
|
+
<TableOfContentsSidebar content={displayContent} />
|
|
407
|
+
</aside>
|
|
408
|
+
</div>
|
|
367
409
|
|
|
368
|
-
{/* Floating action buttons */}
|
|
369
|
-
<
|
|
410
|
+
{/* Floating action buttons (Mobile/Tablet only) */}
|
|
411
|
+
<div className="xl:hidden">
|
|
412
|
+
<TableOfContents content={displayContent} />
|
|
413
|
+
</div>
|
|
370
414
|
<BackToTop />
|
|
371
415
|
</>
|
|
372
416
|
);
|
|
@@ -57,6 +57,60 @@ function extractHeadings(markdown: string): TOCItem[] {
|
|
|
57
57
|
return headings;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
interface TOCListProps {
|
|
61
|
+
headings: TOCItem[];
|
|
62
|
+
onHeadingClick: (id: string) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TOCList({ headings, onHeadingClick }: TOCListProps) {
|
|
66
|
+
return (
|
|
67
|
+
<nav className="space-y-1">
|
|
68
|
+
{headings.map((heading, index) => (
|
|
69
|
+
<button
|
|
70
|
+
key={`${heading.id}-${index}`}
|
|
71
|
+
onClick={() => onHeadingClick(heading.id)}
|
|
72
|
+
className={cn(
|
|
73
|
+
'w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-muted transition-colors flex items-start gap-2 group text-muted-foreground hover:text-foreground',
|
|
74
|
+
heading.level === 2 && 'font-medium text-foreground',
|
|
75
|
+
heading.level === 3 && 'pl-6',
|
|
76
|
+
heading.level === 4 && 'pl-10',
|
|
77
|
+
heading.level === 5 && 'pl-14',
|
|
78
|
+
heading.level === 6 && 'pl-18'
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span className="flex-1 truncate">{heading.text}</span>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</nav>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scrollToHeading(id: string) {
|
|
89
|
+
const element = document.getElementById(id);
|
|
90
|
+
if (element) {
|
|
91
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
92
|
+
|
|
93
|
+
if (window.history.replaceState) {
|
|
94
|
+
window.history.replaceState(null, '', `#${id}`);
|
|
95
|
+
} else {
|
|
96
|
+
window.location.hash = id;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function TableOfContentsSidebar({ content }: TableOfContentsProps) {
|
|
102
|
+
const headings = React.useMemo(() => extractHeadings(content), [content]);
|
|
103
|
+
|
|
104
|
+
if (headings.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="py-2">
|
|
108
|
+
<h4 className="mb-4 text-sm font-semibold leading-none tracking-tight px-2">On this page</h4>
|
|
109
|
+
<TOCList headings={headings} onHeadingClick={scrollToHeading} />
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
export function TableOfContents({ content }: TableOfContentsProps) {
|
|
61
115
|
const [open, setOpen] = React.useState(false);
|
|
62
116
|
const headings = React.useMemo(() => extractHeadings(content), [content]);
|
|
@@ -68,24 +122,7 @@ export function TableOfContents({ content }: TableOfContentsProps) {
|
|
|
68
122
|
setOpen(false);
|
|
69
123
|
// Small delay to allow dialog to close before scrolling
|
|
70
124
|
setTimeout(() => {
|
|
71
|
-
|
|
72
|
-
if (element) {
|
|
73
|
-
// Scroll with offset for sticky header (top navbar + spec header)
|
|
74
|
-
const headerOffset = 180; // Adjust based on your header height
|
|
75
|
-
const elementPosition = element.getBoundingClientRect().top;
|
|
76
|
-
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
77
|
-
|
|
78
|
-
window.scrollTo({
|
|
79
|
-
top: offsetPosition,
|
|
80
|
-
behavior: 'smooth'
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (window.history.replaceState) {
|
|
84
|
-
window.history.replaceState(null, '', `#${id}`);
|
|
85
|
-
} else {
|
|
86
|
-
window.location.hash = id;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
125
|
+
scrollToHeading(id);
|
|
89
126
|
}, 100);
|
|
90
127
|
};
|
|
91
128
|
|
|
@@ -105,25 +142,7 @@ export function TableOfContents({ content }: TableOfContentsProps) {
|
|
|
105
142
|
<DialogHeader>
|
|
106
143
|
<DialogTitle>Table of Contents</DialogTitle>
|
|
107
144
|
</DialogHeader>
|
|
108
|
-
<
|
|
109
|
-
{headings.map((heading, index) => (
|
|
110
|
-
<button
|
|
111
|
-
key={`${heading.id}-${index}`}
|
|
112
|
-
onClick={() => handleHeadingClick(heading.id)}
|
|
113
|
-
className={cn(
|
|
114
|
-
'w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-muted transition-colors flex items-start gap-2 group',
|
|
115
|
-
heading.level === 2 && 'font-medium',
|
|
116
|
-
heading.level === 3 && 'pl-6',
|
|
117
|
-
heading.level === 4 && 'pl-10',
|
|
118
|
-
heading.level === 5 && 'pl-14',
|
|
119
|
-
heading.level === 6 && 'pl-18'
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
<ChevronRight className="h-4 w-4 mt-0.5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
123
|
-
<span className="flex-1">{heading.text}</span>
|
|
124
|
-
</button>
|
|
125
|
-
))}
|
|
126
|
-
</nav>
|
|
145
|
+
<TOCList headings={headings} onHeadingClick={handleHeadingClick} />
|
|
127
146
|
</DialogContent>
|
|
128
147
|
</Dialog>
|
|
129
148
|
);
|