diff --git a/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx index a39ae50..b9edb3a 100644 --- a/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx @@ -163,9 +163,11 @@ export function DashboardMobileNav({ Profile - - Settings - + + + Settings + + diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx index aeb0b39..740fe8e 100644 --- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx @@ -230,9 +230,11 @@ export function DashboardTopbar({ Profile - - Settings - + + + Settings + + diff --git a/apps/dashboard/src/lib/command-palette/registry.ts b/apps/dashboard/src/lib/command-palette/registry.ts index 9d15b24..9116bbe 100644 --- a/apps/dashboard/src/lib/command-palette/registry.ts +++ b/apps/dashboard/src/lib/command-palette/registry.ts @@ -3,6 +3,7 @@ import { GitPullRequestIcon, IssuesIcon, ReviewsIcon, + SettingsIcon, } from "@diffkit/icons"; import type { CommandItem } from "./types"; @@ -71,4 +72,13 @@ registerCommands([ shortcut: ["G", "R"], action: { type: "navigate", to: "/reviews" }, }, + { + id: "nav:settings", + label: "Go to Settings", + group: "Pages", + icon: SettingsIcon, + keywords: ["settings", "preferences", "config"], + shortcut: ["G", "S"], + action: { type: "navigate", to: "/settings" }, + }, ]); diff --git a/apps/dashboard/src/routeTree.gen.ts b/apps/dashboard/src/routeTree.gen.ts index 81dc162..5e556ef 100644 --- a/apps/dashboard/src/routeTree.gen.ts +++ b/apps/dashboard/src/routeTree.gen.ts @@ -15,11 +15,14 @@ import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' import { Route as LoginRouteImport } from './routes/login' import { Route as ProtectedRouteImport } from './routes/_protected' import { Route as ProtectedIndexRouteImport } from './routes/_protected/index' +import { Route as ProtectedSettingsRouteImport } from './routes/_protected/settings' import { Route as ProtectedReviewsRouteImport } from './routes/_protected/reviews' import { Route as ProtectedPullsRouteImport } from './routes/_protected/pulls' import { Route as ProtectedIssuesRouteImport } from './routes/_protected/issues' +import { Route as ProtectedSettingsIndexRouteImport } from './routes/_protected/settings/index' import { Route as ApiWebhooksGithubRouteImport } from './routes/api/webhooks/github' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' +import { Route as ProtectedSettingsShortcutsRouteImport } from './routes/_protected/settings/shortcuts' import { Route as ApiGithubAppCallbackRouteImport } from './routes/api/github/app/callback' import { Route as ApiGithubAppAuthorizeRouteImport } from './routes/api/github/app/authorize' import { Route as ProtectedOwnerRepoReviewPullIdRouteImport } from './routes/_protected/$owner/$repo/review.$pullId' @@ -55,6 +58,11 @@ const ProtectedIndexRoute = ProtectedIndexRouteImport.update({ path: '/', getParentRoute: () => ProtectedRoute, } as any) +const ProtectedSettingsRoute = ProtectedSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => ProtectedRoute, +} as any) const ProtectedReviewsRoute = ProtectedReviewsRouteImport.update({ id: '/reviews', path: '/reviews', @@ -70,6 +78,11 @@ const ProtectedIssuesRoute = ProtectedIssuesRouteImport.update({ path: '/issues', getParentRoute: () => ProtectedRoute, } as any) +const ProtectedSettingsIndexRoute = ProtectedSettingsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ProtectedSettingsRoute, +} as any) const ApiWebhooksGithubRoute = ApiWebhooksGithubRouteImport.update({ id: '/api/webhooks/github', path: '/api/webhooks/github', @@ -80,6 +93,12 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ path: '/api/auth/$', getParentRoute: () => rootRouteImport, } as any) +const ProtectedSettingsShortcutsRoute = + ProtectedSettingsShortcutsRouteImport.update({ + id: '/shortcuts', + path: '/shortcuts', + getParentRoute: () => ProtectedSettingsRoute, + } as any) const ApiGithubAppCallbackRoute = ApiGithubAppCallbackRouteImport.update({ id: '/api/github/app/callback', path: '/api/github/app/callback', @@ -118,8 +137,11 @@ export interface FileRoutesByFullPath { '/issues': typeof ProtectedIssuesRoute '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute + '/settings': typeof ProtectedSettingsRouteWithChildren + '/settings/shortcuts': typeof ProtectedSettingsShortcutsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/webhooks/github': typeof ApiWebhooksGithubRoute + '/settings/': typeof ProtectedSettingsIndexRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute @@ -135,8 +157,10 @@ export interface FileRoutesByTo { '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute '/': typeof ProtectedIndexRoute + '/settings/shortcuts': typeof ProtectedSettingsShortcutsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/webhooks/github': typeof ApiWebhooksGithubRoute + '/settings': typeof ProtectedSettingsIndexRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute @@ -153,9 +177,12 @@ export interface FileRoutesById { '/_protected/issues': typeof ProtectedIssuesRoute '/_protected/pulls': typeof ProtectedPullsRoute '/_protected/reviews': typeof ProtectedReviewsRoute + '/_protected/settings': typeof ProtectedSettingsRouteWithChildren '/_protected/': typeof ProtectedIndexRoute + '/_protected/settings/shortcuts': typeof ProtectedSettingsShortcutsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/webhooks/github': typeof ApiWebhooksGithubRoute + '/_protected/settings/': typeof ProtectedSettingsIndexRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/_protected/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute @@ -173,8 +200,11 @@ export interface FileRouteTypes { | '/issues' | '/pulls' | '/reviews' + | '/settings' + | '/settings/shortcuts' | '/api/auth/$' | '/api/webhooks/github' + | '/settings/' | '/api/github/app/authorize' | '/api/github/app/callback' | '/$owner/$repo/issues/$issueId' @@ -190,8 +220,10 @@ export interface FileRouteTypes { | '/pulls' | '/reviews' | '/' + | '/settings/shortcuts' | '/api/auth/$' | '/api/webhooks/github' + | '/settings' | '/api/github/app/authorize' | '/api/github/app/callback' | '/$owner/$repo/issues/$issueId' @@ -207,9 +239,12 @@ export interface FileRouteTypes { | '/_protected/issues' | '/_protected/pulls' | '/_protected/reviews' + | '/_protected/settings' | '/_protected/' + | '/_protected/settings/shortcuts' | '/api/auth/$' | '/api/webhooks/github' + | '/_protected/settings/' | '/api/github/app/authorize' | '/api/github/app/callback' | '/_protected/$owner/$repo/issues/$issueId' @@ -273,6 +308,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProtectedIndexRouteImport parentRoute: typeof ProtectedRoute } + '/_protected/settings': { + id: '/_protected/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof ProtectedSettingsRouteImport + parentRoute: typeof ProtectedRoute + } '/_protected/reviews': { id: '/_protected/reviews' path: '/reviews' @@ -294,6 +336,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProtectedIssuesRouteImport parentRoute: typeof ProtectedRoute } + '/_protected/settings/': { + id: '/_protected/settings/' + path: '/' + fullPath: '/settings/' + preLoaderRoute: typeof ProtectedSettingsIndexRouteImport + parentRoute: typeof ProtectedSettingsRoute + } '/api/webhooks/github': { id: '/api/webhooks/github' path: '/api/webhooks/github' @@ -308,6 +357,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAuthSplatRouteImport parentRoute: typeof rootRouteImport } + '/_protected/settings/shortcuts': { + id: '/_protected/settings/shortcuts' + path: '/shortcuts' + fullPath: '/settings/shortcuts' + preLoaderRoute: typeof ProtectedSettingsShortcutsRouteImport + parentRoute: typeof ProtectedSettingsRoute + } '/api/github/app/callback': { id: '/api/github/app/callback' path: '/api/github/app/callback' @@ -346,10 +402,24 @@ declare module '@tanstack/react-router' { } } +interface ProtectedSettingsRouteChildren { + ProtectedSettingsShortcutsRoute: typeof ProtectedSettingsShortcutsRoute + ProtectedSettingsIndexRoute: typeof ProtectedSettingsIndexRoute +} + +const ProtectedSettingsRouteChildren: ProtectedSettingsRouteChildren = { + ProtectedSettingsShortcutsRoute: ProtectedSettingsShortcutsRoute, + ProtectedSettingsIndexRoute: ProtectedSettingsIndexRoute, +} + +const ProtectedSettingsRouteWithChildren = + ProtectedSettingsRoute._addFileChildren(ProtectedSettingsRouteChildren) + interface ProtectedRouteChildren { ProtectedIssuesRoute: typeof ProtectedIssuesRoute ProtectedPullsRoute: typeof ProtectedPullsRoute ProtectedReviewsRoute: typeof ProtectedReviewsRoute + ProtectedSettingsRoute: typeof ProtectedSettingsRouteWithChildren ProtectedIndexRoute: typeof ProtectedIndexRoute ProtectedOwnerRepoIssuesIssueIdRoute: typeof ProtectedOwnerRepoIssuesIssueIdRoute ProtectedOwnerRepoPullPullIdRoute: typeof ProtectedOwnerRepoPullPullIdRoute @@ -360,6 +430,7 @@ const ProtectedRouteChildren: ProtectedRouteChildren = { ProtectedIssuesRoute: ProtectedIssuesRoute, ProtectedPullsRoute: ProtectedPullsRoute, ProtectedReviewsRoute: ProtectedReviewsRoute, + ProtectedSettingsRoute: ProtectedSettingsRouteWithChildren, ProtectedIndexRoute: ProtectedIndexRoute, ProtectedOwnerRepoIssuesIssueIdRoute: ProtectedOwnerRepoIssuesIssueIdRoute, ProtectedOwnerRepoPullPullIdRoute: ProtectedOwnerRepoPullPullIdRoute, diff --git a/apps/dashboard/src/routes/_protected/settings.tsx b/apps/dashboard/src/routes/_protected/settings.tsx new file mode 100644 index 0000000..d38c752 --- /dev/null +++ b/apps/dashboard/src/routes/_protected/settings.tsx @@ -0,0 +1,77 @@ +import { ChevronRightIcon } from "@diffkit/icons"; +import { cn } from "@diffkit/ui/lib/utils"; +import { + createFileRoute, + Link, + Outlet, + useMatches, +} from "@tanstack/react-router"; +import { buildSeo, formatPageTitle, PRIVATE_ROUTE_HEADERS } from "#/lib/seo"; + +const settingsNav = [ + { to: "/settings", label: "General" }, + { to: "/settings/shortcuts", label: "Shortcuts" }, +] as const; + +export const Route = createFileRoute("/_protected/settings")({ + headers: () => PRIVATE_ROUTE_HEADERS, + head: ({ match }) => + buildSeo({ + path: match.pathname, + title: formatPageTitle("Settings"), + description: "Configure your DiffKit preferences.", + robots: "noindex", + }), + component: SettingsLayout, +}); + +function SettingsLayout() { + const matches = useMatches(); + const currentPath = matches[matches.length - 1]?.pathname ?? "/settings"; + + return ( +
+
+ + +
+ +
+
+
+ ); +} diff --git a/apps/dashboard/src/routes/_protected/settings/index.tsx b/apps/dashboard/src/routes/_protected/settings/index.tsx new file mode 100644 index 0000000..2372b50 --- /dev/null +++ b/apps/dashboard/src/routes/_protected/settings/index.tsx @@ -0,0 +1,231 @@ +import { MoonIcon, SunIcon, SystemIcon } from "@diffkit/icons"; +import { Button } from "@diffkit/ui/components/button"; +import { cn } from "@diffkit/ui/lib/utils"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { useTheme } from "next-themes"; +import { signOutToLogin } from "#/lib/auth-actions"; +import { useHasMounted } from "#/lib/use-has-mounted"; + +const themeOptions = [ + { + value: "light", + label: "Light", + icon: SunIcon, + }, + { + value: "dark", + label: "Dark", + icon: MoonIcon, + }, + { + value: "system", + label: "System", + icon: SystemIcon, + }, +] as const; + +export const Route = createFileRoute("/_protected/settings/")({ + component: GeneralSettingsPage, +}); + +function GeneralSettingsPage() { + const { user } = Route.useRouteContext(); + + return ( + <> + + + + + +
+
+

GitHub App permissions

+

+ Configure installations, add new organizations, or revoke access. +

+
+ +
+
+ + +
+
+

{user.name ?? "Account"}

+

{user.email}

+
+ +
+
+ + ); +} + +function SettingsSection({ + title, + description, + children, +}: { + title: string; + description: string; + children: React.ReactNode; +}) { + return ( +
+
+

{title}

+

{description}

+
+ {children} +
+ ); +} + +function ThemePicker() { + const { theme, setTheme } = useTheme(); + const hasMounted = useHasMounted(); + + return ( +
+ {themeOptions.map((opt) => { + const isActive = hasMounted && theme === opt.value; + return ( + + ); + })} +
+ ); +} + +/** + * Miniature replica of the dashboard layout: + * bg-muted shell → topbar (avatar dot + nav pills) → rounded card content area with rows. + */ +function LayoutPreview({ mode }: { mode: "light" | "dark" }) { + const light = mode === "light"; + // Shell (bg-muted equivalent) + const shell = light ? "bg-zinc-100" : "bg-zinc-900"; + // Topbar elements + const avatarDot = light ? "bg-zinc-400" : "bg-zinc-500"; + const navPill = light ? "bg-zinc-300" : "bg-zinc-700"; + const navPillActive = light ? "bg-zinc-200/80" : "bg-zinc-600"; + // Content card + const card = light ? "bg-white" : "bg-zinc-800"; + const cardBorder = light ? "border-zinc-200" : "border-zinc-700"; + // Rows inside card + const row = light ? "bg-zinc-100" : "bg-zinc-700"; + const rowMuted = light ? "bg-zinc-200" : "bg-zinc-600"; + + return ( +
+ {/* Topbar */} +
+
+
+
+
+
+ {/* Content card */} +
+ {/* Header row */} +
+
+
+ {/* List rows */} +
+
+
+
+
+ ); +} + +/** + * System theme: renders both light and dark previews stacked, + * using a diagonal clip-path mask to show left-half light / right-half dark. + */ +function SystemThemePreview() { + return ( +
+ {/* Light – full, clipped to left half */} +
+ +
+ {/* Dark – full, clipped to right half */} +
+ +
+
+ ); +} diff --git a/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx b/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx new file mode 100644 index 0000000..b81f040 --- /dev/null +++ b/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx @@ -0,0 +1,292 @@ +import { CommandIcon } from "@diffkit/icons"; +import { cn } from "@diffkit/ui/lib/utils"; +import { createFileRoute } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +function ShiftIcon() { + return ( + + ); +} + +function OptionIcon() { + return ( + + ); +} + +function ControlIcon() { + return ( + + ); +} + +function ArrowLeftIcon() { + return ( + + ); +} + +function ArrowRightIcon() { + return ( + + ); +} + +function SpaceIcon() { + return ( + + ); +} + +function ReturnIcon() { + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Key rendering: maps special key names to SVG icons, falls back to text +// --------------------------------------------------------------------------- + +type KeyToken = string | { icon: ReactNode; label: string }; + +const specialKeys: Record = { + command: { icon: , label: "Cmd" }, + cmd: { icon: , label: "Cmd" }, + "\u2318": { icon: , label: "Cmd" }, + shift: { icon: , label: "Shift" }, + option: { icon: , label: "Option" }, + alt: { icon: , label: "Alt" }, + control: { icon: , label: "Ctrl" }, + ctrl: { icon: , label: "Ctrl" }, + space: { icon: , label: "Space" }, + return: { icon: , label: "Return" }, + enter: { icon: , label: "Enter" }, + "\u2190": { icon: , label: "Left" }, + "\u2192": { icon: , label: "Right" }, +}; + +function resolveKey(key: string): KeyToken { + return specialKeys[key.toLowerCase()] ?? key; +} + +// --------------------------------------------------------------------------- +// Shortcut data +// --------------------------------------------------------------------------- + +type Shortcut = { + keys: string[]; + description: string; +}; + +type ShortcutGroup = { + title: string; + shortcuts: Shortcut[]; +}; + +const isMac = + typeof navigator !== "undefined" && + /Mac|iPhone|iPad/.test(navigator.userAgent); +const mod = isMac ? "\u2318" : "Ctrl"; + +const shortcutGroups: ShortcutGroup[] = [ + { + title: "Navigation", + shortcuts: [ + { keys: ["G", "H"], description: "Go to Overview" }, + { keys: ["G", "P"], description: "Go to Pull Requests" }, + { keys: ["G", "I"], description: "Go to Issues" }, + { keys: ["G", "R"], description: "Go to Reviews" }, + { keys: ["G", "S"], description: "Go to Settings" }, + ], + }, + { + title: "Tabs", + shortcuts: [ + { keys: ["Shift", "1\u20139"], description: "Switch to tab by position" }, + { keys: ["Shift", "\u2190"], description: "Previous tab" }, + { keys: ["Shift", "\u2192"], description: "Next tab" }, + ], + }, + { + title: "General", + shortcuts: [{ keys: [mod, "K"], description: "Open command palette" }], + }, + { + title: "Markdown editor", + shortcuts: [ + { keys: [mod, "B"], description: "Bold" }, + { keys: [mod, "I"], description: "Italic" }, + { keys: [mod, "E"], description: "Inline code" }, + { keys: [mod, "K"], description: "Insert link" }, + { keys: [mod, "H"], description: "Heading" }, + { keys: [mod, "Shift", "."], description: "Blockquote" }, + { keys: [mod, "Shift", "8"], description: "Bulleted list" }, + { keys: [mod, "Shift", "7"], description: "Numbered list" }, + ], + }, +]; + +// --------------------------------------------------------------------------- +// Route & component +// --------------------------------------------------------------------------- + +export const Route = createFileRoute("/_protected/settings/shortcuts")({ + component: ShortcutsPage, +}); + +function ShortcutsPage() { + return ( + <> +
+

Keyboard shortcuts

+

+ Quick actions to navigate and interact with DiffKit. +

+
+ +
+ {shortcutGroups.map((group) => ( +
+

+ {group.title} +

+
+ {group.shortcuts.map((shortcut, i) => ( +
0 && "border-t border-border/70", + )} + > + {shortcut.description} + + {shortcut.keys.map((key) => { + const token = resolveKey(key); + const isSpecial = typeof token !== "string"; + return ( + + {isSpecial ? token.icon : token} + + ); + })} + +
+ ))} +
+
+ ))} +
+ + ); +} diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 9f058d7..1dbab83 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -19,6 +19,7 @@ export { CircleIcon, Clock01Icon as ClockIcon, CodeIcon, + CommandIcon, Comment01Icon as CommentIcon, ComputerIcon as SystemIcon, Copy01Icon as CopyIcon, @@ -38,7 +39,7 @@ export { InboxIcon, Loading03Icon as LoaderCircleIcon, Message01Icon as MessageIcon, - Moon01Icon as MoonIcon, + Moon02Icon as MoonIcon, MoreHorizontalIcon, Notification01Icon as NotificationIcon, PencilEdit01Icon as EditIcon,