Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: keyboard shortcut system and ui enhancements #38

Merged
merged 2 commits into from
Sep 2, 2024
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
5 changes: 5 additions & 0 deletions .changeset/tasty-peaches-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pinorama-studio": minor
---

keyboard shortcut system and ui enhancements
4 changes: 4 additions & 0 deletions packages/pinorama-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
"@fastify/static": "^7.0.4",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-query": "^5.50.1",
Expand All @@ -58,7 +60,9 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-hotkeys-hook": "^4.5.0",
"react-intl": "^6.6.8",
"react-json-view-lite": "^1.4.0",
"react-resizable-panels": "^2.0.20",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CheckIcon, CopyIcon } from "lucide-react"
import { forwardRef, useCallback, useImperativeHandle, useState } from "react"
import { useIntl } from "react-intl"
import { IconButton } from "../icon-button/icon-button"

type ClipboardButtonProps = {
textToCopy: string
keystroke?: string
}

export type ImperativeClipboardButtonHandle = {
copyToClipboard: () => void
}

export const ClipboardButton = forwardRef<
ImperativeClipboardButtonHandle,
ClipboardButtonProps
>(function ClipboardButton(props, ref) {
const intl = useIntl()
const [isCopied, setIsCopied] = useState(false)

const handleClick = useCallback(async () => {
if (isCopied || !props.textToCopy) {
return
}

try {
await navigator.clipboard.writeText(props.textToCopy)
setIsCopied(true)

setTimeout(() => {
setIsCopied(false)
}, 1500)
} catch (err) {
console.error("Failed to copy to clipboard", err)
}
}, [props.textToCopy, isCopied])

useImperativeHandle(
ref,
() => ({
copyToClipboard: handleClick
}),
[handleClick]
)

return (
<IconButton
variant="ghost"
keystroke={props.keystroke}
disabled={!props.textToCopy}
icon={isCopied ? CheckIcon : CopyIcon}
aria-label={intl.formatMessage({ id: "labels.copyToClipboard" })}
tooltip={intl.formatMessage({ id: "labels.copyToClipboard" })}
onClick={handleClick}
/>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./clipboard-button"
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { LoaderIcon, type LucideIcon } from "lucide-react"
import type { ComponentProps } from "react"
import { Kbd } from "../kbd/kbd"

type IconButtonProps = ComponentProps<typeof Button> & {
icon: LucideIcon
tooltip?: string
loading?: boolean
keystroke?: string
}

export function IconButton({
variant = "outline2",
icon,
tooltip,
keystroke,
loading,
...props
}: IconButtonProps) {
const Icon = loading ? LoaderIcon : icon

const Component = (
<Button className="px-2.5" variant={variant} disabled={loading} {...props}>
<Icon className={cn("h-[18px] w-[18px]", loading && "animate-spin")} />
</Button>
)

return tooltip || keystroke
? withTooltip(Component, tooltip, keystroke)
: Component
}

function withTooltip(
WrappedComponent: React.ReactNode,
tooltip?: string,
keystroke?: string
) {
return (
<Tooltip>
<TooltipTrigger asChild>{WrappedComponent}</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="flex space-x-1.5">
<div>{tooltip}</div>
{keystroke ? <Kbd>{keystroke}</Kbd> : null}
</TooltipContent>
</TooltipPortal>
</Tooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./icon-button"
1 change: 1 addition & 0 deletions packages/pinorama-studio/src/components/kbd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./kbd"
17 changes: 17 additions & 0 deletions packages/pinorama-studio/src/components/kbd/kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { cn } from "@/lib/utils"

export function Kbd({
children,
className
}: { children: React.ReactNode; className?: string }) {
return (
<kbd
className={cn(
"h-5 items-center rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground opacity-100",
className
)}
>
{children}
</kbd>
)
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
import { SearchIcon, XIcon } from "lucide-react"
import { forwardRef } from "react"
import { Kbd } from "../kbd"
import { Button } from "../ui/button"
import { Input } from "../ui/input"

type SearchInputProps = {
value: string
onChange: (text: string) => void
placeholder: string
keystroke?: string
}

export const SearchInput = forwardRef(function SearchInput(
props: SearchInputProps,
ref: React.Ref<HTMLInputElement>
) {
const hasValue = props.value.length > 0

return (
<div className="relative flex items-center w-full">
<SearchIcon className="h-4 w-4 absolute left-3 text-muted-foreground" />
<Input
ref={ref}
type="text"
placeholder={props.placeholder}
className="pl-9"
className="pl-9 pr-16"
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape" && e.target instanceof HTMLInputElement) {
e.target.blur()
}
}}
/>
{props.value.length > 0 ? (
{hasValue ? (
<Button
variant={"ghost"}
size={"xs"}
aria-label="Clear"
className="absolute right-2"
className="absolute right-8"
onClick={() => props.onChange("")}
>
<XIcon className="h-4 w-4" />
</Button>
) : null}
{props.keystroke ? (
<Kbd className="absolute right-2">{props.keystroke}</Kbd>
) : null}
</div>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Kbd } from "@/components/kbd/kbd"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog"
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger
} from "@/components/ui/tooltip"
import { useAllModuleHotkeys } from "@/hooks/use-module-hotkeys"
import { KeyboardIcon } from "lucide-react"
import { FormattedMessage } from "react-intl"

export function HotkeysButton() {
const hotkeys = useAllModuleHotkeys()

const handleClick = () => {}

return (
<Dialog>
<Tooltip>
<TooltipTrigger>
<DialogTrigger asChild>
<Button
aria-label={"Settings"}
variant={"secondary"}
size={"sm"}
onClick={handleClick}
>
<KeyboardIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
<FormattedMessage id="labels.keyboardShortcuts" />
</TooltipContent>
</TooltipPortal>
</Tooltip>
<DialogContent className="w-80">
<DialogHeader>
<DialogTitle>
<FormattedMessage id="labels.keyboardShortcuts" />
</DialogTitle>
<DialogDescription>
<FormattedMessage id="labels.allShortcutsSeparatedByModule" />
</DialogDescription>
</DialogHeader>
{Object.entries(hotkeys).map(([module, hotkeys]) => (
<div key={module}>
<div className="text-sm font-semibold py-2">{module}</div>
<ul className="space-y-2">
{hotkeys.map((hotkey) => (
<li
key={hotkey.keystroke}
className="flex items-center justify-between"
>
<span className="text-muted-foreground text-sm">
{hotkey.description}
</span>
<Kbd>{hotkey.keystroke}</Kbd>
</li>
))}
</ul>
</div>
))}
</DialogContent>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConnectionStatusButton } from "./components/connection-status-button"
import { ConnectionToggleButton } from "./components/connection-toggle-button"
import { HotkeysButton } from "./components/hotkeys-button"
import { PinoramaLogo } from "./components/pinorama-logo"
import { ThemeToggleButton } from "./components/theme-toggle-button"

Expand All @@ -19,6 +20,7 @@ export function TitleBar() {

{/* Right */}
<div className="flex items-center space-x-1.5">
<HotkeysButton />
<ThemeToggleButton />
<ConnectionToggleButton />
</div>
Expand Down
Loading