[WEB-4733] dev: propel toolbar component (#7742)

* dev: toolbar component added to propel

* dev: toolbar story added

* chore: propel config updated

* chore: code refactor

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2025-09-10 00:15:10 +05:30 committed by GitHub
parent 5a63e6dad2
commit 3b8bb1effc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 292 additions and 0 deletions

View file

@ -38,6 +38,7 @@
"./table": "./dist/table/index.js",
"./tabs": "./dist/tabs/index.js",
"./toast": "./dist/toast/index.js",
"./toolbar": "./dist/toolbar/index.js",
"./tooltip": "./dist/tooltip/index.js",
"./utils": "./dist/utils/index.js"
},

View file

@ -0,0 +1,8 @@
export { Toolbar } from "./toolbar";
export type {
ToolbarProps,
ToolbarGroupProps,
ToolbarItemProps,
ToolbarSeparatorProps,
ToolbarSubmitButtonProps,
} from "./toolbar";

View file

@ -0,0 +1,123 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import {
Bold,
Italic,
Underline,
Strikethrough,
Code,
Link,
List,
ListOrdered,
Quote,
AlignLeft,
AlignCenter,
AlignRight,
Undo,
Redo,
Globe2,
Lock,
} from "lucide-react";
import { Toolbar } from "./toolbar";
const meta: Meta<typeof Toolbar> = {
title: "Components/Toolbar",
component: Toolbar,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof Toolbar>;
export const Default: Story = {
render: () => (
<div className="p-4 space-y-4">
<div className="w-96 border rounded">
<Toolbar>
<Toolbar.Group isFirst>
<Toolbar.Item icon={Undo} tooltip="Undo" />
<Toolbar.Item icon={Redo} tooltip="Redo" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Bold} tooltip="Bold" />
<Toolbar.Item icon={Italic} tooltip="Italic" />
<Toolbar.Item icon={Underline} tooltip="Underline" />
<Toolbar.Item icon={Strikethrough} tooltip="Strikethrough" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={AlignLeft} tooltip="Align Left" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" />
<Toolbar.Item icon={AlignRight} tooltip="Align Right" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Link} tooltip="Link" />
<Toolbar.Item icon={Code} tooltip="Code" />
</Toolbar.Group>
</Toolbar>
</div>
</div>
),
};
export const WithActiveStates: Story = {
render: () => (
<div className="p-4">
<Toolbar>
<Toolbar.Group isFirst>
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} isActive />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Underline} tooltip="Underline" shortcut={["Cmd", "U"]} isActive />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" isActive />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={AlignLeft} tooltip="Align Left" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" isActive />
<Toolbar.Item icon={AlignRight} tooltip="Align Right" />
</Toolbar.Group>
</Toolbar>
</div>
),
};
export const CommentToolbar: Story = {
render: () => (
<div className="p-4 space-y-4">
<h3 className="text-sm font-medium">Comment Toolbar with Access Control</h3>
<div className="rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar>
{/* Access Specifier */}
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar.Item icon={Lock} tooltip="Private" isActive />
<Toolbar.Item icon={Globe2} tooltip="Public" />
</div>
<div className="flex w-full items-stretch justify-between gap-2 rounded border-[0.5px] border-custom-border-200 p-1">
<div className="flex items-stretch">
<Toolbar.Group isFirst>
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Code} tooltip="Code" shortcut={["Cmd", "`"]} />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
</Toolbar.Group>
</div>
<Toolbar.SubmitButton>Comment</Toolbar.SubmitButton>
</div>
</Toolbar>
</div>
</div>
),
};

View file

