@leanspec/ui 0.2.3 → 0.2.4
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/dist/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom link component for ReactMarkdown that handles internal spec links
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { AnchorHTMLAttributes } from 'react';
|
|
7
|
+
|
|
8
|
+
interface MarkdownLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
9
|
+
href?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
currentSpecNumber?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Transform internal spec links to proper web app routes
|
|
16
|
+
* Examples:
|
|
17
|
+
* - ../048-spec-complexity-analysis/ -> /specs/48
|
|
18
|
+
* - ../048-spec-complexity-analysis/README.md -> /specs/48
|
|
19
|
+
* - ../048-spec-complexity-analysis/DESIGN.md -> /specs/48?subspec=DESIGN.md
|
|
20
|
+
* - ./DESIGN.md -> current spec with ?subspec=DESIGN.md
|
|
21
|
+
* - #heading -> #heading (anchor links unchanged)
|
|
22
|
+
* - https://... -> https://... (external links unchanged)
|
|
23
|
+
*/
|
|
24
|
+
function transformSpecLink(href: string, currentSpecNumber?: number): string {
|
|
25
|
+
// Don't transform anchor links or external URLs
|
|
26
|
+
if (href.startsWith('#') || href.startsWith('http://') || href.startsWith('https://')) {
|
|
27
|
+
return href;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Match same-directory sub-spec links: ./FILE.md
|
|
31
|
+
const sameDirectoryPattern = /^\.\/([^/]+\.md)$/;
|
|
32
|
+
const sameDirectoryMatch = href.match(sameDirectoryPattern);
|
|
33
|
+
|
|
34
|
+
if (sameDirectoryMatch && currentSpecNumber) {
|
|
35
|
+
const subSpecFile = sameDirectoryMatch[1];
|
|
36
|
+
if (subSpecFile !== 'README.md') {
|
|
37
|
+
return `/specs/${currentSpecNumber}?subspec=${subSpecFile}`;
|
|
38
|
+
} else {
|
|
39
|
+
return `/specs/${currentSpecNumber}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Match internal spec links: ../NNN-spec-name/ or ../NNN-spec-name/FILE.md
|
|
44
|
+
const specLinkPattern = /^\.\.\/(\d+)-[^/]+\/?([^/]+\.md)?$/;
|
|
45
|
+
const match = href.match(specLinkPattern);
|
|
46
|
+
|
|
47
|
+
if (match) {
|
|
48
|
+
// Convert to number to remove leading zeros (018 -> 18)
|
|
49
|
+
const specNumber = parseInt(match[1], 10);
|
|
50
|
+
const subSpecFile = match[2];
|
|
51
|
+
|
|
52
|
+
if (subSpecFile && subSpecFile !== 'README.md') {
|
|
53
|
+
// Sub-spec link (e.g., DESIGN.md, IMPLEMENTATION.md)
|
|
54
|
+
return `/specs/${specNumber}?subspec=${subSpecFile}`;
|
|
55
|
+
} else {
|
|
56
|
+
// Main spec link (README.md or just the directory)
|
|
57
|
+
return `/specs/${specNumber}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If no pattern matches, return original href
|
|
62
|
+
return href;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function MarkdownLink({ href, children, currentSpecNumber, ...props }: MarkdownLinkProps) {
|
|
66
|
+
if (!href) {
|
|
67
|
+
return <a {...props}>{children}</a>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const transformedHref = transformSpecLink(href, currentSpecNumber);
|
|
71
|
+
|
|
72
|
+
// External links or anchor links - use regular <a> tag
|
|
73
|
+
const isExternal = transformedHref.startsWith('http://') || transformedHref.startsWith('https://');
|
|
74
|
+
const isAnchor = transformedHref.startsWith('#');
|
|
75
|
+
|
|
76
|
+
if (isExternal || isAnchor) {
|
|
77
|
+
return (
|
|
78
|
+
<a
|
|
79
|
+
href={transformedHref}
|
|
80
|
+
{...props}
|
|
81
|
+
target={isExternal ? '_blank' : undefined}
|
|
82
|
+
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</a>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Internal spec links - use Next.js Link
|
|
90
|
+
return (
|
|
91
|
+
<Link href={transformedHref} {...props} className="text-primary hover:underline">
|
|
92
|
+
{children}
|
|
93
|
+
</Link>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { ChevronRight, Menu, BookOpen } from 'lucide-react';
|
|
7
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
8
|
+
import { QuickSearch } from '@/components/quick-search';
|
|
9
|
+
import { Button } from '@/components/ui/button';
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
11
|
+
|
|
12
|
+
interface Spec {
|
|
13
|
+
id: string
|
|
14
|
+
specNumber: string
|
|
15
|
+
title: string
|
|
16
|
+
status: string
|
|
17
|
+
priority: string
|
|
18
|
+
tags: string[]
|
|
19
|
+
createdAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NavigationProps {
|
|
23
|
+
specs: Spec[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BreadcrumbItem {
|
|
27
|
+
label: string;
|
|
28
|
+
href?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {
|
|
32
|
+
return (
|
|
33
|
+
<nav className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
34
|
+
{items.map((item, index) => (
|
|
35
|
+
<div key={index} className="flex items-center gap-1">
|
|
36
|
+
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
|
37
|
+
{item.href ? (
|
|
38
|
+
<Link
|
|
39
|
+
href={item.href}
|
|
40
|
+
className="hover:text-foreground transition-colors"
|
|
41
|
+
>
|
|
42
|
+
{item.label}
|
|
43
|
+
</Link>
|
|
44
|
+
) : (
|
|
45
|
+
<span className="text-foreground">{item.label}</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</nav>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
|
54
|
+
if (pathname === '/') {
|
|
55
|
+
return [{ label: 'Home' }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (pathname === '/stats') {
|
|
59
|
+
return [
|
|
60
|
+
{ label: 'Home', href: '/' },
|
|
61
|
+
{ label: 'Stats' }
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (pathname === '/specs' || pathname.startsWith('/specs?')) {
|
|
66
|
+
const searchParams = new URLSearchParams(pathname.split('?')[1] || '');
|
|
67
|
+
const view = searchParams.get('view');
|
|
68
|
+
const viewLabel = view === 'board' ? 'Board View' : 'List View';
|
|
69
|
+
return [
|
|
70
|
+
{ label: 'Home', href: '/' },
|
|
71
|
+
{ label: `Specs (${viewLabel})` }
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (pathname.startsWith('/specs/')) {
|
|
76
|
+
const specId = pathname.split('/')[2];
|
|
77
|
+
return [
|
|
78
|
+
{ label: 'Home', href: '/' },
|
|
79
|
+
{ label: 'Specs', href: '/specs' },
|
|
80
|
+
{ label: specId }
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (pathname === '/board') {
|
|
85
|
+
return [
|
|
86
|
+
{ label: 'Home', href: '/' },
|
|
87
|
+
{ label: 'Board' }
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [{ label: 'Home', href: '/' }];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Navigation({ specs }: NavigationProps) {
|
|
95
|
+
const pathname = usePathname();
|
|
96
|
+
const breadcrumbs = getBreadcrumbs(pathname);
|
|
97
|
+
|
|
98
|
+
const toggleSidebar = () => {
|
|
99
|
+
if (typeof window !== 'undefined' && window.toggleMainSidebar) {
|
|
100
|
+
window.toggleMainSidebar();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<header className="sticky top-0 z-50 w-full h-14 border-b border-border bg-background">
|
|
106
|
+
<div className="flex items-center justify-between h-full px-2 sm:px-4">
|
|
107
|
+
{/* Left: Mobile Menu + Logo + Breadcrumb */}
|
|
108
|
+
<div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
|
|
109
|
+
{/* Mobile hamburger menu */}
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="icon"
|
|
113
|
+
onClick={toggleSidebar}
|
|
114
|
+
className="lg:hidden h-9 w-9 shrink-0"
|
|
115
|
+
>
|
|
116
|
+
<Menu className="h-5 w-5" />
|
|
117
|
+
<span className="sr-only">Toggle menu</span>
|
|
118
|
+
</Button>
|
|
119
|
+
|
|
120
|
+
<Link href="/" className="flex items-center space-x-2 shrink-0">
|
|
121
|
+
<Image
|
|
122
|
+
src="/logo-with-bg.svg"
|
|
123
|
+
alt="LeanSpec"
|
|
124
|
+
width={32}
|
|
125
|
+
height={32}
|
|
126
|
+
className="h-7 w-7 sm:h-8 sm:w-8 dark:hidden"
|
|
127
|
+
/>
|
|
128
|
+
<Image
|
|
129
|
+
src="/logo-dark-bg.svg"
|
|
130
|
+
alt="LeanSpec"
|
|
131
|
+
width={32}
|
|
132
|
+
height={32}
|
|
133
|
+
className="h-7 w-7 sm:h-8 sm:w-8 hidden dark:block"
|
|
134
|
+
/>
|
|
135
|
+
<span className="font-bold text-lg sm:text-xl hidden sm:inline">LeanSpec</span>
|
|
136
|
+
</Link>
|
|
137
|
+
<div className="hidden md:block min-w-0">
|
|
138
|
+
<Breadcrumb items={breadcrumbs} />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Right: Search + Theme + Docs + GitHub */}
|
|
143
|
+
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
144
|
+
<QuickSearch specs={specs} />
|
|
145
|
+
<TooltipProvider>
|
|
146
|
+
<Tooltip>
|
|
147
|
+
<TooltipTrigger asChild>
|
|
148
|
+
<div>
|
|
149
|
+
<ThemeToggle />
|
|
150
|
+
</div>
|
|
151
|
+
</TooltipTrigger>
|
|
152
|
+
<TooltipContent>
|
|
153
|
+
<p>Toggle theme</p>
|
|
154
|
+
</TooltipContent>
|
|
155
|
+
</Tooltip>
|
|
156
|
+
|
|
157
|
+
<Tooltip>
|
|
158
|
+
<TooltipTrigger asChild>
|
|
159
|
+
<Button variant="ghost" size="icon" asChild className="h-9 w-9 sm:h-10 sm:w-10">
|
|
160
|
+
<a
|
|
161
|
+
href="https://www.lean-spec.dev"
|
|
162
|
+
target="_blank"
|
|
163
|
+
rel="noopener noreferrer"
|
|
164
|
+
aria-label="Documentation"
|
|
165
|
+
>
|
|
166
|
+
<BookOpen className="h-5 w-5" />
|
|
167
|
+
</a>
|
|
168
|
+
</Button>
|
|
169
|
+
</TooltipTrigger>
|
|
170
|
+
<TooltipContent>
|
|
171
|
+
<p>Documentation</p>
|
|
172
|
+
</TooltipContent>
|
|
173
|
+
</Tooltip>
|
|
174
|
+
|
|
175
|
+
<Tooltip>
|
|
176
|
+
<TooltipTrigger asChild>
|
|
177
|
+
<Button variant="ghost" size="icon" asChild className="h-9 w-9 sm:h-10 sm:w-10">
|
|
178
|
+
<a
|
|
179
|
+
href="https://github.com/codervisor/lean-spec"
|
|
180
|
+
target="_blank"
|
|
181
|
+
rel="noopener noreferrer"
|
|
182
|
+
aria-label="GitHub repository"
|
|
183
|
+
>
|
|
184
|
+
<Image
|
|
185
|
+
src="/github-mark-white.svg"
|
|
186
|
+
alt="GitHub"
|
|
187
|
+
width={20}
|
|
188
|
+
height={20}
|
|
189
|
+
className="hidden dark:block"
|
|
190
|
+
/>
|
|
191
|
+
<Image
|
|
192
|
+
src="/github-mark.svg"
|
|
193
|
+
alt="GitHub"
|
|
194
|
+
width={20}
|
|
195
|
+
height={20}
|
|
196
|
+
className="dark:hidden"
|
|
197
|
+
/>
|
|
198
|
+
</a>
|
|
199
|
+
</Button>
|
|
200
|
+
</TooltipTrigger>
|
|
201
|
+
<TooltipContent>
|
|
202
|
+
<p>GitHub repository</p>
|
|
203
|
+
</TooltipContent>
|
|
204
|
+
</Tooltip>
|
|
205
|
+
</TooltipProvider>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</header>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Priority badge component with icons
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface PriorityBadgeProps {
|
|
10
|
+
priority: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
iconOnly?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const priorityConfig = {
|
|
16
|
+
'critical': {
|
|
17
|
+
icon: AlertCircle,
|
|
18
|
+
label: 'Critical',
|
|
19
|
+
className: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
|
|
20
|
+
},
|
|
21
|
+
'high': {
|
|
22
|
+
icon: ArrowUp,
|
|
23
|
+
label: 'High',
|
|
24
|
+
className: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400'
|
|
25
|
+
},
|
|
26
|
+
'medium': {
|
|
27
|
+
icon: Minus,
|
|
28
|
+
label: 'Medium',
|
|
29
|
+
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'
|
|
30
|
+
},
|
|
31
|
+
'low': {
|
|
32
|
+
icon: ArrowDown,
|
|
33
|
+
label: 'Low',
|
|
34
|
+
className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function PriorityBadge({ priority, className, iconOnly = false }: PriorityBadgeProps) {
|
|
39
|
+
const config = priorityConfig[priority as keyof typeof priorityConfig] || priorityConfig.medium;
|
|
40
|
+
const Icon = config.icon;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Badge className={cn('flex items-center w-fit', !iconOnly && 'gap-1.5', config.className, className)}>
|
|
44
|
+
<Icon className="h-3.5 w-3.5" />
|
|
45
|
+
{!iconOnly && config.label}
|
|
46
|
+
</Badge>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getPriorityLabel(priority: string): string {
|
|
51
|
+
const config = priorityConfig[priority as keyof typeof priorityConfig] || priorityConfig.medium;
|
|
52
|
+
return config.label;
|
|
53
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { FileText, Search, Clock, Tag } from "lucide-react"
|
|
6
|
+
import Fuse from "fuse.js"
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CommandDialog,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
} from "@/components/ui/command"
|
|
16
|
+
import { StatusBadge } from "@/components/status-badge"
|
|
17
|
+
import { PriorityBadge } from "@/components/priority-badge"
|
|
18
|
+
|
|
19
|
+
interface Spec {
|
|
20
|
+
id: string
|
|
21
|
+
specNumber: string
|
|
22
|
+
title: string
|
|
23
|
+
status: string
|
|
24
|
+
priority: string
|
|
25
|
+
tags: string[]
|
|
26
|
+
createdAt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface QuickSearchProps {
|
|
30
|
+
specs: Spec[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function QuickSearch({ specs }: QuickSearchProps) {
|
|
34
|
+
const router = useRouter()
|
|
35
|
+
const [open, setOpen] = React.useState(false)
|
|
36
|
+
const [search, setSearch] = React.useState("")
|
|
37
|
+
const [recentSearches, setRecentSearches] = React.useState<string[]>([])
|
|
38
|
+
|
|
39
|
+
// Load recent searches from localStorage
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const saved = localStorage.getItem("leanspec-recent-searches")
|
|
42
|
+
if (saved) {
|
|
43
|
+
setRecentSearches(JSON.parse(saved))
|
|
44
|
+
}
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
// Setup keyboard shortcut
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
const down = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
setOpen((open) => !open)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.addEventListener("keydown", down)
|
|
57
|
+
return () => document.removeEventListener("keydown", down)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
// Setup fuzzy search
|
|
61
|
+
const fuse = React.useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
new Fuse(specs, {
|
|
64
|
+
keys: [
|
|
65
|
+
{ name: "title", weight: 2 },
|
|
66
|
+
{ name: "specNumber", weight: 1.5 },
|
|
67
|
+
{ name: "tags", weight: 0.5 },
|
|
68
|
+
],
|
|
69
|
+
threshold: 0.4,
|
|
70
|
+
includeScore: true,
|
|
71
|
+
minMatchCharLength: 2,
|
|
72
|
+
}),
|
|
73
|
+
[specs]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const results = React.useMemo(() => {
|
|
77
|
+
if (!search) return specs.slice(0, 8)
|
|
78
|
+
return fuse.search(search).map((result) => result.item)
|
|
79
|
+
}, [search, fuse, specs])
|
|
80
|
+
|
|
81
|
+
const handleSelect = (specId: string, title: string) => {
|
|
82
|
+
// Save to recent searches
|
|
83
|
+
const updated = [title, ...recentSearches.filter((s) => s !== title)].slice(0, 5)
|
|
84
|
+
setRecentSearches(updated)
|
|
85
|
+
localStorage.setItem("leanspec-recent-searches", JSON.stringify(updated))
|
|
86
|
+
|
|
87
|
+
// Navigate
|
|
88
|
+
router.push(`/specs/${specId}`)
|
|
89
|
+
setOpen(false)
|
|
90
|
+
setSearch("")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => setOpen(true)}
|
|
97
|
+
className="inline-flex items-center gap-2 px-2 sm:px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors border rounded-md hover:border-foreground/20"
|
|
98
|
+
>
|
|
99
|
+
<Search className="h-4 w-4" />
|
|
100
|
+
<span className="hidden sm:inline">Quick search...</span>
|
|
101
|
+
<kbd className="hidden md:inline-flex pointer-events-none h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100">
|
|
102
|
+
<span className="text-xs">⌘</span>K
|
|
103
|
+
</kbd>
|
|
104
|
+
</button>
|
|
105
|
+
|
|
106
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
107
|
+
<CommandInput
|
|
108
|
+
placeholder="Search specs by title, number, or tags..."
|
|
109
|
+
value={search}
|
|
110
|
+
onValueChange={setSearch}
|
|
111
|
+
/>
|
|
112
|
+
<CommandList>
|
|
113
|
+
<CommandEmpty>No specs found.</CommandEmpty>
|
|
114
|
+
|
|
115
|
+
{!search && recentSearches.length > 0 && (
|
|
116
|
+
<CommandGroup heading="Recent Searches">
|
|
117
|
+
{recentSearches.map((recent) => (
|
|
118
|
+
<CommandItem
|
|
119
|
+
key={recent}
|
|
120
|
+
value={recent}
|
|
121
|
+
onSelect={() => {
|
|
122
|
+
setSearch(recent)
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<Clock className="mr-2 h-4 w-4" />
|
|
126
|
+
{recent}
|
|
127
|
+
</CommandItem>
|
|
128
|
+
))}
|
|
129
|
+
</CommandGroup>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<CommandGroup heading="Specs">
|
|
133
|
+
{results.map((spec) => (
|
|
134
|
+
<CommandItem
|
|
135
|
+
key={spec.id}
|
|
136
|
+
value={`${spec.specNumber} ${spec.title}`}
|
|
137
|
+
onSelect={() => handleSelect(spec.id, spec.title)}
|
|
138
|
+
>
|
|
139
|
+
<FileText className="mr-2 h-4 w-4" />
|
|
140
|
+
<div className="flex-1 flex items-center justify-between gap-2">
|
|
141
|
+
<div className="flex-1 min-w-0">
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<span className="font-medium">#{spec.specNumber}</span>
|
|
144
|
+
<span className="truncate">{spec.title}</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
148
|
+
<StatusBadge status={spec.status} />
|
|
149
|
+
<PriorityBadge priority={spec.priority} />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</CommandItem>
|
|
153
|
+
))}
|
|
154
|
+
</CommandGroup>
|
|
155
|
+
|
|
156
|
+
{search && (
|
|
157
|
+
<CommandGroup heading="Filter by Tag">
|
|
158
|
+
{Array.from(new Set(specs.flatMap((s) => s.tags)))
|
|
159
|
+
.filter((tag) => tag.toLowerCase().includes(search.toLowerCase()))
|
|
160
|
+
.slice(0, 5)
|
|
161
|
+
.map((tag) => (
|
|
162
|
+
<CommandItem
|
|
163
|
+
key={tag}
|
|
164
|
+
value={tag}
|
|
165
|
+
onSelect={() => {
|
|
166
|
+
router.push(`/?tag=${encodeURIComponent(tag)}`)
|
|
167
|
+
setOpen(false)
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<Tag className="mr-2 h-4 w-4" />
|
|
171
|
+
Filter by: <span className="ml-1 font-medium">{tag}</span>
|
|
172
|
+
</CommandItem>
|
|
173
|
+
))}
|
|
174
|
+
</CommandGroup>
|
|
175
|
+
)}
|
|
176
|
+
</CommandList>
|
|
177
|
+
</CommandDialog>
|
|
178
|
+
</>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
2
|
+
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
|
3
|
+
|
|
4
|
+
export function SpecListSkeleton() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="space-y-4">
|
|
7
|
+
{[...Array(5)].map((_, i) => (
|
|
8
|
+
<Card key={i}>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<div className="flex items-start justify-between">
|
|
11
|
+
<div className="space-y-2 flex-1">
|
|
12
|
+
<Skeleton className="h-6 w-3/4" />
|
|
13
|
+
<Skeleton className="h-4 w-1/2" />
|
|
14
|
+
</div>
|
|
15
|
+
<Skeleton className="h-6 w-20" />
|
|
16
|
+
</div>
|
|
17
|
+
</CardHeader>
|
|
18
|
+
<CardContent>
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<Skeleton className="h-5 w-16" />
|
|
21
|
+
<Skeleton className="h-5 w-16" />
|
|
22
|
+
<Skeleton className="h-5 w-16" />
|
|
23
|
+
</div>
|
|
24
|
+
</CardContent>
|
|
25
|
+
</Card>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SpecDetailSkeleton() {
|
|
32
|
+
return (
|
|
33
|
+
<div className="container mx-auto py-8 px-4 max-w-6xl">
|
|
34
|
+
<div className="space-y-6">
|
|
35
|
+
{/* Breadcrumb */}
|
|
36
|
+
<Skeleton className="h-5 w-48" />
|
|
37
|
+
|
|
38
|
+
{/* Header */}
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
<Skeleton className="h-10 w-2/3" />
|
|
41
|
+
<Skeleton className="h-5 w-full" />
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Metadata section */}
|
|
45
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
46
|
+
{[...Array(4)].map((_, i) => (
|
|
47
|
+
<Card key={i}>
|
|
48
|
+
<CardContent className="p-4">
|
|
49
|
+
<Skeleton className="h-4 w-20 mb-2" />
|
|
50
|
+
<Skeleton className="h-6 w-24" />
|
|
51
|
+
</CardContent>
|
|
52
|
+
</Card>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Content */}
|
|
57
|
+
<Card>
|
|
58
|
+
<CardContent className="p-6 space-y-4">
|
|
59
|
+
<Skeleton className="h-8 w-1/3" />
|
|
60
|
+
<Skeleton className="h-4 w-full" />
|
|
61
|
+
<Skeleton className="h-4 w-full" />
|
|
62
|
+
<Skeleton className="h-4 w-4/5" />
|
|
63
|
+
<div className="py-4" />
|
|
64
|
+
<Skeleton className="h-8 w-1/4" />
|
|
65
|
+
<Skeleton className="h-4 w-full" />
|
|
66
|
+
<Skeleton className="h-4 w-full" />
|
|
67
|
+
<Skeleton className="h-4 w-3/4" />
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function StatsCardSkeleton() {
|
|
76
|
+
return (
|
|
77
|
+
<Card className="relative overflow-hidden">
|
|
78
|
+
<CardHeader className="pb-3">
|
|
79
|
+
<div className="flex items-center justify-between">
|
|
80
|
+
<Skeleton className="h-4 w-24" />
|
|
81
|
+
<Skeleton className="h-5 w-5 rounded-full" />
|
|
82
|
+
</div>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent>
|
|
85
|
+
<Skeleton className="h-8 w-16 mb-2" />
|
|
86
|
+
<Skeleton className="h-3 w-32" />
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function KanbanBoardSkeleton() {
|
|
93
|
+
return (
|
|
94
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
95
|
+
{[...Array(4)].map((_, i) => (
|
|
96
|
+
<div key={i} className="space-y-4">
|
|
97
|
+
<div className="flex items-center justify-between">
|
|
98
|
+
<Skeleton className="h-6 w-32" />
|
|
99
|
+
<Skeleton className="h-6 w-8" />
|
|
100
|
+
</div>
|
|
101
|
+
<div className="space-y-3">
|
|
102
|
+
{[...Array(3)].map((_, j) => (
|
|
103
|
+
<Card key={j}>
|
|
104
|
+
<CardContent className="p-4 space-y-2">
|
|
105
|
+
<Skeleton className="h-5 w-full" />
|
|
106
|
+
<Skeleton className="h-4 w-3/4" />
|
|
107
|
+
<div className="flex gap-2 pt-2">
|
|
108
|
+
<Skeleton className="h-5 w-16" />
|
|
109
|
+
<Skeleton className="h-5 w-16" />
|
|
110
|
+
</div>
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|