From a27aa82fdd6387c2d1ef9d691fffe611228b0867 Mon Sep 17 00:00:00 2001 From: dest Date: Tue, 14 Apr 2026 14:56:47 +0300 Subject: [PATCH] feat(hotkeys): add keyboard shortcuts and dialog/drawer --- .../src/app/[locale]/components/AppLayout.tsx | 9 +- apps/web/app/[locale]/(app)/layout.tsx | 9 +- packages/core/package.json | 1 + .../src/components/common/hotkeys-dialog.tsx | 125 ++++++++++++++ .../core/src/components/layout/app-layout.tsx | 13 ++ packages/core/src/config/hotkeys.ts | 63 +++++++ packages/core/src/hooks/use-app-hotkeys.ts | 106 ++++++++++++ packages/core/src/lib/utils.ts | 25 +++ packages/core/src/stores/hotkeys-store.ts | 15 ++ packages/i18n/src/messages/de.json | 16 ++ packages/i18n/src/messages/en.json | 16 ++ packages/i18n/src/messages/es.json | 16 ++ packages/i18n/src/messages/fr.json | 16 ++ packages/i18n/src/messages/it.json | 16 ++ packages/i18n/src/messages/ja.json | 16 ++ packages/i18n/src/messages/pt.json | 16 ++ packages/i18n/src/messages/ru.json | 16 ++ packages/i18n/src/messages/tr.json | 16 ++ packages/i18n/src/messages/zh.json | 16 ++ packages/ui/package.json | 3 +- packages/ui/src/components/dialog.tsx | 158 ++++++++++++++++++ packages/ui/src/components/drawer.tsx | 135 +++++++++++++++ packages/ui/src/components/kbd.tsx | 28 ++++ packages/ui/src/components/sidebar.tsx | 17 -- packages/ui/src/styles/globals.css | 10 ++ pnpm-lock.yaml | 32 ++++ 26 files changed, 887 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/components/common/hotkeys-dialog.tsx create mode 100644 packages/core/src/config/hotkeys.ts create mode 100644 packages/core/src/hooks/use-app-hotkeys.ts create mode 100644 packages/core/src/stores/hotkeys-store.ts create mode 100644 packages/ui/src/components/dialog.tsx create mode 100644 packages/ui/src/components/drawer.tsx create mode 100644 packages/ui/src/components/kbd.tsx diff --git a/apps/native/src/app/[locale]/components/AppLayout.tsx b/apps/native/src/app/[locale]/components/AppLayout.tsx index ee2c8af..25b6293 100644 --- a/apps/native/src/app/[locale]/components/AppLayout.tsx +++ b/apps/native/src/app/[locale]/components/AppLayout.tsx @@ -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 { @@ -23,10 +23,15 @@ const NativeLink = ({ }; export function AppLayout({ children }: AppLayoutProps) { + const router = useRouter(); const pathname = usePathname(); return ( - + router.push(path)} + LinkComponent={NativeLink} + > {children} ); diff --git a/apps/web/app/[locale]/(app)/layout.tsx b/apps/web/app/[locale]/(app)/layout.tsx index dc6cf16..6492467 100644 --- a/apps/web/app/[locale]/(app)/layout.tsx +++ b/apps/web/app/[locale]/(app)/layout.tsx @@ -1,6 +1,6 @@ "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 { @@ -8,10 +8,15 @@ interface AppGroupLayoutProps { } export default function AppGroupLayout({ children }: AppGroupLayoutProps) { + const router = useRouter(); const pathname = usePathname(); return ( - + router.push(path)} + LinkComponent={Link} + > {children} ); diff --git a/packages/core/package.json b/packages/core/package.json index 5442f2d..46b30da 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" }, diff --git a/packages/core/src/components/common/hotkeys-dialog.tsx b/packages/core/src/components/common/hotkeys-dialog.tsx new file mode 100644 index 0000000..3d198c9 --- /dev/null +++ b/packages/core/src/components/common/hotkeys-dialog.tsx @@ -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 ( +
+ {t(hotkey.translationKey)} + + {keys.map((key, i) => ( + + {key} + {isSequence && i < keys.length - 1 && ( + + {t("then")} + + )} + + ))} + +
+ ); +} + +function HotkeysList() { + const t = useTranslations("HotkeysDialog"); + + const generalHotkeys = hotkeys.filter((h) => h.category === "general"); + const navigationHotkeys = hotkeys.filter((h) => h.category === "navigation"); + + return ( +
+ {generalHotkeys.length > 0 && ( +
+

+ {t("general")} +

+
+ {generalHotkeys.map((hotkey) => ( + + ))} +
+
+ )} + + {generalHotkeys.length > 0 && navigationHotkeys.length > 0 && ( + + )} + + {navigationHotkeys.length > 0 && ( +
+

+ {t("navigation")} +

+
+ {navigationHotkeys.map((hotkey) => ( + + ))} +
+
+ )} +
+ ); +} + +export function HotkeysDialog() { + const { isOpen, close } = useHotkeysDialogStore(); + const t = useTranslations("HotkeysDialog"); + const isMobile = useIsMobile(); + + if (isMobile) { + return ( + !open && close()}> + + + {t("title")} + {t("description")} + +
+ +
+
+
+ ); + } + + return ( + !open && close()}> + + + {t("title")} + {t("description")} + +
+ +
+
+
+ ); +} diff --git a/packages/core/src/components/layout/app-layout.tsx b/packages/core/src/components/layout/app-layout.tsx index 025ba58..68a1338 100644 --- a/packages/core/src/components/layout/app-layout.tsx +++ b/packages/core/src/components/layout/app-layout.tsx @@ -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, @@ -10,6 +14,7 @@ import { interface AppLayoutProps { children: ReactNode; pathname: string; + navigate: (path: string) => void; LinkComponent?: | ComponentType<{ href: string; @@ -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 ( @@ -34,11 +45,13 @@ export function AppLayout({ enableColorScheme > + {children} + ); diff --git a/packages/core/src/config/hotkeys.ts b/packages/core/src/config/hotkeys.ts new file mode 100644 index 0000000..a890321 --- /dev/null +++ b/packages/core/src/config/hotkeys.ts @@ -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", + }, +]; diff --git a/packages/core/src/hooks/use-app-hotkeys.ts b/packages/core/src/hooks/use-app-hotkeys.ts new file mode 100644 index 0000000..af77b69 --- /dev/null +++ b/packages/core/src/hooks/use-app-hotkeys.ts @@ -0,0 +1,106 @@ +"use client"; + +import { useHotkeys } from "react-hotkeys-hook"; +import { hotkeys } from "@workspace/core/config/hotkeys"; +import { useSidebar } from "@workspace/ui/components/sidebar"; +import { useHotkeysDialogStore } from "@workspace/core/stores/hotkeys-store"; +import { useThemeTransition } from "@workspace/core/hooks/use-theme-transition"; + +interface UseAppHotkeysOptions { + navigate: (path: string) => void; +} + +export function useAppHotkeys({ navigate }: UseAppHotkeysOptions) { + const { toggleSidebar } = useSidebar(); + const { theme, handleThemeChange } = useThemeTransition(); + const toggleHotkeysDialog = useHotkeysDialogStore((s) => s.toggle); + + const getKeys = (id: string) => hotkeys.find((h) => h.id === id)?.keys || ""; + + // Command Palette (placeholder for now) + useHotkeys( + getKeys("command-palette"), + (e: KeyboardEvent) => { + e.preventDefault(); + // TODO: Command palette logic will be added in the feat/command-palette branch. + console.log("Command Palette triggered!"); + }, + { enableOnFormTags: false, delimiter: "|" }, + ); + + // Toggle Theme + useHotkeys( + getKeys("toggle-mode"), + (e: KeyboardEvent) => { + e.preventDefault(); + handleThemeChange((theme === "dark" ? "light" : "dark") as any); + }, + { enableOnFormTags: false }, + ); + + // Toggle Sidebar + useHotkeys( + getKeys("toggle-sidebar"), + (e: KeyboardEvent) => { + e.preventDefault(); + toggleSidebar(); + }, + { enableOnFormTags: false, delimiter: "|" }, + ); + + // Go to Home + useHotkeys( + getKeys("go-home"), + (e: KeyboardEvent) => { + e.preventDefault(); + navigate("/home"); + }, + { enableOnFormTags: false }, + ); + // Go to Dashboard + useHotkeys( + getKeys("go-dashboard"), + (e: KeyboardEvent) => { + e.preventDefault(); + navigate("/dashboard"); + }, + { enableOnFormTags: false }, + ); + // Go to Analytics + useHotkeys( + getKeys("go-analytics"), + (e: KeyboardEvent) => { + e.preventDefault(); + navigate("/dashboard/analytics"); + }, + { enableOnFormTags: false }, + ); + // Go to Overview + useHotkeys( + getKeys("go-overview"), + (e: KeyboardEvent) => { + e.preventDefault(); + navigate("/dashboard/overview"); + }, + { enableOnFormTags: false }, + ); + // Go to Settings + useHotkeys( + getKeys("go-settings"), + (e: KeyboardEvent) => { + e.preventDefault(); + navigate("/settings"); + }, + { enableOnFormTags: false }, + ); + + // Show Keyboard Shortcuts + useHotkeys( + getKeys("show-hotkeys"), + (e: KeyboardEvent) => { + e.preventDefault(); + toggleHotkeysDialog(); + }, + { enableOnFormTags: false, useKey: true }, + ); +} diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts index 845615d..50068f2 100644 --- a/packages/core/src/lib/utils.ts +++ b/packages/core/src/lib/utils.ts @@ -17,3 +17,28 @@ export async function fetchLatestGithubVersion(): Promise { return null; } } + +/** + * Returns a human-readable label for a hotkey key string. + * Detects macOS to show ⌘ instead of Ctrl. + * Handles sequence strings (e.g. "g>s") and combinators (e.g. "mod+k"). + */ +export function formatHotkeyDisplay(keys: string): string[] { + const isMac = + typeof navigator !== "undefined" && + /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); + + // Split by either `+` or `>` to support sequences like "g>s" and chords like "mod+k" + return keys.split(/[+>]+/).map((key) => { + switch (key) { + case "mod": + return isMac ? "⌘" : "Ctrl"; + case "shift": + return isMac ? "⇧" : "Shift"; + case "alt": + return isMac ? "⌥" : "Alt"; + default: + return key.toUpperCase(); + } + }); +} diff --git a/packages/core/src/stores/hotkeys-store.ts b/packages/core/src/stores/hotkeys-store.ts new file mode 100644 index 0000000..850af70 --- /dev/null +++ b/packages/core/src/stores/hotkeys-store.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +interface HotkeysDialogState { + isOpen: boolean; + toggle: () => void; + open: () => void; + close: () => void; +} + +export const useHotkeysDialogStore = create()((set) => ({ + isOpen: false, + toggle: () => set((state) => ({ isOpen: !state.isOpen })), + open: () => set({ isOpen: true }), + close: () => set({ isOpen: false }), +})); diff --git a/packages/i18n/src/messages/de.json b/packages/i18n/src/messages/de.json index 9e57f0d..6270d92 100644 --- a/packages/i18n/src/messages/de.json +++ b/packages/i18n/src/messages/de.json @@ -69,5 +69,21 @@ "billing": "Abrechnung", "notifications": "Benachrichtigungen", "logOut": "Abmelden" + }, + "HotkeysDialog": { + "title": "Tastenkürzel", + "description": "Verfügbare Tastenkürzel für diese Anwendung.", + "general": "Allgemein", + "navigation": "Navigation", + "commandPalette": "Befehlspalette", + "toggleSidebar": "Seitenleiste umschalten", + "toggleMode": "Modus umschalten", + "showHotkeys": "Tastenkürzel anzeigen", + "goHome": "Zur Startseite gehen", + "goDashboard": "Zum Dashboard gehen", + "goAnalytics": "Zur Analyse gehen", + "goOverview": "Zur Übersicht gehen", + "goSettings": "Zu Einstellungen gehen", + "then": "dann" } } diff --git a/packages/i18n/src/messages/en.json b/packages/i18n/src/messages/en.json index 6489dbe..d18d7b3 100644 --- a/packages/i18n/src/messages/en.json +++ b/packages/i18n/src/messages/en.json @@ -69,5 +69,21 @@ "billing": "Billing", "notifications": "Notifications", "logOut": "Log out" + }, + "HotkeysDialog": { + "title": "Keyboard Shortcuts", + "description": "Available keyboard shortcuts for this application.", + "general": "General", + "navigation": "Navigation", + "commandPalette": "Command Palette", + "toggleSidebar": "Toggle Sidebar", + "toggleMode": "Toggle Mode", + "showHotkeys": "Show Shortcuts", + "goHome": "Go to Home", + "goDashboard": "Go to Dashboard", + "goAnalytics": "Go to Analytics", + "goOverview": "Go to Overview", + "goSettings": "Go to Settings", + "then": "then" } } diff --git a/packages/i18n/src/messages/es.json b/packages/i18n/src/messages/es.json index 1ff435a..92a9014 100644 --- a/packages/i18n/src/messages/es.json +++ b/packages/i18n/src/messages/es.json @@ -69,5 +69,21 @@ "billing": "Facturación", "notifications": "Notificaciones", "logOut": "Cerrar sesión" + }, + "HotkeysDialog": { + "title": "Atajos de Teclado", + "description": "Atajos de teclado disponibles para esta aplicación.", + "general": "General", + "navigation": "Navegación", + "commandPalette": "Paleta de Comandos", + "toggleSidebar": "Alternar barra lateral", + "toggleMode": "Alternar Modo", + "showHotkeys": "Mostrar atajos", + "goHome": "Ir a Inicio", + "goDashboard": "Ir al Tablero", + "goAnalytics": "Ir a Analíticas", + "goOverview": "Ir a Resumen", + "goSettings": "Ir a Ajustes", + "then": "luego" } } diff --git a/packages/i18n/src/messages/fr.json b/packages/i18n/src/messages/fr.json index 9c4bbe8..a8d0c24 100644 --- a/packages/i18n/src/messages/fr.json +++ b/packages/i18n/src/messages/fr.json @@ -69,5 +69,21 @@ "billing": "Facturation", "notifications": "Notifications", "logOut": "Se déconnecter" + }, + "HotkeysDialog": { + "title": "Raccourcis Clavier", + "description": "Raccourcis clavier disponibles pour cette application.", + "general": "Général", + "navigation": "Navigation", + "commandPalette": "Palette de Commandes", + "toggleSidebar": "Basculer la barre latérale", + "toggleMode": "Basculer le mode", + "showHotkeys": "Afficher les raccourcis", + "goHome": "Aller à l'Accueil", + "goDashboard": "Aller au tableau de bord", + "goAnalytics": "Aller à l'Analyse", + "goOverview": "Aller à l'Aperçu", + "goSettings": "Aller aux Paramètres", + "then": "puis" } } diff --git a/packages/i18n/src/messages/it.json b/packages/i18n/src/messages/it.json index e8c53cd..06d212e 100644 --- a/packages/i18n/src/messages/it.json +++ b/packages/i18n/src/messages/it.json @@ -69,5 +69,21 @@ "billing": "Fatturazione", "notifications": "Notifiche", "logOut": "Esci" + }, + "HotkeysDialog": { + "title": "Scorciatoie da Tastiera", + "description": "Scorciatoie da tastiera disponibili per questa applicazione.", + "general": "Generale", + "navigation": "Navigazione", + "commandPalette": "Tavolozza dei Comandi", + "toggleSidebar": "Attiva/Disattiva barra laterale", + "toggleMode": "Cambia Modalità", + "showHotkeys": "Mostra scorciatoie", + "goHome": "Vai alla Home", + "goDashboard": "Vai alla Dashboard", + "goAnalytics": "Vai all'Analisi", + "goOverview": "Vai alla Panoramica", + "goSettings": "Vai alle Impostazioni", + "then": "poi" } } diff --git a/packages/i18n/src/messages/ja.json b/packages/i18n/src/messages/ja.json index 9262e05..40d40f3 100644 --- a/packages/i18n/src/messages/ja.json +++ b/packages/i18n/src/messages/ja.json @@ -69,5 +69,21 @@ "billing": "請求", "notifications": "通知", "logOut": "ログアウト" + }, + "HotkeysDialog": { + "title": "キーボードショートカット", + "description": "このアプリケーションで利用可能なキーボードショートカット。", + "general": "一般", + "navigation": "ナビゲーション", + "commandPalette": "コマンドパレット", + "toggleSidebar": "サイドバーを切り替える", + "toggleMode": "モードを切り替える", + "showHotkeys": "ショートカットを表示", + "goHome": "ホームに戻る", + "goDashboard": "ダッシュボードに戻る", + "goAnalytics": "分析に移動", + "goOverview": "概要に移動", + "goSettings": "設定に行く", + "then": "次に" } } diff --git a/packages/i18n/src/messages/pt.json b/packages/i18n/src/messages/pt.json index 8cb7609..8f7cd07 100644 --- a/packages/i18n/src/messages/pt.json +++ b/packages/i18n/src/messages/pt.json @@ -69,5 +69,21 @@ "billing": "Faturamento", "notifications": "Notificações", "logOut": "Sair" + }, + "HotkeysDialog": { + "title": "Atalhos de Teclado", + "description": "Atalhos de teclado disponíveis para este aplicativo.", + "general": "Geral", + "navigation": "Navegação", + "commandPalette": "Paleta de Comandos", + "toggleSidebar": "Alternar barra lateral", + "toggleMode": "Alternar Modo", + "showHotkeys": "Mostrar atalhos", + "goHome": "Ir para o Início", + "goDashboard": "Ir para o Painel", + "goAnalytics": "Ir para Análises", + "goOverview": "Ir para Visão Geral", + "goSettings": "Ir para Configurações", + "then": "em seguida" } } diff --git a/packages/i18n/src/messages/ru.json b/packages/i18n/src/messages/ru.json index b870318..d83a60f 100644 --- a/packages/i18n/src/messages/ru.json +++ b/packages/i18n/src/messages/ru.json @@ -69,5 +69,21 @@ "billing": "Выставление счетов", "notifications": "Уведомления", "logOut": "Выйти" + }, + "HotkeysDialog": { + "title": "Сочетания Клавиш", + "description": "Доступные сочетания клавиш для этого приложения.", + "general": "Общие", + "navigation": "Навигация", + "commandPalette": "Палитра Команд", + "toggleSidebar": "Переключить боковую панель", + "toggleMode": "Переключить режим", + "showHotkeys": "Показать сочетания клавиш", + "goHome": "На Главную", + "goDashboard": "На панель управления", + "goAnalytics": "К аналитике", + "goOverview": "К обзору", + "goSettings": "Перейти к Настройкам", + "then": "затем" } } diff --git a/packages/i18n/src/messages/tr.json b/packages/i18n/src/messages/tr.json index 687fd58..e645ab8 100644 --- a/packages/i18n/src/messages/tr.json +++ b/packages/i18n/src/messages/tr.json @@ -69,5 +69,21 @@ "billing": "Faturalama", "notifications": "Bildirimler", "logOut": "Çıkış Yap" + }, + "HotkeysDialog": { + "title": "Klavye Kısayolları", + "description": "Bu uygulama için kullanılabilir klavye kısayolları.", + "general": "Genel", + "navigation": "Navigasyon", + "commandPalette": "Komut Paleti", + "toggleSidebar": "Kenar Çubuğunu Aç/Kapat", + "toggleMode": "Modu Değiştir", + "showHotkeys": "Kısayolları Göster", + "goHome": "Ana Sayfaya Git", + "goDashboard": "Kontrol Paneline Git", + "goAnalytics": "Analize Git", + "goOverview": "Genel Bakışa Git", + "goSettings": "Ayarlara Git", + "then": "sonra" } } diff --git a/packages/i18n/src/messages/zh.json b/packages/i18n/src/messages/zh.json index 57a4e70..79e88d8 100644 --- a/packages/i18n/src/messages/zh.json +++ b/packages/i18n/src/messages/zh.json @@ -69,5 +69,21 @@ "billing": "账单", "notifications": "通知", "logOut": "退出登录" + }, + "HotkeysDialog": { + "title": "键盘快捷键", + "description": "此应用程序可用的键盘快捷键。", + "general": "常规", + "navigation": "导航", + "commandPalette": "命令面板", + "toggleSidebar": "切换侧边栏", + "toggleMode": "切换模式", + "showHotkeys": "显示快捷键", + "goHome": "转到主页", + "goDashboard": "转到仪表板", + "goAnalytics": "转到分析", + "goOverview": "转到概览", + "goSettings": "转到设置", + "then": "然后" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 004f311..0f342b3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,7 +29,8 @@ "react-dom": "^19.2.4", "react-use-measure": "^2.1.7", "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/postcss": "^4.2.2", diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx new file mode 100644 index 0000000..fb03592 --- /dev/null +++ b/packages/ui/src/components/dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "@workspace/ui/lib/utils" +import { Button } from "@workspace/ui/components/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/packages/ui/src/components/drawer.tsx b/packages/ui/src/components/drawer.tsx new file mode 100644 index 0000000..c153ead --- /dev/null +++ b/packages/ui/src/components/drawer.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@workspace/ui/lib/utils" + +function Drawer({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/packages/ui/src/components/kbd.tsx b/packages/ui/src/components/kbd.tsx new file mode 100644 index 0000000..9b0f85c --- /dev/null +++ b/packages/ui/src/components/kbd.tsx @@ -0,0 +1,28 @@ +import { cn } from "@workspace/ui/lib/utils" + +function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { + return ( + + ) +} + +function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { Kbd, KbdGroup } diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx index 83e9486..fe99490 100644 --- a/packages/ui/src/components/sidebar.tsx +++ b/packages/ui/src/components/sidebar.tsx @@ -30,7 +30,6 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; type SidebarContextProps = { state: "expanded" | "collapsed"; @@ -93,22 +92,6 @@ function SidebarProvider({ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. const state = open ? "expanded" : "collapsed"; diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 2fd40ac..042733a 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -191,6 +191,16 @@ } } +@layer utilities { + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } +} + [data-slot="sidebar-menu-button"][data-active="true"] { background-color: var(--primary) !important; color: var(--primary-foreground) !important; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a3d9a7..562c737 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,6 +230,9 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + react-hotkeys-hook: + specifier: ^5.2.4 + version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-use-measure: specifier: ^2.1.7 version: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -399,6 +402,9 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: '@tailwindcss/postcss': specifier: ^4.2.2 @@ -4919,6 +4925,12 @@ packages: peerDependencies: react: ^19.2.4 + react-hotkeys-hook@5.2.4: + resolution: {integrity: sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5491,6 +5503,12 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -10330,6 +10348,11 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-hotkeys-hook@5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is@16.13.1: {} react-medium-image-zoom@5.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -11107,6 +11130,15 @@ snapshots: uuid@11.1.0: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3