@ -0,0 +1,159 @@
import * as React from "react";
import { LucideIcon } from "lucide-react";
import { Tooltip } from "../tooltip";
import { cn } from "../utils";
export interface ToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
className?: string;
}
export interface ToolbarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
className?: string;
isFirst?: boolean;
}
export interface ToolbarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: LucideIcon;
isActive?: boolean;
tooltip?: string;
shortcut?: string[];
className?: string;
children?: React.ReactNode;
}
export interface ToolbarSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
export interface ToolbarSubmitButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive";
className?: string;
children: React.ReactNode;
}
const ToolbarRoot = React.forwardRef<HTMLDivElement, ToolbarProps>(({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn("flex h-9 w-full items-stretch gap-1.5 bg-custom-background-90 overflow-x-scroll", className)}
{...props}
>
{children}
</div>
));
const ToolbarGroup = React.forwardRef<HTMLDivElement, ToolbarGroupProps>(
({ className, children, isFirst = false, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex items-stretch gap-0.5 border-r border-custom-border-200 px-2.5",
{
"pl-0": isFirst,
},
className
)}
{...props}
>
{children}
</div>
)
);
const ToolbarItem = React.forwardRef<HTMLButtonElement, ToolbarItemProps>(
({ icon: Icon, isActive = false, tooltip, shortcut, className, children, ...props }, ref) => {
const button = (
<button
ref={ref}
type="button"
className={cn(
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80 transition-colors",
{
"bg-custom-background-80 text-custom-text-100": isActive,
},
className
)}
{...props}
>
<Icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": isActive,
})}
strokeWidth={2.5}
/>
{children}
</button>
);
if (tooltip) {
return (
<Tooltip
tooltipContent={
<div className="flex flex-col gap-1 text-center text-xs">
<span className="font-medium">{tooltip}</span>
{shortcut && <kbd className="text-custom-text-400">{shortcut.join(" + ")}</kbd>}
</div>
}
>
{button}
</Tooltip>
);
}
return button;
}
);
const ToolbarSeparator = React.forwardRef<HTMLDivElement, ToolbarSeparatorProps>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("h-full w-px bg-custom-border-200 mx-1", className)} {...props} />
));
const buttonVariants = {
primary: "bg-custom-primary-100 text-white hover:bg-custom-primary-200 focus:bg-custom-primary-200",
secondary:
"bg-custom-background-100 text-custom-text-200 border border-custom-border-200 hover:bg-custom-background-90 focus:bg-custom-background-90",
outline:
"border border-custom-primary-100 text-custom-primary-100 bg-transparent hover:bg-custom-primary-100/10 focus:bg-custom-primary-100/20",
ghost: "text-custom-text-200 hover:bg-custom-background-90 focus:bg-custom-background-90",
destructive: "bg-red-500 text-white hover:bg-red-600 focus:bg-red-600",
};
const ToolbarSubmitButton = React.forwardRef<HTMLButtonElement, ToolbarSubmitButtonProps>(
({ loading = false, variant = "primary", className, children, disabled, ...props }, ref) => (
<div className="sticky right-1">
<button
ref={ref}
className={cn(
"inline-flex items-center justify-center gap-2 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors duration-200",
"focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-2",
"disabled:opacity-50 disabled:pointer-events-none",
buttonVariants[variant],
className
)}
disabled={disabled || loading}
{...props}
>
{loading && <div className="h-3 w-3 animate-spin rounded-full border border-current border-t-transparent" />}
{children}
</button>
</div>
)
);
ToolbarRoot.displayName = "ToolbarRoot";
ToolbarGroup.displayName = "ToolbarGroup";
ToolbarItem.displayName = "ToolbarItem";
ToolbarSeparator.displayName = "ToolbarSeparator";
ToolbarSubmitButton.displayName = "ToolbarSubmitButton";
// compound components
const Toolbar = Object.assign(ToolbarRoot, {
Group: ToolbarGroup,
Item: ToolbarItem,
Separator: ToolbarSeparator,
SubmitButton: ToolbarSubmitButton,
});
export { Toolbar };

View file

@ -22,6 +22,7 @@ export default defineConfig({
"src/table/index.ts",
"src/tabs/index.ts",
"src/toast/index.ts",
"src/toolbar/index.ts",
"src/tooltip/index.ts",
"src/utils/index.ts",
],