command palette tweaks

This commit is contained in:
miloschwartz
2026-07-07 21:55:15 -04:00
parent 1e7863ce4f
commit d082de3d88
12 changed files with 182 additions and 39 deletions
+16 -13
View File
@@ -1519,7 +1519,7 @@
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
"navbarDocsLink": "Documentation",
"commandPaletteTitle": "Command palette",
"commandPaletteTitle": "Command Palette",
"commandPaletteDescription": "Search for pages, organizations, resources, and actions",
"commandPaletteSearchPlaceholder": "Search pages, resources, actions...",
"commandPaletteNoResults": "No results found.",
@@ -1529,17 +1529,18 @@
"commandPaletteSites": "Sites",
"commandPaletteResources": "Resources",
"commandPaletteUsers": "Users",
"commandPaletteClients": "Machine clients",
"commandPaletteClients": "Machine Clients",
"commandPaletteActions": "Actions",
"commandPaletteCreateSite": "Create site",
"commandPaletteCreateProxyResource": "Create public resource",
"commandPaletteCreateUser": "Create user",
"commandPaletteCreateApiKey": "Create API key",
"commandPaletteCreateMachineClient": "Create machine client",
"commandPaletteCreateAlertRule": "Create alert rule",
"commandPaletteCreateIdentityProvider": "Create identity provider",
"commandPaletteToggleTheme": "Toggle theme",
"commandPaletteChooseOrganization": "Choose organization",
"commandPaletteCreateSite": "Create Site",
"commandPaletteCreateProxyResource": "Create Public Resource",
"commandPaletteCreatePrivateResource": "Create Private Resource",
"commandPaletteCreateUser": "Create User",
"commandPaletteCreateApiKey": "Create API Key",
"commandPaletteCreateMachineClient": "Create Machine Client",
"commandPaletteCreateAlertRule": "Create Alert Rule",
"commandPaletteCreateIdentityProvider": "Create Identity Provider",
"commandPaletteToggleTheme": "Toggle Theme",
"commandPaletteChooseOrganization": "Choose Organization",
"commandPaletteShortcutMac": "⌘K",
"commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Unable to enable 2FA",
@@ -1620,7 +1621,7 @@
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
"commandSites": "Sites",
"commandActionModeInfo": "Type \">\" to open action mode",
"commandActionModeInfo": "Type \">\" To Open Action Mode",
"commandResources": "Resources",
"commandProxyResources": "Public Resources",
"commandClientResources": "Private Resources",
@@ -1655,6 +1656,8 @@
"commandBilling": "Billing",
"commandEnterpriseLicenses": "Licenses",
"commandSettings": "Settings",
"commandLauncher": "Launcher",
"commandResourceLauncher": "Resource Launcher",
"commandSearchResults": "Search Results",
"alertingTitle": "Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications",
@@ -3262,7 +3265,7 @@
"editInternalResourceDialogAddUsers": "Add Users",
"editInternalResourceDialogAddClients": "Add Clients",
"editInternalResourceDialogDestinationLabel": "Destination",
"editInternalResourceDialogDestinationDescription": "Choose where this resource runs and how clients reach it. Selecting multiple sites will create a high availability resource that can be accessed from any of the selected sites.",
"editInternalResourceDialogDestinationDescription": "Choose where this resource runs and how clients reach it",
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
+7
View File
@@ -1,9 +1,11 @@
import { Layout } from "@app/components/Layout";
import ResourceLauncher from "@app/components/resource-launcher/ResourceLauncher";
import { commandBarNavSections } from "@app/app/navigation";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { fetchLauncherPageData } from "@app/lib/launcherServerData";
import { verifySession } from "@app/lib/auth/verifySession";
import { pullEnv } from "@app/lib/pullEnv";
import UserProvider from "@app/providers/UserProvider";
import { ListUserOrgsResponse } from "@server/routers/org";
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
@@ -57,6 +59,8 @@ export default async function OrgPage(props: OrgPageProps) {
} catch (e) {}
const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner);
const env = pullEnv();
const primaryOrg = orgs.find((o) => o.orgId === orgId)?.isPrimaryOrg;
const searchParams = new URLSearchParams(await props.searchParams);
const launcherData = overview
@@ -73,6 +77,9 @@ export default async function OrgPage(props: OrgPageProps) {
orgId={orgId}
orgs={orgs}
navItems={[]}
commandNavItems={commandBarNavSections(env, {
isPrimaryOrg: primaryOrg
})}
showSidebar={false}
launcherMode
showViewAsAdmin={isAdminOrOwner}
+12 -1
View File
@@ -15,6 +15,7 @@ import {
GlobeLock,
KeyRound,
Laptop,
LayoutGrid,
Link as LinkIcon,
Logs,
MonitorUp,
@@ -49,7 +50,7 @@ export const orgLangingNavItems: SidebarNavItem[] = [
{
title: "sidebarAccount",
href: "/{orgId}",
icon: <User className="size-4 flex-none" />
icon: <LayoutGrid className="size-4 flex-none" />
}
];
@@ -360,6 +361,16 @@ export const commandBarNavSections = (
env?: Env,
options?: OrgNavSectionsOptions
): CommandBarNavSection[] => [
{
heading: "commandLauncher",
items: [
{
title: "commandResourceLauncher",
href: "/{orgId}",
icon: <LayoutGrid className="size-4 flex-none" />
}
]
},
{
heading: "network",
items: [
+4 -1
View File
@@ -105,8 +105,11 @@ export function LayoutHeader({
{showTopBar && (
<div className="flex items-center space-x-2">
<CommandPaletteTrigger />
<ThemeSwitcher />
<CommandPaletteTrigger
orgId={orgId}
orgs={orgs}
/>
<ProfileIcon />
</div>
)}
+7 -3
View File
@@ -17,7 +17,7 @@ import {
import { useUserContext } from "@app/hooks/useUserContext";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
import { Menu, Server, Settings, SquareMousePointer } from "lucide-react";
import { Menu, Server, Settings, LayoutGrid } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -151,7 +151,7 @@ export function LayoutMobileMenu({
}
>
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground mr-3">
<SquareMousePointer className="h-4 w-4" />
<LayoutGrid className="h-4 w-4" />
</span>
<span className="flex-1">
{t(
@@ -207,7 +207,11 @@ export function LayoutMobileMenu({
{showTopBar && (
<div className="ml-auto flex items-center justify-end">
<div className="flex items-center space-x-2">
<CommandPaletteTrigger variant="mobile" />
<CommandPaletteTrigger
variant="mobile"
orgId={orgId}
orgs={orgs}
/>
<ThemeSwitcher />
<ProfileIcon />
</div>
+7 -5
View File
@@ -21,9 +21,9 @@ import { ListUserOrgsResponse } from "@server/routers/org";
import {
ArrowRight,
ExternalLink,
LayoutGrid,
PanelRightOpen,
Server,
SquareMousePointer
Server
} from "lucide-react";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
@@ -186,7 +186,7 @@ export function LayoutSidebar({
)}
>
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground">
<SquareMousePointer className="h-4 w-4" />
<LayoutGrid className="h-4 w-4" />
</span>
</Link>
</TooltipTrigger>
@@ -195,7 +195,9 @@ export function LayoutSidebar({
sideOffset={8}
>
<p>
{t("resourceSidebarLauncherTitle")}
{t(
"resourceSidebarLauncherTitle"
)}
</p>
</TooltipContent>
</Tooltip>
@@ -208,7 +210,7 @@ export function LayoutSidebar({
)}
>
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center text-muted-foreground">
<SquareMousePointer className="h-4 w-4" />
<LayoutGrid className="h-4 w-4" />
</span>
<span className="flex-1">
{t("resourceSidebarLauncherTitle")}
@@ -13,7 +13,9 @@ import {
CommandList,
CommandSeparator
} from "@app/components/ui/command";
import { Button } from "@app/components/ui/button";
import { cn } from "@app/lib/cn";
import { useMediaQuery } from "@app/hooks/useMediaQuery";
import { ListUserOrgsResponse } from "@server/routers/org";
import {
ChevronRightIcon,
@@ -22,7 +24,8 @@ import {
LaptopIcon,
PlugIcon,
ServerIcon,
UserIcon
UserIcon,
X
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
@@ -34,6 +37,7 @@ import React, {
useMemo,
useState
} from "react";
import { useCanUseCommandPalette } from "./useCanUseCommandPalette";
import { useCommandPaletteActions } from "./useCommandPaletteActions";
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
@@ -57,6 +61,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
const router = useRouter();
const { open, setOpen } = useCommandPalette();
const [search, setSearch] = useState("");
const isDesktop = useMediaQuery("(min-width: 768px)");
const isActionMode = search.startsWith(">");
@@ -134,6 +139,20 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
value={search}
onValueChange={setSearch}
isLoading={!isActionMode && shouldSearch && isLoading}
trailing={
!isDesktop ? (
<Button
type="button"
variant="ghost"
size="icon"
className="size-8 shrink-0"
aria-label={t("close")}
onClick={() => handleOpenChange(false)}
>
<X className="size-4" />
</Button>
) : undefined
}
/>
<CommandList className="max-h-118 min-h-0 h-(--cmdk-list-height) scroll-pb-4 scroll-pt-2 transition-[height] duration-250 ease-in-out">
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
@@ -427,13 +446,14 @@ function isEditableTarget(target: EventTarget | null) {
);
}
export function CommandPaletteProvider({
function CommandPaletteProviderInner({
children,
orgId,
orgs,
navItems
}: CommandPaletteProviderProps) {
const [open, setOpen] = useState(false);
const canUseCommandPalette = useCanUseCommandPalette(orgId, orgs);
const toggle = useCallback(() => {
setOpen((current) => !current);
@@ -449,6 +469,10 @@ export function CommandPaletteProvider({
);
useEffect(() => {
if (!canUseCommandPalette) {
return;
}
function onKeyDown(event: KeyboardEvent) {
if (
event.key.toLowerCase() !== "k" ||
@@ -467,12 +491,18 @@ export function CommandPaletteProvider({
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [open, toggle]);
}, [canUseCommandPalette, open, toggle]);
return (
<CommandPaletteContext value={contextValue}>
{children}
<CommandPalette orgId={orgId} orgs={orgs} navItems={navItems} />
{canUseCommandPalette ? (
<CommandPalette orgId={orgId} orgs={orgs} navItems={navItems} />
) : null}
</CommandPaletteContext>
);
}
export function CommandPaletteProvider(props: CommandPaletteProviderProps) {
return <CommandPaletteProviderInner {...props} />;
}
@@ -6,11 +6,15 @@ import { cn } from "@app/lib/cn";
import { Search } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { ListUserOrgsResponse } from "@server/routers/org";
import { useCommandPalette } from "./CommandPalette";
import { useCanUseCommandPalette } from "./useCanUseCommandPalette";
type CommandPaletteTriggerProps = {
variant?: "header" | "mobile";
className?: string;
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
};
function useIsMac() {
@@ -25,11 +29,18 @@ function useIsMac() {
export function CommandPaletteTrigger({
variant = "header",
className
className,
orgId,
orgs
}: CommandPaletteTriggerProps) {
const t = useTranslations();
const { setOpen } = useCommandPalette();
const isMac = useIsMac();
const canUseCommandPalette = useCanUseCommandPalette(orgId, orgs);
if (!canUseCommandPalette) {
return null;
}
if (variant === "mobile") {
return (
@@ -49,13 +60,13 @@ export function CommandPaletteTrigger({
<Button
variant="outline"
className={cn(
"hidden h-9 w-56 justify-start gap-2 px-3 text-muted-foreground md:flex lg:w-64",
"relative hidden h-9 w-56 justify-start pl-8 pr-3 text-muted-foreground md:flex lg:w-64",
className
)}
aria-label={t("commandPaletteTitle")}
onClick={() => setOpen(true)}
>
<Search className="size-4 shrink-0 opacity-50" />
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<span className="flex-1 truncate text-left text-sm font-normal">
{t("commandPaletteSearchPlaceholder")}
</span>
@@ -0,0 +1,24 @@
"use client";
import { useUserContext } from "@app/hooks/useUserContext";
import { ListUserOrgsResponse } from "@server/routers/org";
import { usePathname } from "next/navigation";
export function useCanUseCommandPalette(
orgId?: string,
orgs?: ListUserOrgsResponse["orgs"]
) {
const pathname = usePathname();
const { user } = useUserContext();
if (pathname?.startsWith("/admin")) {
return user.serverAdmin;
}
if (!orgId) {
return false;
}
const currentOrg = orgs?.find((org) => org.orgId === orgId);
return Boolean(currentOrg?.isAdmin || currentOrg?.isOwner);
}
@@ -7,6 +7,7 @@ import {
BellRing,
Building2,
Globe,
GlobeLock,
KeyRound,
MonitorUp,
Plus,
@@ -86,6 +87,12 @@ export function useCommandPaletteActions(
icon: <Globe className="size-4" />,
href: `/${orgId}/settings/resources/proxy/create`
});
actions.push({
id: "create-private-resource",
label: t("commandPaletteCreatePrivateResource"),
icon: <GlobeLock className="size-4" />,
href: `/${orgId}/settings/resources/private/create`
});
actions.push({
id: "create-machine-client",
label: t("commandPaletteCreateMachineClient"),
@@ -130,7 +130,7 @@ export function useCommandPaletteSearch({
return privateResourcesQuery.data.map((resource) => ({
id: `site-resource-${resource.siteResourceId}`,
name: resource.name,
href: `/${orgId}/settings/resources/private?query=${resource.name}`
href: `/${orgId}/settings/resources/private/${resource.niceId}`
}));
}, [orgId, privateResourcesQuery.data]);
+49 -8
View File
@@ -11,8 +11,21 @@ import {
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle
} from "@/components/ui/sheet";
import { useMediaQuery } from "@app/hooks/useMediaQuery";
import { cn } from "@app/lib/cn";
const desktop = "(min-width: 768px)";
const commandSurfaceClassName =
"transition duration-150 mt-0 [&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5";
function Command({
className,
...props
@@ -35,6 +48,7 @@ function CommandDialog({
children,
className,
showCloseButton = true,
commandProps,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
@@ -43,6 +57,35 @@ function CommandDialog({
showCloseButton?: boolean;
commandProps?: React.ComponentProps<typeof Command>;
}) {
const isDesktop = useMediaQuery(desktop);
const command = (
<Command {...commandProps} className={commandSurfaceClassName}>
{children}
</Command>
);
if (!isDesktop) {
return (
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
<SheetContent
side="top"
className={cn(
"flex max-h-[85dvh] w-full flex-col gap-0 overflow-hidden rounded-none border-x-0 p-0 pt-[env(safe-area-inset-top,0px)]",
className
)}
onOpenAutoFocus={(event) => event.preventDefault()}
>
<SheetHeader className="sr-only">
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
{command}
</SheetContent>
</Sheet>
);
}
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
@@ -51,16 +94,11 @@ function CommandDialog({
</DialogHeader>
<DialogContent
className={cn(
"overflow-hidden p-0 place-items-start",
"overflow-hidden p-0 place-items-start md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100dvh-clamp(1.5rem,12vh,200px)-1.5rem)] md:translate-y-0",
className
)}
>
<Command
{...props.commandProps}
className="transition duration-150 mt-0 [&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
>
{children}
</Command>
{command}
</DialogContent>
</Dialog>
);
@@ -69,9 +107,11 @@ function CommandDialog({
function CommandInput({
className,
isLoading,
trailing,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input> & {
isLoading?: boolean;
trailing?: React.ReactNode;
}) {
return (
<div
@@ -86,11 +126,12 @@ function CommandInput({
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-muted-foreground flex h-10 min-w-0 flex-1 rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
{trailing ? <div className="shrink-0">{trailing}</div> : null}
</div>
);
}