Skip to content

Commit

Permalink
feat: keyboard shortcut system and ui enhancements (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
cesconix authored Sep 2, 2024
1 parent 16cd892 commit f7dc713
Show file tree
Hide file tree
Showing 59 changed files with 1,442 additions and 462 deletions.
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

0 comments on commit f7dc713

Please sign in to comment.