@leanspec/ui 0.2.5-dev.20251119062438 → 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.
Files changed (58) hide show
  1. package/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
  2. package/.next/standalone/packages/ui/.next/build-manifest.json +2 -2
  3. package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
  5. package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
  12. package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  15. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js.nft.json +1 -1
  20. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
  24. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b2fe773d._.js +3 -0
  25. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_e1889307._.js +1 -1
  26. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_specs_specs-client_tsx_0bb8f8f8._.js +1 -1
  27. package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
  28. package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
  29. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
  30. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
  31. package/.next/standalone/packages/ui/.next/static/chunks/093ea4b175adb770.js +1 -0
  32. package/.next/standalone/packages/ui/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
  33. package/.next/standalone/packages/ui/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
  34. package/.next/standalone/packages/ui/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
  35. package/.next/standalone/packages/ui/package.json +1 -1
  36. package/.next/standalone/packages/ui/src/app/globals.css +27 -0
  37. package/.next/standalone/packages/ui/src/app/specs/specs-client.tsx +249 -221
  38. package/.next/standalone/packages/ui/src/components/spec-detail-client.tsx +71 -27
  39. package/.next/standalone/packages/ui/src/components/table-of-contents.tsx +56 -37
  40. package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
  41. package/.next/static/chunks/093ea4b175adb770.js +1 -0
  42. package/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
  43. package/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
  44. package/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
  45. package/package.json +1 -1
  46. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__9d8d3fd6._.js +0 -3
  47. package/.next/standalone/packages/ui/.next/static/chunks/061e3819fd59154d.js +0 -1
  48. package/.next/standalone/packages/ui/.next/static/chunks/148ab58e68b383da.js +0 -1
  49. package/.next/standalone/packages/ui/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
  50. package/.next/static/chunks/061e3819fd59154d.js +0 -1
  51. package/.next/static/chunks/148ab58e68b383da.js +0 -1
  52. package/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
  53. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
  54. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
  55. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
  56. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
  57. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
  58. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
@@ -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: any;
108
- dependsOn: any[];
109
- requiredBy: any[];
110
- related: any[];
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 (full width) */}
349
- <main className="px-3 sm:px-6 py-4 sm:py-8">
350
- <div className="space-y-6">
351
- {isLoading && <div className="text-sm text-muted-foreground">Loading...</div>}
352
- {error && <div className="text-sm text-destructive">Error loading spec</div>}
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
- <article className="prose prose-slate dark:prose-invert max-w-none prose-sm sm:prose-base">
355
- <ReactMarkdown
356
- remarkPlugins={[remarkGfm, remarkStripHtmlComments]}
357
- rehypePlugins={[rehypeHighlight, rehypeSlug]}
358
- components={{
359
- a: (props) => <MarkdownLink {...props} currentSpecNumber={spec.specNumber || undefined} />,
360
- }}
361
- >
362
- {displayContent}
363
- </ReactMarkdown>
364
- </article>
365
- </div>
366
- </main>
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
- <TableOfContents content={displayContent} />
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
- const element = document.getElementById(id);
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
- <nav className="space-y-1">
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
  );