diff --git a/apps/web/package.json b/apps/web/package.json index 9319ea00..399fd44d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,26 +7,41 @@ "dev": "vite", "build": "tsc -b && vite build", "check-types": "tsc -b", - "sync-preset": "node ./scripts/sync-preset.mjs", - "test": "bun test", - "preview": "vite preview" + "test": "vitest run", + "preview": "vite preview", + "preset:sync": "node ./scripts/sync-preset.mjs" }, "dependencies": { + "@base-ui-components/react": "^1.0.0-rc.0", "@base-ui/react": "^1.3.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/inter": "^5.2.8", "@fontsource/geist-mono": "^5.2.7", + "@fontsource/instrument-serif": "^5.2.8", + "@fontsource/manrope": "^5.2.8", "@fontsource/nunito": "^5.2.7", + "@hugeicons/core-free-icons": "^4.0.0", + "@hugeicons/react": "^1.1.6", + "@nivo/bar": "^0.99.0", + "@nivo/core": "^0.99.0", "@orpc/client": "latest", "@orpc/contract": "latest", "@orpc/tanstack-query": "latest", "@rudel/api-routes": "workspace:*", + "@tabler/icons-react": "^3.40.0", "@tanstack/react-query": "^5.80.0", "@tanstack/react-table": "^8.21.3", "better-auth": "^1.5.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "dialkit": "^1.1.0", "html-to-image": "^1.11.13", "lucide-react": "^0.564.0", + "motion": "^12.38.0", "next-themes": "^0.4.6", "posthog-js": "^1.292.0", "radix-ui": "^1.4.3", @@ -39,22 +54,29 @@ "recharts": "^3.7.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.4.1", + "tailwind-merge": "^3.5.0", + "vaul": "^1.1.2", "zod": "^3.25.0" }, "devDependencies": { "@rudel/typescript-config": "workspace:*", "@tailwindcss/vite": "^4.1.0", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", "@types/node": "^22.0.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.1.1", + "agentation": "^2.3.3", "bun-types": "latest", - "shadcn": "^3.8.5", + "jsdom": "26.1.0", + "shadcn": "^4.1.0", "tailwindcss": "^4.1.0", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "3.2.4" } } diff --git a/apps/web/public/shell/team-lineup-stats-panel-noise.svg b/apps/web/public/shell/team-lineup-stats-panel-noise.svg new file mode 100644 index 00000000..a5b82f66 --- /dev/null +++ b/apps/web/public/shell/team-lineup-stats-panel-noise.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/scripts/sync-preset.mjs b/apps/web/scripts/sync-preset.mjs index 14e16b7a..f90eedd1 100644 --- a/apps/web/scripts/sync-preset.mjs +++ b/apps/web/scripts/sync-preset.mjs @@ -34,9 +34,9 @@ const registryComponents = [ const managedPackages = { dependencies: [ - "@base-ui/react", - "@fontsource/geist-mono", - "@fontsource/nunito", + "@fontsource-variable/inter", + "@hugeicons/core-free-icons", + "@hugeicons/react", "class-variance-authority", "clsx", "tailwind-merge", @@ -91,20 +91,10 @@ async function syncComponentsJson(sourcePath, destinationPath) { async function syncIndexCss(sourcePath, destinationPath) { let content = await fs.readFile(sourcePath, "utf8"); - - if (!content.includes('@import "./app/preset-extensions.css";')) { - content = content.replace( - '@import "shadcn/tailwind.css";\n', - '@import "shadcn/tailwind.css";\n@import "./app/preset-extensions.css";\n', - ); - } - - if (!content.includes('@import "./app/luma.css";')) { - content = content.replace( - '@import "./app/preset-extensions.css";\n', - '@import "./app/preset-extensions.css";\n@import "./app/luma.css";\n', - ); - } + content = content.replace( + '@import "@fontsource-variable/inter";\n', + '@import "@fontsource-variable/inter";\n@import "./app/preset-extensions.css";\n', + ); await fs.writeFile(destinationPath, content, "utf8"); } @@ -237,10 +227,7 @@ async function main() { const sourcePath = path.join(scaffoldRoot, "src", "app", "luma.css"); try { await fs.access(sourcePath); - await copyFileWithNormalization( - sourcePath, - path.join(appRoot, managedFile), - ); + await copyFileWithNormalization(sourcePath, path.join(appRoot, managedFile)); syncedFiles.push(managedFile); } catch { skippedFiles.push(`${managedFile} (not generated by preset)`); @@ -249,13 +236,7 @@ async function main() { } const fileName = path.basename(managedFile); - const sourcePath = path.join( - scaffoldRoot, - "src", - "components", - "ui", - fileName, - ); + const sourcePath = path.join(scaffoldRoot, "src", "components", "ui", fileName); const destinationPath = path.join(appRoot, managedFile); try { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0a057b77..c3ff08cd 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,15 +1,13 @@ import { useLocation } from "react-router-dom"; import { AppLoadingScreen } from "@/app/bootstrap/AppLoadingScreen"; -import { ProductAnalyticsSessionSync } from "@/features/analytics/tracking/ProductAnalyticsSessionSync"; +import { DeviceAuthorizationApp } from "@/features/auth/DeviceAuthorizationApp"; import { AuthenticatedApp } from "@/features/auth/AuthenticatedApp"; +import { GuestApp } from "@/features/auth/GuestApp"; import { getDeviceUserCode, getValidRedirect, - isResetPasswordPath, } from "@/features/auth/auth-route-utils"; -import { DeviceAuthorizationApp } from "@/features/auth/DeviceAuthorizationApp"; -import { GuestApp } from "@/features/auth/GuestApp"; -import { ResetPasswordApp } from "@/features/auth/ResetPasswordApp"; +import { ProductAnalyticsSessionSync } from "@/features/analytics/tracking/ProductAnalyticsSessionSync"; import { authClient } from "./lib/auth-client"; function App() { @@ -27,41 +25,20 @@ function App() { ); } - if (deviceUserCode) { - return ( - <> - + return ( + <> + + {deviceUserCode ? ( - - ); - } - - if (session) { - return ( - <> - + ) : session ? ( - - ); - } - - if (isResetPasswordPath(location.pathname)) { - return ( - <> - - - - ); - } - - return ( - <> - - + ) : ( + + )} ); } diff --git a/apps/web/src/DevTools.tsx b/apps/web/src/DevTools.tsx new file mode 100644 index 00000000..f5af019e --- /dev/null +++ b/apps/web/src/DevTools.tsx @@ -0,0 +1,5 @@ +import { Agentation } from "agentation"; + +export function DevTools() { + return ; +} diff --git a/apps/web/src/app/AppRouter.tsx b/apps/web/src/app/AppRouter.tsx index 6f135947..29735f4f 100644 --- a/apps/web/src/app/AppRouter.tsx +++ b/apps/web/src/app/AppRouter.tsx @@ -1,57 +1,167 @@ -import { Navigate, Route, Routes } from "react-router-dom"; -import { DashboardPage } from "@/features/dashboard/DashboardPage"; -import { AccountSettingsPage } from "@/features/settings/account/AccountSettingsPage"; -import { WorkspaceSettingsPage } from "@/features/settings/workspace/WorkspaceSettingsPage"; +import { type ComponentType, lazy, Suspense } from "react"; +import { Navigate, Route, Routes, useLocation } from "react-router-dom"; +import { NotFoundPage } from "@/app/system/NotFoundPage"; +import { AcceptInvitationPage } from "@/features/invitations/AcceptInvitationPage"; +import { settingsRouteMap } from "@/features/settings/config/settings-routes"; +import { SettingsIndexRedirect } from "@/features/settings/SettingsIndexRedirect"; import { AppShellLayout } from "@/features/shell/AppShellLayout"; -import { TeamPage } from "@/features/team/TeamPage"; -import { AcceptInvitationPage } from "@/pages/AcceptInvitationPage"; -import { AdminPage } from "@/pages/dashboard/AdminPage"; -import { CreateOrgPage } from "@/pages/dashboard/CreateOrgPage"; -import { DeveloperDetailPage } from "@/pages/dashboard/DeveloperDetailPage"; -import { DevelopersListPage } from "@/pages/dashboard/DevelopersListPage"; -import { ErrorsPage } from "@/pages/dashboard/ErrorsPage"; -import { InvitationsPage } from "@/pages/dashboard/InvitationsPage"; -import { LearningsPage } from "@/pages/dashboard/LearningsPage"; -import { ProjectDetailPage } from "@/pages/dashboard/ProjectDetailPage"; -import { ProjectsListPage } from "@/pages/dashboard/ProjectsListPage"; -import { ROIPage } from "@/pages/dashboard/ROIPage"; -import { SessionDetailPage } from "@/pages/dashboard/SessionDetailPage"; -import { SessionsListPage } from "@/pages/dashboard/SessionsListPage"; +import { shellRouteMap } from "@/features/shell/config/shell-routes"; +import { appendSidebarShellDebugParams } from "@/features/shell/config/sidebar-shell-debug"; + +function lazyNamed>( + loader: () => Promise, + exportName: keyof TModule, +) { + return lazy(async () => { + const module = await loader(); + return { + default: module[exportName] as ComponentType, + }; + }); +} + +const DashboardPage = lazyNamed( + () => import("@/features/dashboard/DashboardPage"), + "DashboardPage", +); +const SessionsListPage = lazyNamed( + () => import("@/pages/dashboard/SessionsListPage"), + "SessionsListPage", +); +const SessionDetailPage = lazyNamed( + () => import("@/pages/dashboard/SessionDetailPage"), + "SessionDetailPage", +); +const SettingsLayout = lazyNamed( + () => import("@/features/settings/SettingsLayout"), + "SettingsLayout", +); +const WorkspaceSettingsPage = lazyNamed( + () => import("@/features/settings/workspace/WorkspaceSettingsPage"), + "WorkspaceSettingsPage", +); +const AccountSettingsPage = lazyNamed( + () => import("@/features/settings/account/AccountSettingsPage"), + "AccountSettingsPage", +); +const TeamPage = lazyNamed( + () => import("@/features/team/TeamPage"), + "TeamPage", +); +const PresetBaselinePage = lazyNamed( + () => import("@/app/system/PresetBaselinePage"), + "PresetBaselinePage", +); +const LEGACY_DASHBOARDY_PATH = "/dashboardy"; + +function DashboardRouteLoadingScreen() { + return ( +
+
+
+
+
+
+
+
+

Loading…

+
+ ); +} + +function LazyRoute({ Component }: { Component: ComponentType }) { + return ( + }> + + + ); +} export function AppRouter({ rootRedirectTarget, }: { rootRedirectTarget: string | null; }) { + const location = useLocation(); + const rootRedirect = appendSidebarShellDebugParams( + rootRedirectTarget || shellRouteMap.dashboard.path, + new URLSearchParams(location.search), + ); + const canonicalWorkspaceSettingsPath = appendSidebarShellDebugParams( + settingsRouteMap.workspace.path, + new URLSearchParams(location.search), + ); + return ( - } - /> + } /> } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } + /> + }> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } /> + } + /> + + } + /> + } + /> + + } + /> + - } /> + } /> ); } diff --git a/apps/web/src/app/app-surface.css b/apps/web/src/app/app-surface.css new file mode 100644 index 00000000..d0fa3dfb --- /dev/null +++ b/apps/web/src/app/app-surface.css @@ -0,0 +1,803 @@ +@theme inline { + --color-dashboard-01-tone-blue: var(--dashboard-01-tone-blue); + --color-dashboard-01-tone-teal: var(--dashboard-01-tone-teal); + --color-dashboard-01-tone-orange: var(--dashboard-01-tone-orange); + --color-dashboard-01-tone-lime: var(--dashboard-01-tone-lime); + --color-dashboard-01-tone-violet: var(--dashboard-01-tone-violet); + --color-dashboard-01-tone-rose: var(--dashboard-01-tone-rose); + --color-dashboard-01-tone-slate: var(--dashboard-01-tone-slate); +} + +.dashboard-01-theme, +body:has(.dashboard-01-preview), +.dashboard-01-preview { + --dashboard-01-font-sans: var(--font-sans); + --dashboard-01-font-heading: var(--app-font-heading); + --dashboard-01-font-roster-display: var(--app-font-heading); + --dashboard-01-font-roster-mono: "Geist Mono", ui-monospace, monospace; + --dashboard-01-content-background: #fefefe; + --dashboard-01-rail-icon: #a4a7a7; + --dashboard-01-rail-icon-active: #121517; + --dashboard-01-rail-hover: rgba(0, 0, 0, 0.02); + --dashboard-01-avatar-background: #e6e7e8; + --dashboard-01-avatar-foreground: #6b6f72; + --dashboard-01-tone-blue: #2f5fe5; + --dashboard-01-tone-teal: #25b5aa; + --dashboard-01-tone-orange: #ef9a14; + --dashboard-01-tone-lime: #8abf4a; + --dashboard-01-tone-violet: #8c7cf3; + --dashboard-01-tone-rose: #f08ba7; + --dashboard-01-tone-slate: #94a3b8; + --dashboard-01-metric-button-border: rgba(217, 217, 217, 0.5); + --dashboard-01-metric-button-focus-ring: rgba(115, 130, 255, 0.6); + --dashboard-01-metric-button-focus-ring-offset: #f7f8f9; + --dashboard-01-metric-button-surface: linear-gradient( + 180deg, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.03) 100% + ); + --dashboard-01-metric-button-surface-selected: linear-gradient( + 180deg, + rgba(255, 255, 255, 1) 0%, + rgba(249, 249, 249, 1) 100% + ); + --dashboard-01-metric-button-shadow-selected: + 0 1px 3.1px rgba(0, 0, 0, 0.18), + 0 12px 24px -18px + var(--dashboard-01-metric-button-shadow-color, rgba(15, 23, 42, 0.18)), + inset 0 10px 5.5px rgba(255, 255, 255, 0.9); + --dashboard-01-insights-surface: rgba(255, 255, 255, 0.95); + --dashboard-01-insights-subsurface: rgba(244, 246, 248, 0.9); + --dashboard-01-insights-border: rgba(218, 223, 228, 0.78); + --dashboard-01-insights-track: rgba(255, 255, 255, 0.98); + --dashboard-01-insights-chip-surface: rgba(255, 255, 255, 0.9); + --dashboard-01-insights-chip-border: rgba(214, 219, 224, 0.9); + --dashboard-01-insights-chip-foreground: #16181c; + --dashboard-01-insights-meta: #6b7280; + --dashboard-01-insights-shadow: + 0 1px 3px rgba(15, 23, 42, 0.08), 0 18px 40px -28px rgba(15, 23, 42, 0.18); + --team-lineup-metric-picker-foreground: #303032; + --team-lineup-role-chart-main-l: 0.628; + --team-lineup-role-chart-main-c: 0.201; + --team-lineup-role-series-strong-l: 0.612; + --team-lineup-role-series-strong-c: 0.21; + --team-lineup-role-series-soft-l: 0.833; + --team-lineup-role-series-soft-c: 0.083; + --team-lineup-role-series-mid-l: 0.72; + --team-lineup-role-series-mid-c: 0.145; + --team-lineup-role-card-surface-l: 0.968; + --team-lineup-role-card-surface-c: 0.018; + --team-lineup-role-card-border-l: 0.812; + --team-lineup-role-card-border-c: 0.062; + --team-lineup-role-card-shadow-l: 0.628; + --team-lineup-role-card-shadow-c: 0.201; + --team-lineup-hue-output: 273.8; + --team-lineup-hue-quality: 250; + --team-lineup-hue-efficiency: 145; + --team-lineup-hue-speed: 28; + --team-lineup-hue-craft: 72; + --team-lineup-hue-consistency: 205; + --team-lineup-output-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-output) + ); + --team-lineup-output-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-output) + ); + --team-lineup-output-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-output) + ); + --team-lineup-output-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-output) + ); + --team-lineup-output-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-output) / + 0.42 + ); + --team-lineup-output-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-output) / + 0.46 + ); + --team-lineup-output-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-output) / + 0.18 + ); + --team-lineup-quality-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-quality) + ); + --team-lineup-quality-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-quality) + ); + --team-lineup-quality-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-quality) + ); + --team-lineup-quality-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-quality) + ); + --team-lineup-quality-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-quality) / + 0.42 + ); + --team-lineup-quality-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-quality) / + 0.46 + ); + --team-lineup-quality-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-quality) / + 0.18 + ); + --team-lineup-efficiency-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-efficiency) + ); + --team-lineup-efficiency-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-efficiency) + ); + --team-lineup-efficiency-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-efficiency) + ); + --team-lineup-efficiency-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-efficiency) + ); + --team-lineup-efficiency-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-efficiency) / + 0.42 + ); + --team-lineup-efficiency-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-efficiency) / + 0.46 + ); + --team-lineup-efficiency-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-efficiency) / + 0.18 + ); + --team-lineup-speed-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-speed) + ); + --team-lineup-speed-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-speed) + ); + --team-lineup-speed-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-speed) + ); + --team-lineup-speed-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-speed) + ); + --team-lineup-speed-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-speed) / + 0.42 + ); + --team-lineup-speed-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-speed) / + 0.46 + ); + --team-lineup-speed-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-speed) / + 0.18 + ); + --team-lineup-craft-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-craft) + ); + --team-lineup-craft-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-craft) + ); + --team-lineup-craft-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-craft) + ); + --team-lineup-craft-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-craft) + ); + --team-lineup-craft-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-craft) / + 0.42 + ); + --team-lineup-craft-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-craft) / + 0.46 + ); + --team-lineup-craft-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-craft) / + 0.18 + ); + --team-lineup-consistency-chart-main: oklch( + var(--team-lineup-role-chart-main-l) var(--team-lineup-role-chart-main-c) + var(--team-lineup-hue-consistency) + ); + --team-lineup-consistency-series-strong: oklch( + var(--team-lineup-role-series-strong-l) + var(--team-lineup-role-series-strong-c) var(--team-lineup-hue-consistency) + ); + --team-lineup-consistency-series-soft: oklch( + var(--team-lineup-role-series-soft-l) var(--team-lineup-role-series-soft-c) + var(--team-lineup-hue-consistency) + ); + --team-lineup-consistency-series-mid: oklch( + var(--team-lineup-role-series-mid-l) var(--team-lineup-role-series-mid-c) + var(--team-lineup-hue-consistency) + ); + --team-lineup-consistency-card-surface: oklch( + var(--team-lineup-role-card-surface-l) + var(--team-lineup-role-card-surface-c) var(--team-lineup-hue-consistency) / + 0.42 + ); + --team-lineup-consistency-card-border: oklch( + var(--team-lineup-role-card-border-l) var(--team-lineup-role-card-border-c) + var(--team-lineup-hue-consistency) / + 0.46 + ); + --team-lineup-consistency-card-shadow: oklch( + var(--team-lineup-role-card-shadow-l) var(--team-lineup-role-card-shadow-c) + var(--team-lineup-hue-consistency) / + 0.18 + ); + --team-lineup-card-grain-opacity: 0.17; + --team-lineup-card-grain-size: 110px; + --team-lineup-card-grain-contrast: 100%; + --team-lineup-featured-panel-base-angle: 135deg; + --team-lineup-featured-panel-base-edge-color: rgb(58, 58, 58); + --team-lineup-featured-panel-base-mid-color: rgb(42, 42, 42); + --team-lineup-featured-panel-base-center-color: rgb(26, 26, 26); + --team-lineup-featured-panel-base-gradient: linear-gradient( + var(--team-lineup-featured-panel-base-angle), + var(--team-lineup-featured-panel-base-edge-color) 0%, + var(--team-lineup-featured-panel-base-mid-color) 25%, + var(--team-lineup-featured-panel-base-center-color) 50%, + var(--team-lineup-featured-panel-base-mid-color) 75%, + var(--team-lineup-featured-panel-base-edge-color) 100% + ); + --team-lineup-featured-panel-wash-angle: 133.23deg; + --team-lineup-featured-panel-wash-color-1: rgba(255, 128, 128, 0.686275); + --team-lineup-featured-panel-wash-color-1-stop: 2.91%; + --team-lineup-featured-panel-wash-color-2: rgba(255, 237, 102, 0.584314); + --team-lineup-featured-panel-wash-color-2-stop: 41.18%; + --team-lineup-featured-panel-wash-color-3: rgba(51, 228, 255, 0.584314); + --team-lineup-featured-panel-wash-color-3-stop: 65.46%; + --team-lineup-featured-panel-wash-color-4: rgba(240, 117, 205, 0.686275); + --team-lineup-featured-panel-wash-color-4-stop: 96.73%; + --team-lineup-featured-panel-wash-opacity: 0.48; + --team-lineup-featured-panel-difference-opacity: 1; + --team-lineup-featured-panel-lattice-opacity: 0.4; + --team-lineup-featured-panel-noise-large-opacity: 0.3; + --team-lineup-featured-panel-noise-small-opacity: 0.5; + --team-lineup-featured-panel-noise-large-size: 200px; + --team-lineup-featured-panel-noise-small-size: 100px; + --team-lineup-featured-panel-color-wash: linear-gradient( + var(--team-lineup-featured-panel-wash-angle), + var(--team-lineup-featured-panel-wash-color-1) + var(--team-lineup-featured-panel-wash-color-1-stop), + var(--team-lineup-featured-panel-wash-color-2) + var(--team-lineup-featured-panel-wash-color-2-stop), + var(--team-lineup-featured-panel-wash-color-3) + var(--team-lineup-featured-panel-wash-color-3-stop), + var(--team-lineup-featured-panel-wash-color-4) + var(--team-lineup-featured-panel-wash-color-4-stop) + ); + --team-lineup-featured-panel-mask-angle: 135deg; + --team-lineup-featured-panel-mask-start-opacity: 0.58; + --team-lineup-featured-panel-mask-mid-opacity: 0.3; + --team-lineup-featured-panel-mask-tail-opacity: 0.06; + --team-lineup-featured-panel-mask-mid-stop: 32%; + --team-lineup-featured-panel-mask-tail-stop: 58%; + --team-lineup-featured-panel-mask-zero-stop: 84%; + --team-lineup-featured-panel-color-opacity-mask: linear-gradient( + var(--team-lineup-featured-panel-mask-angle), + rgba(0, 0, 0, var(--team-lineup-featured-panel-mask-start-opacity)) 0%, + rgba(0, 0, 0, var(--team-lineup-featured-panel-mask-mid-opacity)) + var(--team-lineup-featured-panel-mask-mid-stop), + rgba(0, 0, 0, var(--team-lineup-featured-panel-mask-tail-opacity)) + var(--team-lineup-featured-panel-mask-tail-stop), + rgba(0, 0, 0, 0) var(--team-lineup-featured-panel-mask-zero-stop) + ); + --dashboard-01-chrome-turbulence-opacity: 0.18; + --dashboard-01-chrome-noise-large-size: 130px; + --dashboard-01-chrome-noise-small-size: 136px; + --dashboard-01-chrome-turbulence-contrast: 190%; + --dashboard-01-chrome-turbulence-darkness: 0.8; + --dashboard-01-chrome-highlight-opacity: 0.15; + --dashboard-01-chrome-surface: #ffffff; + --dashboard-01-chrome-noise-texture: + url("/shell/team-lineup-stats-panel-noise.svg"), + url("/shell/team-lineup-stats-panel-noise.svg"); + --dashboard-01-window-shadow: 0 0 4px rgb(0 0 0 / 0.13); + --sidebar: var(--dashboard-01-chrome-surface); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + font-family: var(--dashboard-01-font-sans); +} + +.dashboard-01-preview { + position: relative; + isolation: isolate; + background: var(--sidebar); + color: var(--foreground); + min-height: 100vh; +} + +.dark .dashboard-01-theme, +.dark body:has(.dashboard-01-preview), +.dark .dashboard-01-preview { + --dashboard-01-content-background: oklch(0.205 0 0); + --dashboard-01-rail-icon: oklch(0.708 0 0); + --dashboard-01-rail-icon-active: oklch(0.985 0 0); + --dashboard-01-rail-hover: rgba(0, 0, 0, 0.02); + --dashboard-01-avatar-background: oklch(0.269 0 0); + --dashboard-01-avatar-foreground: oklch(0.708 0 0); + --dashboard-01-tone-blue: #6d8fff; + --dashboard-01-tone-teal: #4fd3c9; + --dashboard-01-tone-orange: #f6b34d; + --dashboard-01-tone-lime: #a0d35c; + --dashboard-01-tone-violet: #a99afc; + --dashboard-01-tone-rose: #f4a8bd; + --dashboard-01-tone-slate: #a5b4c5; + --dashboard-01-metric-button-border: rgba(255, 255, 255, 0.12); + --dashboard-01-metric-button-focus-ring: rgba(144, 156, 255, 0.55); + --dashboard-01-metric-button-focus-ring-offset: oklch(0.145 0 0); + --dashboard-01-metric-button-surface: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.04) 0%, + rgba(255, 255, 255, 0.01) 100% + ); + --dashboard-01-metric-button-surface-selected: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.16) 0%, + rgba(255, 255, 255, 0.08) 100% + ); + --dashboard-01-metric-button-shadow-selected: + 0 1px 3.1px rgba(0, 0, 0, 0.3), + 0 12px 24px -18px + var(--dashboard-01-metric-button-shadow-color, rgba(15, 23, 42, 0.28)), + inset 0 10px 5.5px rgba(255, 255, 255, 0.08); + --dashboard-01-insights-surface: rgba(37, 39, 42, 0.95); + --dashboard-01-insights-subsurface: rgba(55, 58, 62, 0.78); + --dashboard-01-insights-border: rgba(255, 255, 255, 0.12); + --dashboard-01-insights-track: rgba(18, 20, 23, 0.72); + --dashboard-01-insights-chip-surface: rgba(255, 255, 255, 0.06); + --dashboard-01-insights-chip-border: rgba(255, 255, 255, 0.14); + --dashboard-01-insights-chip-foreground: oklch(0.985 0 0); + --dashboard-01-insights-meta: oklch(0.708 0 0); + --dashboard-01-insights-shadow: + 0 1px 3px rgba(0, 0, 0, 0.32), 0 18px 40px -28px rgba(0, 0, 0, 0.55); + --team-lineup-metric-picker-foreground: oklch(0.985 0 0); + --dashboard-01-chrome-turbulence-opacity: 0.18; + --dashboard-01-chrome-turbulence-contrast: 190%; + --dashboard-01-chrome-turbulence-darkness: 0.8; + --dashboard-01-chrome-highlight-opacity: 0.15; + --dashboard-01-chrome-surface: oklch(0.205 0 0); + --dashboard-01-window-shadow: 0 0 4px rgb(0 0 0 / 0.13); + --sidebar: var(--dashboard-01-chrome-surface); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +html:has(.dashboard-01-preview), +body:has(.dashboard-01-preview) { + background: var(--dashboard-01-chrome-surface); +} + +body:has(.dashboard-01-preview) { + position: relative; + isolation: isolate; +} + +.dark .dashboard-01-preview { + background: var(--dashboard-01-chrome-surface); + color: var(--foreground); +} + +.dashboard-01-preview, +.dashboard-01-chrome-frame, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"] { + position: relative; + isolation: isolate; + background: var(--dashboard-01-chrome-surface); +} + +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"] { + overflow: hidden; + border-right-width: 0; +} + +.dashboard-01-chrome-sidebar[data-sidebar-news-promote-debug="true"][data-sidebar-news-card-active="true"] { + z-index: var(--sidebar-news-active-sidebar-z, 10); +} + +.dashboard-01-chrome-sidebar[data-sidebar-news-overflow-debug="true"][data-sidebar-news-card-active="true"] + [data-slot="sidebar-inner"], +.dashboard-01-chrome-sidebar[data-sidebar-news-overflow-debug="true"][data-sidebar-news-card-active="true"] + [data-slot="sidebar-inner"] + > div { + overflow: visible; +} + +.dashboard-01-preview[data-sidebar-news-hide-performance-chart-debug="true"][data-sidebar-news-card-active="true"] + [data-slot="dashboard-performance-chart-shell"] { + opacity: 0; + pointer-events: none; +} + +body:has(.dashboard-01-preview)::before, +body:has(.dashboard-01-preview)::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; +} + +.dashboard-01-window { + box-shadow: var(--dashboard-01-window-shadow); +} + +.team-lineup-metric-button { + display: inline-flex; + align-items: center; + gap: 0; + width: fit-content; + max-width: 100%; + align-self: flex-start; + border-radius: 9px; + border: 1px solid var(--dashboard-01-metric-button-border); + background: var(--dashboard-01-metric-button-surface); + padding: 8px 12px; + text-align: left; + color: var(--team-lineup-metric-picker-foreground); + box-shadow: + 0 1px 2px rgba(15, 23, 42, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.32); + transition: + border-color 200ms ease, + box-shadow 200ms ease, + transform 200ms ease, + background 200ms ease; +} + +.team-lineup-metric-button[data-selected="true"] { + border-color: color-mix( + in srgb, + var(--dashboard-01-metric-button-border) 72%, + white + ); + background: var(--dashboard-01-metric-button-surface-selected); + color: var(--team-lineup-metric-picker-foreground); + box-shadow: var(--dashboard-01-metric-button-shadow-selected); + transform: translateY(-0.5px); +} + +.team-lineup-metric-button:hover { + border-color: color-mix( + in srgb, + var(--dashboard-01-metric-button-border) 82%, + white + ); + box-shadow: + 0 1px 3px rgba(15, 23, 42, 0.08), + 0 10px 18px -18px rgba(15, 23, 42, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.42); +} + +.team-lineup-metric-button:focus-visible { + outline: none; + box-shadow: + var(--dashboard-01-metric-button-shadow-selected), + 0 0 0 2px var(--dashboard-01-metric-button-focus-ring), + 0 0 0 4px var(--dashboard-01-metric-button-focus-ring-offset); +} + +@media (prefers-reduced-motion: reduce) { + .team-lineup-metric-button { + transition: none; + } +} + +.team-lineup-metric-button__label { + display: block; + margin: 0; + font-family: var(--dashboard-01-font-sans); + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 16px; + letter-spacing: -0.03em; + color: inherit; + text-box-trim: trim-both; + text-box-edge: cap alphabetic; +} + +@utility dashboard-big-number { + font-family: var(--dashboard-01-font-roster-display); + font-style: normal; + font-weight: 800; + letter-spacing: -0.035em; + text-box-trim: trim-both; + text-box-edge: cap alphabetic; +} + +@layer components { + .dashboard-01-insights-root { + background: var(--dashboard-01-insights-surface); + border: 1px solid var(--dashboard-01-insights-border); + box-shadow: var(--dashboard-01-insights-shadow); + } + + .dashboard-01-insights-panel { + background: var(--dashboard-01-insights-subsurface); + border: 1px solid var(--dashboard-01-insights-border); + box-shadow: none; + } + + .dashboard-01-insights-track { + background: var(--dashboard-01-insights-track); + } + + .dashboard-01-insights-chip { + background: var(--dashboard-01-insights-chip-surface); + border-color: var(--dashboard-01-insights-chip-border); + color: var(--dashboard-01-insights-chip-foreground); + } + + .dashboard-01-insights-meta { + color: var(--dashboard-01-insights-meta); + } +} + +.dashboard-01-preview::before, +.dashboard-01-preview::after, +.dashboard-01-chrome-frame::before, +.dashboard-01-chrome-frame::after, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"]::before, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"]::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; +} + +body:has(.dashboard-01-preview)::before, +.dashboard-01-preview::before, +.dashboard-01-chrome-frame::before, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"]::before { + background-image: var(--dashboard-01-chrome-noise-texture); + background-position: + 0 0, + center; + background-repeat: repeat, repeat; + background-size: + var(--dashboard-01-chrome-noise-large-size) + var(--dashboard-01-chrome-noise-large-size), + var(--dashboard-01-chrome-noise-small-size) + var(--dashboard-01-chrome-noise-small-size); + opacity: var(--dashboard-01-chrome-turbulence-opacity); + mix-blend-mode: multiply; + filter: contrast(var(--dashboard-01-chrome-turbulence-contrast)) + brightness(var(--dashboard-01-chrome-turbulence-darkness)); +} + +body:has(.dashboard-01-preview)::after, +.dashboard-01-preview::after, +.dashboard-01-chrome-frame::after, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"]::after { + background-image: var(--dashboard-01-chrome-noise-texture); + background-position: + 0 0, + center; + background-repeat: repeat, repeat; + background-size: + var(--dashboard-01-chrome-noise-large-size) + var(--dashboard-01-chrome-noise-large-size), + var(--dashboard-01-chrome-noise-small-size) + var(--dashboard-01-chrome-noise-small-size); + opacity: var(--dashboard-01-chrome-highlight-opacity); + mix-blend-mode: screen; + filter: contrast(calc(var(--dashboard-01-chrome-turbulence-contrast) * 0.75)) + brightness(1.22); +} + +.dashboard-01-preview > *, +.dashboard-01-chrome-frame > *, +.dashboard-01-chrome-sidebar [data-slot="sidebar-inner"] > * { + position: relative; + z-index: 1; +} + +.team-lineup-featured-card::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + opacity: var(--team-lineup-card-grain-opacity); + mix-blend-mode: multiply; + filter: contrast(var(--team-lineup-card-grain-contrast)); + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); + background-size: var(--team-lineup-card-grain-size) + var(--team-lineup-card-grain-size); + background-position: center; +} + +.team-lineup-featured-card > * { + position: relative; + z-index: 1; +} + +body:has(.dashboard-01-preview) > * { + position: relative; + z-index: 1; +} + +.team-lineup-featured-media-panel { + position: relative; + isolation: isolate; + overflow: hidden; +} + +.team-lineup-featured-stats-panel { + position: relative; + isolation: isolate; + overflow: hidden; +} + +.team-lineup-featured-shared-surface { + position: absolute; + left: 0; + top: 0; + z-index: 0; + isolation: isolate; + pointer-events: none; + overflow: hidden; + transform-origin: top left; + background-image: var(--team-lineup-featured-panel-base-gradient); +} + +.team-lineup-featured-shared-surface::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + opacity: var(--team-lineup-featured-panel-wash-opacity); + z-index: 0; + background-image: var(--team-lineup-featured-panel-color-wash); + mix-blend-mode: color-dodge; + mask-image: var(--team-lineup-featured-panel-color-opacity-mask); + mask-repeat: no-repeat; + mask-size: 100% 100%; + -webkit-mask-image: var(--team-lineup-featured-panel-color-opacity-mask); + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; +} + +.team-lineup-featured-shared-surface::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: #ffffff; + opacity: var(--team-lineup-featured-panel-difference-opacity); + mix-blend-mode: difference; +} + +.team-lineup-featured-shared-surface__lattice { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 2; + opacity: var(--team-lineup-featured-panel-lattice-opacity); + mix-blend-mode: soft-light; + pointer-events: none; +} + +.team-lineup-featured-shared-surface__noise-large, +.team-lineup-featured-shared-surface__noise-small { + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + background-image: url("/shell/team-lineup-stats-panel-noise.svg"); + background-repeat: repeat; + mix-blend-mode: overlay; +} + +.team-lineup-featured-shared-surface__noise-large { + background-size: var(--team-lineup-featured-panel-noise-large-size) + var(--team-lineup-featured-panel-noise-large-size); + opacity: var(--team-lineup-featured-panel-noise-large-opacity); +} + +.team-lineup-featured-shared-surface__noise-small { + background-size: var(--team-lineup-featured-panel-noise-small-size) + var(--team-lineup-featured-panel-noise-small-size); + opacity: var(--team-lineup-featured-panel-noise-small-opacity); +} + +.team-lineup-featured-media-panel__content, +.team-lineup-featured-stats-panel__content { + position: relative; + z-index: 1; + height: 100%; +} + +.team-lineup-surface-scope[data-lineup-surface-debug="base"] + .team-lineup-featured-shared-surface::before, +.team-lineup-surface-scope[data-lineup-surface-debug="base"] + .team-lineup-featured-shared-surface::after, +.team-lineup-surface-scope[data-lineup-surface-debug="base"] + .team-lineup-featured-shared-surface__lattice, +.team-lineup-surface-scope[data-lineup-surface-debug="base"] + .team-lineup-featured-shared-surface__noise-large, +.team-lineup-surface-scope[data-lineup-surface-debug="base"] + .team-lineup-featured-shared-surface__noise-small { + opacity: 0; +} + +.team-lineup-surface-scope[data-lineup-surface-debug="wash"] + .team-lineup-featured-shared-surface::after, +.team-lineup-surface-scope[data-lineup-surface-debug="wash"] + .team-lineup-featured-shared-surface__lattice, +.team-lineup-surface-scope[data-lineup-surface-debug="wash"] + .team-lineup-featured-shared-surface__noise-large, +.team-lineup-surface-scope[data-lineup-surface-debug="wash"] + .team-lineup-featured-shared-surface__noise-small { + opacity: 0; +} + +.team-lineup-surface-scope[data-lineup-surface-debug="difference"] + .team-lineup-featured-shared-surface::before, +.team-lineup-surface-scope[data-lineup-surface-debug="difference"] + .team-lineup-featured-shared-surface__lattice, +.team-lineup-surface-scope[data-lineup-surface-debug="difference"] + .team-lineup-featured-shared-surface__noise-large, +.team-lineup-surface-scope[data-lineup-surface-debug="difference"] + .team-lineup-featured-shared-surface__noise-small { + opacity: 0; +} + +.team-lineup-surface-scope[data-lineup-surface-debug="pattern"] + .team-lineup-featured-shared-surface::before, +.team-lineup-surface-scope[data-lineup-surface-debug="pattern"] + .team-lineup-featured-shared-surface::after { + opacity: 0; +} diff --git a/apps/web/src/app/bootstrap/AppLoadingScreen.tsx b/apps/web/src/app/bootstrap/AppLoadingScreen.tsx index cc770cf9..37aff05d 100644 --- a/apps/web/src/app/bootstrap/AppLoadingScreen.tsx +++ b/apps/web/src/app/bootstrap/AppLoadingScreen.tsx @@ -1,5 +1,5 @@ export function AppLoadingScreen({ - message = "Loading...", + message = "Loading…", }: { message?: string; }) { diff --git a/apps/web/src/app/hooks/use-mobile.ts b/apps/web/src/app/hooks/use-mobile.ts new file mode 100644 index 00000000..54ca82c4 --- /dev/null +++ b/apps/web/src/app/hooks/use-mobile.ts @@ -0,0 +1,22 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; +const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`; + +function subscribe(callback: () => void) { + const mediaQuery = window.matchMedia(MOBILE_QUERY); + mediaQuery.addEventListener("change", callback); + return () => mediaQuery.removeEventListener("change", callback); +} + +function getSnapshot() { + return window.matchMedia(MOBILE_QUERY).matches; +} + +export function useIsMobile() { + return React.useSyncExternalStore( + subscribe, + getSnapshot, + () => false, + ); +} diff --git a/apps/web/src/app/hooks/useMountEffect.ts b/apps/web/src/app/hooks/useMountEffect.ts new file mode 100644 index 00000000..87e80cf1 --- /dev/null +++ b/apps/web/src/app/hooks/useMountEffect.ts @@ -0,0 +1,5 @@ +import { useEffect } from "react"; + +export function useMountEffect(effect: () => void | (() => void)) { + useEffect(effect, []); +} diff --git a/apps/web/src/app/preset-extensions.css b/apps/web/src/app/preset-extensions.css index bdfe2b0d..ed804e4d 100644 --- a/apps/web/src/app/preset-extensions.css +++ b/apps/web/src/app/preset-extensions.css @@ -33,7 +33,6 @@ :root { --app-font-heading: "Nunito", var(--font-sans); - --app-font-sans: var(--font-sans); --surface: #f7f8f9; --heading: oklch(0.145 0 0); --subheading: oklch(0.556 0 0); diff --git a/apps/web/src/app/providers/AppProviders.tsx b/apps/web/src/app/providers/AppProviders.tsx index bbd55696..93219037 100644 --- a/apps/web/src/app/providers/AppProviders.tsx +++ b/apps/web/src/app/providers/AppProviders.tsx @@ -1,15 +1,19 @@ -import { QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; -import { BrowserRouter } from "react-router-dom"; -import { queryClient } from "@/lib/query-client"; -import { ThemeProvider } from "@/providers/ThemeProvider"; +import { DateRangeProvider } from "@/features/analytics/date-range/DateRangeProvider"; +import { ChatwootBootstrap } from "@/features/support/chatwoot/ChatwootBootstrap"; +import { OrganizationProvider } from "@/features/workspace/organization/OrganizationProvider"; -export function AppProviders({ children }: { children: ReactNode }) { +type AppProvidersProps = { + children: ReactNode; +}; + +export function AppProviders({ children }: AppProvidersProps) { return ( - - - {children} - - + + + + {children} + + ); } diff --git a/apps/web/src/app/providers/ThemeProvider.tsx b/apps/web/src/app/providers/ThemeProvider.tsx new file mode 100644 index 00000000..b2079139 --- /dev/null +++ b/apps/web/src/app/providers/ThemeProvider.tsx @@ -0,0 +1,14 @@ +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/routes.ts b/apps/web/src/app/routes.ts new file mode 100644 index 00000000..d60352a4 --- /dev/null +++ b/apps/web/src/app/routes.ts @@ -0,0 +1,21 @@ +const DASHBOARD_PATH = "/dashboard"; +const TEAM_PATH = "/team"; +const SETTINGS_ROOT_PATH = "/settings"; +const SETTINGS_WORKSPACE_PATH = `${SETTINGS_ROOT_PATH}/workspace`; +const SETTINGS_INVITATIONS_PATH = `${SETTINGS_ROOT_PATH}/invitations`; +const SETTINGS_ACCOUNT_PATH = `${SETTINGS_ROOT_PATH}/account`; +const SETTINGS_CREATE_WORKSPACE_PATH = `${SETTINGS_ROOT_PATH}/create-workspace`; +const PRESET_BASELINE_PATH = "/__preset-baseline"; + +export const appRoutes = { + home: () => DASHBOARD_PATH, + dashboard: () => DASHBOARD_PATH, + team: () => TEAM_PATH, + settings: () => SETTINGS_ROOT_PATH, + settingsRoot: () => SETTINGS_ROOT_PATH, + presetBaseline: () => PRESET_BASELINE_PATH, + settingsWorkspace: () => SETTINGS_WORKSPACE_PATH, + settingsInvitations: () => SETTINGS_INVITATIONS_PATH, + settingsAccount: () => SETTINGS_ACCOUNT_PATH, + settingsCreateWorkspace: () => SETTINGS_CREATE_WORKSPACE_PATH, +}; diff --git a/apps/web/src/app/system/NotFoundPage.tsx b/apps/web/src/app/system/NotFoundPage.tsx new file mode 100644 index 00000000..9aa855cf --- /dev/null +++ b/apps/web/src/app/system/NotFoundPage.tsx @@ -0,0 +1,24 @@ +import { Link } from "react-router-dom"; +import { appRoutes } from "@/app/routes"; +import { buttonVariants } from "@/app/ui/button"; + +export function NotFoundPage() { + return ( +
+
+
+ 404 +
+

+ Page not found +

+

+ This route is no longer part of the reduced redesign surface. +

+ + Go to dashboard + +
+
+ ); +} diff --git a/apps/web/src/app/system/PresetBaselinePage.tsx b/apps/web/src/app/system/PresetBaselinePage.tsx new file mode 100644 index 00000000..5f3507c2 --- /dev/null +++ b/apps/web/src/app/system/PresetBaselinePage.tsx @@ -0,0 +1,326 @@ +import { addDays } from "date-fns"; +import { CalendarIcon, ChevronDownIcon, LayoutPanelTopIcon } from "lucide-react"; +import { useState } from "react"; +import type { DateRange } from "react-day-picker"; +import { Badge } from "@/app/ui/badge"; +import { Button } from "@/app/ui/button"; +import { Calendar } from "@/app/ui/calendar"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + CardFooter, +} from "@/app/ui/card"; +import { Checkbox } from "@/app/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/app/ui/dialog"; +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, +} from "@/app/ui/field"; +import { Input } from "@/app/ui/input"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/app/ui/dropdown-menu"; +import { + Popover, + PopoverContent, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "@/app/ui/popover"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/app/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/app/ui/sheet"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/ui/tabs"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/ui/tooltip"; + +export function PresetBaselinePage() { + const [assigneeEnabled, setAssigneeEnabled] = useState(true); + const [selectedRange, setSelectedRange] = useState({ + from: new Date(new Date().getFullYear(), 0, 20), + to: addDays(new Date(new Date().getFullYear(), 0, 20), 12), + }); + const [includeAlerts, setIncludeAlerts] = useState(true); + + return ( + +
+
+
+ Internal +

Preset baseline

+

+ This page renders only preset-managed Base UI primitives so baseline + drift is visible without the custom shell, sidebar, insights cards, + or team cards interfering. +

+
+ +
+ + + Controls + + Buttons, menus, tooltip, dialog, and sheet should match the + preset exactly. + + + +
+ + + + + +
+ +
+ + }> + Menu + + + + + Workspace + Rename + Duplicate + + + + setIncludeAlerts(Boolean(checked)) + } + > + Include alerts + + + + + + }> + + Quick view + + + + Popover surface + +

