Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/native/src/app/[locale]/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React from "react";
import { Link, usePathname } from "@workspace/i18n/navigation";
import { Link, usePathname, useRouter } from "@workspace/i18n/navigation";
import { AppLayout as MainLayout } from "@workspace/core/components/layout/app-layout";

interface AppLayoutProps {
Expand All @@ -23,10 +23,15 @@ const NativeLink = ({
};

export function AppLayout({ children }: AppLayoutProps) {
const router = useRouter();
const pathname = usePathname();

return (
<MainLayout pathname={pathname} LinkComponent={NativeLink}>
<MainLayout
pathname={pathname}
navigate={(path) => router.push(path)}
LinkComponent={NativeLink}
>
{children}
</MainLayout>
);
Expand Down
9 changes: 7 additions & 2 deletions apps/web/app/[locale]/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
"use client";

import { Link, usePathname } from "@workspace/i18n/navigation";
import { Link, usePathname, useRouter } from "@workspace/i18n/navigation";
import { AppLayout } from "@workspace/core/components/layout/app-layout";

interface AppGroupLayoutProps {
children: React.ReactNode;
}

export default function AppGroupLayout({ children }: AppGroupLayoutProps) {
const router = useRouter();
const pathname = usePathname();

return (
<AppLayout pathname={pathname} LinkComponent={Link}>
<AppLayout
pathname={pathname}
navigate={(path) => router.push(path)}
LinkComponent={Link}
>
{children}
</AppLayout>
);
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hotkeys-hook": "^5.2.4",
"react-use-measure": "^2.1.7",
"zustand": "^5.0.11"
},
Expand Down
125 changes: 125 additions & 0 deletions packages/core/src/components/common/hotkeys-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import React from "react";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "@workspace/ui/components/drawer";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@workspace/ui/components/dialog";
import { Kbd, KbdGroup } from "@workspace/ui/components/kbd";
import { Separator } from "@workspace/ui/components/separator";
import { useTranslations } from "@workspace/i18n";
import { useHotkeysDialogStore } from "@workspace/core/stores/hotkeys-store";
import { hotkeys, type HotkeyDefinition } from "@workspace/core/config/hotkeys";
import { formatHotkeyDisplay } from "@workspace/core/lib/utils";
import { useIsMobile } from "@workspace/ui/hooks/use-mobile";

function HotkeyRow({ hotkey }: { hotkey: HotkeyDefinition }) {
const t = useTranslations("HotkeysDialog");
const keys = formatHotkeyDisplay(hotkey.keys);
const isSequence = hotkey.keys.includes(">");

return (
<div className="flex items-center justify-between py-2">
<span className="text-sm">{t(hotkey.translationKey)}</span>
<KbdGroup className="gap-0.5">
{keys.map((key, i) => (
<React.Fragment key={i}>
<Kbd>{key}</Kbd>
{isSequence && i < keys.length - 1 && (
<span className="text-[10px] text-muted-foreground mx-1.5">
{t("then")}
</span>
)}
</React.Fragment>
))}
</KbdGroup>
</div>
);
}

function HotkeysList() {
const t = useTranslations("HotkeysDialog");

const generalHotkeys = hotkeys.filter((h) => h.category === "general");
const navigationHotkeys = hotkeys.filter((h) => h.category === "navigation");

return (
<div className="space-y-4">
{generalHotkeys.length > 0 && (
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
{t("general")}
</h3>
<div className="space-y-0.5">
{generalHotkeys.map((hotkey) => (
<HotkeyRow key={hotkey.id} hotkey={hotkey} />
))}
</div>
</div>
)}

{generalHotkeys.length > 0 && navigationHotkeys.length > 0 && (
<Separator />
)}

{navigationHotkeys.length > 0 && (
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
{t("navigation")}
</h3>
<div className="space-y-0.5">
{navigationHotkeys.map((hotkey) => (
<HotkeyRow key={hotkey.id} hotkey={hotkey} />
))}
</div>
</div>
)}
</div>
);
}

export function HotkeysDialog() {
const { isOpen, close } = useHotkeysDialogStore();
const t = useTranslations("HotkeysDialog");
const isMobile = useIsMobile();

if (isMobile) {
return (
<Drawer open={isOpen} onOpenChange={(open) => !open && close()}>
<DrawerContent className="px-4 pb-8">
<DrawerHeader>
<DrawerTitle>{t("title")}</DrawerTitle>
<DrawerDescription>{t("description")}</DrawerDescription>
</DrawerHeader>
<div className="overflow-y-auto w-full max-w-sm mx-auto no-scrollbar">
<HotkeysList />
</div>
</DrawerContent>
</Drawer>
);
}

return (
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
<DialogContent className="sm:max-w-md max-h-[85vh] flex flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>{t("title")}</DialogTitle>
<DialogDescription>{t("description")}</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto flex-1 no-scrollbar -mx-6 px-6">
<HotkeysList />
</div>
</DialogContent>
</Dialog>
);
}
13 changes: 13 additions & 0 deletions packages/core/src/components/layout/app-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"use client";

import { ReactNode, ComponentType } from "react";
import { AppSidebar } from "@workspace/core/components/layout/app-sidebar";
import { AppHeader } from "@workspace/core/components/layout/app-header";
import { HotkeysDialog } from "@workspace/core/components/common/hotkeys-dialog";
import { ThemeProvider } from "@workspace/core/providers/theme-provider";
import { useAppHotkeys } from "@workspace/core/hooks/use-app-hotkeys";
import {
SidebarInset,
SidebarProvider,
Expand All @@ -10,6 +14,7 @@ import {
interface AppLayoutProps {
children: ReactNode;
pathname: string;
navigate: (path: string) => void;
LinkComponent?:
| ComponentType<{
href: string;
Expand All @@ -20,9 +25,15 @@ interface AppLayoutProps {
| "a";
}

function HotkeysRegistrar({ navigate }: { navigate: (path: string) => void }) {
useAppHotkeys({ navigate });
return null;
}

export function AppLayout({
children,
pathname,
navigate,
LinkComponent,
}: AppLayoutProps) {
return (
Expand All @@ -34,11 +45,13 @@ export function AppLayout({
enableColorScheme
>
<SidebarProvider className="h-screen">
<HotkeysRegistrar navigate={navigate} />
<AppSidebar pathname={pathname} LinkComponent={LinkComponent} />
<SidebarInset>
<AppHeader pathname={pathname} LinkComponent={LinkComponent} />
{children}
</SidebarInset>
<HotkeysDialog />
</SidebarProvider>
</ThemeProvider>
);
Expand Down
63 changes: 63 additions & 0 deletions packages/core/src/config/hotkeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export interface HotkeyDefinition {
id: string;
keys: string;
translationKey: string;
category: "navigation" | "general";
}

export const hotkeys: HotkeyDefinition[] = [
{
id: "command-palette",
keys: "mod+k",
translationKey: "commandPalette",
category: "general",
},
{
id: "toggle-sidebar",
keys: "mod+b",
translationKey: "toggleSidebar",
category: "general",
},
{
id: "toggle-mode",
keys: "shift+d",
translationKey: "toggleMode",
category: "general",
},
{
id: "go-home",
keys: "g>h",
translationKey: "goHome",
category: "navigation",
},
{
id: "go-dashboard",
keys: "g>d",
translationKey: "goDashboard",
category: "navigation",
},
{
id: "go-analytics",
keys: "g>a",
translationKey: "goAnalytics",
category: "navigation",
},
{
id: "go-overview",
keys: "g>o",
translationKey: "goOverview",
category: "navigation",
},
{
id: "go-settings",
keys: "g>s",
translationKey: "goSettings",
category: "navigation",
},
{
id: "show-hotkeys",
keys: "?",
translationKey: "showHotkeys",
category: "general",
},
];
Loading
Loading