diff --git a/package-lock.json b/package-lock.json
index 74e165a..5249800 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@tailwindcss/postcss": "^4.2.1",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-virtual": "^3.13.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -4915,6 +4916,23 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.13.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.19.tgz",
+ "integrity": "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
@@ -4928,6 +4946,16 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.13.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz",
+ "integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
diff --git a/package.json b/package.json
index 0ae8245..b59b581 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@tailwindcss/postcss": "^4.2.1",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-virtual": "^3.13.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
index bf4dd68..7bb7c6a 100644
--- a/src/app/(app)/layout.tsx
+++ b/src/app/(app)/layout.tsx
@@ -2,7 +2,7 @@ import { Suspense } from "react";
import { Sidebar, MobileSidebar } from "@/components/layout/sidebar";
import { Topbar } from "@/components/layout/topbar";
import { QueryProvider } from "@/components/query-provider";
-import { CommandPalette } from "@/components/command-palette";
+import { LazyCommandPalette } from "@/components/lazy-command-palette";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
@@ -11,13 +11,13 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
-
+
{children}
-
+
);
}
diff --git a/src/app/(app)/projects/[projectId]/board/page.tsx b/src/app/(app)/projects/[projectId]/board/page.tsx
index f1916ab..9ff91ce 100644
--- a/src/app/(app)/projects/[projectId]/board/page.tsx
+++ b/src/app/(app)/projects/[projectId]/board/page.tsx
@@ -1,10 +1,15 @@
"use client";
+import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import { useDeliverables, useUpdateStageStatus } from "@/hooks/use-deliverables";
-import { KanbanBoard } from "@/components/views/kanban-board";
import { Skeleton } from "@/components/ui/skeleton";
+const KanbanBoard = dynamic(
+ () => import("@/components/views/kanban-board").then((m) => m.KanbanBoard),
+ { ssr: false }
+);
+
export default function BoardViewPage() {
const { projectId } = useParams<{ projectId: string }>();
const { data: deliverables, isLoading } = useDeliverables(projectId);
diff --git a/src/app/(app)/projects/[projectId]/timeline/page.tsx b/src/app/(app)/projects/[projectId]/timeline/page.tsx
index f2ea743..0a2abf2 100644
--- a/src/app/(app)/projects/[projectId]/timeline/page.tsx
+++ b/src/app/(app)/projects/[projectId]/timeline/page.tsx
@@ -1,10 +1,15 @@
"use client";
+import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import { useDeliverables } from "@/hooks/use-deliverables";
-import { GanttTimeline } from "@/components/views/gantt-timeline";
import { Skeleton } from "@/components/ui/skeleton";
+const GanttTimeline = dynamic(
+ () => import("@/components/views/gantt-timeline").then((m) => m.GanttTimeline),
+ { ssr: false }
+);
+
export default function TimelineViewPage() {
const { projectId } = useParams<{ projectId: string }>();
const { data: deliverables, isLoading } = useDeliverables(projectId);
diff --git a/src/app/error.tsx b/src/app/error.tsx
index fb247a6..a33800f 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -16,9 +16,9 @@ export default function GlobalError({
}, [error]);
return (
-
+
-
+
Something went wrong
diff --git a/src/app/globals.css b/src/app/globals.css
index 0604426..8a1fed2 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -114,4 +114,27 @@
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
}
+
+ /* Focus-visible ring for keyboard navigation */
+ :focus-visible {
+ @apply outline-2 outline-offset-2 outline-[var(--ring)];
+ }
+
+ /* Remove default focus outlines for mouse users */
+ :focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ /* Screen-reader only utility */
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+ }
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 5269a33..0c89d17 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -41,6 +41,12 @@ export default function RootLayout({
suppressHydrationWarning
>
+
+ Skip to main content
+
-
+
404
The page you're looking for doesn't exist.
diff --git a/src/components/deliverables/pipeline-progress.tsx b/src/components/deliverables/pipeline-progress.tsx
index 9d48be9..95f3373 100644
--- a/src/components/deliverables/pipeline-progress.tsx
+++ b/src/components/deliverables/pipeline-progress.tsx
@@ -26,7 +26,14 @@ export function PipelineProgress({ stages }: { stages: Stage[] }) {
).length;
return (
-
+
{sorted.map((stage) => (
diff --git a/src/components/layout/breadcrumbs.tsx b/src/components/layout/breadcrumbs.tsx
index 7004c8b..05125e5 100644
--- a/src/components/layout/breadcrumbs.tsx
+++ b/src/components/layout/breadcrumbs.tsx
@@ -32,18 +32,19 @@ export function Breadcrumbs() {
});
return (
-