+ This should be the stock preset popover treatment. +

+
+
+ + + }> + Open dialog + + + + Dialog surface + + The modal should reflect the preset radius, padding, + shadow, and typography. + + + + + + + }> + Open sheet + + + + Sheet surface + + Side panels should inherit the preset generic layer. + + + + + + + }> + + + Tooltip baseline + +
+
+
+ + + + Forms + + Field, input, select, checkbox, and tabs should stay close to + the preset with Inter as the generic baseline. + + + + + + Workspace name + + + Generic form controls should look preset-managed, not + dashboard-specific. + + + + Owner + + + + + setAssigneeEnabled(Boolean(checked)) + } + /> + + Keep assignee notifications enabled + + + + + + + Overview + Activity + Members + + + + + Preset-managed tabs should sit on a generic surface. + + + + + + + Activity content placeholder. + + + + + + + Members content placeholder. + + + + + + +
+ +
+ + + Date range + + The calendar should read as the exact preset baseline, not a + custom transparent overlay. + + + + + + + + + + Supporting surfaces + + Badges, footer treatment, and small cards should match the + preset baseline. + + + +
+ Default + Secondary + Outline + Destructive +
+ + + Small card + + + Use this page to compare the product baseline against the + preset scaffold without shell chrome. + + + + + +
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/ui/AppToaster.tsx b/apps/web/src/app/ui/AppToaster.tsx new file mode 100644 index 00000000..eeffee36 --- /dev/null +++ b/apps/web/src/app/ui/AppToaster.tsx @@ -0,0 +1,42 @@ +import type { CSSProperties } from "react"; +import { useTheme } from "next-themes"; +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +export function AppToaster(props: ToasterProps) { + const { theme = "system" } = useTheme(); + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as CSSProperties + } + toastOptions={{ + classNames: { + toast: "cn-toast", + }, + }} + {...props} + /> + ); +} diff --git a/apps/web/src/app/ui/avatar.tsx b/apps/web/src/app/ui/avatar.tsx index 068d9400..e92a2f48 100644 --- a/apps/web/src/app/ui/avatar.tsx +++ b/apps/web/src/app/ui/avatar.tsx @@ -1,107 +1,107 @@ -import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; -import type * as React from "react"; +import * as React from "react" +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" function Avatar({ - className, - size = "default", - ...props + className, + size = "default", + ...props }: AvatarPrimitive.Root.Props & { - size?: "default" | "sm" | "lg"; + size?: "default" | "sm" | "lg" }) { - return ( - - ); + return ( + + ) } function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { - return ( - - ); + return ( + + ) } function AvatarFallback({ - className, - ...props + className, + ...props }: AvatarPrimitive.Fallback.Props) { - return ( - - ); + return ( + + ) } function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { - return ( - svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className, - )} - {...props} - /> - ); + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) } function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); + return ( +
+ ) } function AvatarGroupCount({ - className, - ...props + className, + ...props }: React.ComponentProps<"div">) { - return ( -
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", - className, - )} - {...props} - /> - ); + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) } export { - Avatar, - AvatarImage, - AvatarFallback, - AvatarGroup, - AvatarGroupCount, - AvatarBadge, -}; + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/apps/web/src/app/ui/badge.tsx b/apps/web/src/app/ui/badge.tsx index c316cf37..3deefd0d 100644 --- a/apps/web/src/app/ui/badge.tsx +++ b/apps/web/src/app/ui/badge.tsx @@ -1,52 +1,52 @@ -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const badgeVariants = cva( - "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: - "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: - "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", - outline: - "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: - "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) function Badge({ - className, - variant = "default", - render, - ...props + className, + variant = "default", + render, + ...props }: useRender.ComponentProps<"span"> & VariantProps) { - return useRender({ - defaultTagName: "span", - props: mergeProps<"span">( - { - className: cn(badgeVariants({ variant }), className), - }, - props, - ), - render, - state: { - slot: "badge", - variant, - }, - }); + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props + ), + render, + state: { + slot: "badge", + variant, + }, + }) } -export { Badge, badgeVariants }; +export { Badge, badgeVariants } diff --git a/apps/web/src/app/ui/button.tsx b/apps/web/src/app/ui/button.tsx index aa3d30f0..f8460633 100644 --- a/apps/web/src/app/ui/button.tsx +++ b/apps/web/src/app/ui/button.tsx @@ -1,58 +1,58 @@ -"use client"; +"use client" -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; +import { Button as ButtonPrimitive } from "@base-ui/react/button" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", - xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-9", - "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); + "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", + xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + icon: "size-9", + "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) function Button({ - className, - variant = "default", - size = "default", - ...props + className, + variant = "default", + size = "default", + ...props }: ButtonPrimitive.Props & VariantProps) { - return ( - - ); + return ( + + ) } -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/apps/web/src/app/ui/calendar.tsx b/apps/web/src/app/ui/calendar.tsx index 4dab360b..baed5254 100644 --- a/apps/web/src/app/ui/calendar.tsx +++ b/apps/web/src/app/ui/calendar.tsx @@ -1,216 +1,220 @@ -import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; -import * as React from "react"; +import * as React from "react" import { - type DayButton, - DayPicker, - getDefaultClassNames, - type Locale, -} from "react-day-picker"; -import { Button, buttonVariants } from "@/app/ui/button"; -import { cn } from "@/lib/utils"; + DayPicker, + getDefaultClassNames, + type DayButton, + type Locale, +} from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/app/ui/button" +import { HugeiconsIcon } from "@hugeicons/react" +import { ArrowLeftIcon, ArrowRightIcon, ArrowDownIcon } from "@hugeicons/core-free-icons" function Calendar({ - className, - classNames, - showOutsideDays = true, - captionLayout = "label", - buttonVariant = "ghost", - locale, - formatters, - components, - ...props + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + locale, + formatters, + components, + ...props }: React.ComponentProps & { - buttonVariant?: React.ComponentProps["variant"]; + buttonVariant?: React.ComponentProps["variant"] }) { - const defaultClassNames = getDefaultClassNames(); + const defaultClassNames = getDefaultClassNames() - return ( - svg]:rotate-180`, - String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, - className, - )} - captionLayout={captionLayout} - locale={locale} - formatters={{ - formatMonthDropdown: (date) => - date.toLocaleString(locale?.code, { month: "short" }), - ...formatters, - }} - classNames={{ - root: cn("w-fit", defaultClassNames.root), - months: cn( - "relative flex flex-col gap-4 md:flex-row", - defaultClassNames.months, - ), - month: cn("flex w-full flex-col gap-4", defaultClassNames.month), - nav: cn( - "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", - defaultClassNames.nav, - ), - button_previous: cn( - buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", - defaultClassNames.button_previous, - ), - button_next: cn( - buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", - defaultClassNames.button_next, - ), - month_caption: cn( - "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", - defaultClassNames.month_caption, - ), - dropdowns: cn( - "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", - defaultClassNames.dropdowns, - ), - dropdown_root: cn( - "relative rounded-(--cell-radius)", - defaultClassNames.dropdown_root, - ), - dropdown: cn( - "absolute inset-0 bg-popover opacity-0", - defaultClassNames.dropdown, - ), - caption_label: cn( - "font-medium select-none", - captionLayout === "label" - ? "text-sm" - : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", - defaultClassNames.caption_label, - ), - table: "w-full border-collapse", - weekdays: cn("flex", defaultClassNames.weekdays), - weekday: cn( - "flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none", - defaultClassNames.weekday, - ), - week: cn("mt-2 flex w-full", defaultClassNames.week), - week_number_header: cn( - "w-(--cell-size) select-none", - defaultClassNames.week_number_header, - ), - week_number: cn( - "text-[0.8rem] text-muted-foreground select-none", - defaultClassNames.week_number, - ), - day: cn( - "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", - props.showWeekNumber - ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" - : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", - defaultClassNames.day, - ), - range_start: cn( - "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted", - defaultClassNames.range_start, - ), - range_middle: cn("rounded-none", defaultClassNames.range_middle), - range_end: cn( - "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted", - defaultClassNames.range_end, - ), - today: cn( - "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none", - defaultClassNames.today, - ), - outside: cn( - "text-muted-foreground aria-selected:text-muted-foreground", - defaultClassNames.outside, - ), - disabled: cn( - "text-muted-foreground opacity-50", - defaultClassNames.disabled, - ), - hidden: cn("invisible", defaultClassNames.hidden), - ...classNames, - }} - components={{ - Root: ({ className, rootRef, ...props }) => { - return ( -
- ); - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return ( - - ); - } + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + locale={locale} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString(locale?.code, { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_next + ), + month_caption: cn( + "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative rounded-(--cell-radius)", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute inset-0 bg-popover opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "font-medium select-none", + captionLayout === "label" + ? "text-sm" + : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none", + defaultClassNames.weekday + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-(--cell-size) select-none", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] text-muted-foreground select-none", + defaultClassNames.week_number + ), + day: cn( + "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" + : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", + defaultClassNames.day + ), + range_start: cn( + "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn( + "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted", + defaultClassNames.range_end + ), + today: cn( + "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } - if (orientation === "right") { - return ( - - ); - } + if (orientation === "right") { + return ( + + ) + } - return ; - }, - DayButton: ({ ...props }) => ( - - ), - WeekNumber: ({ children, ...props }) => { - return ( - -
- {children} -
- - ); - }, - ...components, - }} - {...props} - /> - ); + return ( + + ) + }, + DayButton: ({ ...props }) => ( + + ), + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) } function CalendarDayButton({ - className, - day, - modifiers, - locale, - ...props + className, + day, + modifiers, + locale, + ...props }: React.ComponentProps & { locale?: Partial }) { - const defaultClassNames = getDefaultClassNames(); + const defaultClassNames = getDefaultClassNames() - const ref = React.useRef(null); - React.useEffect(() => { - if (modifiers.focused) ref.current?.focus(); - }, [modifiers.focused]); + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) - return ( - + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar(); + + return ( +