diff --git a/examples/storybook/postcss.config.js b/examples/storybook/postcss.config.js index 70cccb4..2aa7205 100644 --- a/examples/storybook/postcss.config.js +++ b/examples/storybook/postcss.config.js @@ -1,4 +1,6 @@ -export const plugins = { - tailwindcss: {}, - autoprefixer: {}, +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/examples/storybook/src/stories/Tooltip.stories.tsx b/examples/storybook/src/stories/Tooltip.stories.tsx new file mode 100644 index 0000000..c3f0651 --- /dev/null +++ b/examples/storybook/src/stories/Tooltip.stories.tsx @@ -0,0 +1,112 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import * as React from "react"; +import { Tooltip, type TooltipProps } from "@synopsisapp/symbiosis-ui"; + +const meta: Meta = { + title: "Components/Tooltip", + component: Tooltip.Root, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Uncontrolled: Story = { + render: (args: TooltipProps["Root"]) => ( + + + Hover me + + + + ), + argTypes: { + defaultOpen: { + control: false, + table: { + defaultValue: { summary: "false" }, + type: { summary: "boolean" }, + }, + description: + "The open state of the tooltip when it is initially rendered. Use when you do not need to control its open state.", + required: false, + }, + open: { + control: { + type: "boolean", + }, + table: { + defaultValue: { summary: "false" }, + }, + type: "boolean", + description: "The controlled open state of the tooltip. Must be used in conjunction with onOpenChange.", + defaultValue: false, + required: false, + }, + onOpenChange: { + table: { + type: { summary: "(open: boolean) => void" }, + }, + description: "Event handler called when the open state of the tooltip changes.", + control: false, + required: false, + }, + }, +}; + +export const Controlled: Story = { + render: (args) => { + const [open, setOpen] = React.useState(false); + return ( + + + Hover me + + + + ); + }, + parameters: { + docs: { + source: { + code: ` +const ControlledTooltip = () => { + const [open, setOpen] = React.useState(false); + + return ( + + + Hover me + + + + ); +}; + `, + language: "jsx", + type: "code", + }, + }, + }, +}; + +export const CustomContent: Story = { + render: (args: TooltipProps["Root"]) => ( + + + Hover me + + +
Popover content
+
+
+ ), +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 834b8cb..265abb3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -45,6 +45,7 @@ "peerDependencies": { "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", "@tailwindcss/typography": "^0.5.13", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/packages/ui/src/components/Tooltip/index.tsx b/packages/ui/src/components/Tooltip/index.tsx new file mode 100644 index 0000000..aa815b2 --- /dev/null +++ b/packages/ui/src/components/Tooltip/index.tsx @@ -0,0 +1,51 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { Text } from "../Text"; +import { cn } from "../../utils/cn"; +import type { TooltipRootProps, TooltipContentProps, TooltipTriggerProps } from "./types"; +import { sharedTooltipStyles } from "./styles"; + +export const TooltipRoot = ({ children, defaultOpen, open, onOpenChange }: TooltipRootProps) => { + return ( + + + {children} + + + ); +}; + +TooltipRoot.displayName = "Tooltip.Root"; + +export const TooltipContent = ({ children, label, className }: TooltipContentProps) => { + return ( + + + {label ? ( + + {label} + + ) : ( + children + )} + + + ); +}; + +TooltipContent.displayName = "Tooltip.Content"; + +export const TooltipTrigger = ({ children }: TooltipTriggerProps) => { + return ( + +
{children}
+
+ ); +}; + +TooltipTrigger.displayName = "Tooltip.Trigger"; + +export const Tooltip = { + Root: TooltipRoot, + Content: TooltipContent, + Trigger: TooltipTrigger, +}; diff --git a/packages/ui/src/components/Tooltip/styles.ts b/packages/ui/src/components/Tooltip/styles.ts new file mode 100644 index 0000000..e1518a6 --- /dev/null +++ b/packages/ui/src/components/Tooltip/styles.ts @@ -0,0 +1,10 @@ +import { cn } from "../../utils/cn"; + +export const sharedTooltipStyles = cn( + "py-2 px-3 rounded-md bg-gray-700 text-white max-w-[200px]", + "will-change-[transform,opacity]", + "data-[state=delayed-open]:data-[side=top]:animate-slide-up-and-fade", + "data-[state=delayed-open]:data-[side=right]:animate-slide-right-and-fade", + "data-[state=delayed-open]:data-[side=left]:animate-slide-left-and-fade", + "data-[state=delayed-open]:data-[side=bottom]:animate-slide-down-and-fade", +); diff --git a/packages/ui/src/components/Tooltip/types.ts b/packages/ui/src/components/Tooltip/types.ts new file mode 100644 index 0000000..2bb9e66 --- /dev/null +++ b/packages/ui/src/components/Tooltip/types.ts @@ -0,0 +1,22 @@ +export type TooltipRootProps = { + children: React.ReactNode; + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +export type TooltipContentProps = { + children?: React.ReactNode; + label?: string; + className?: string; +}; + +export type TooltipTriggerProps = { + children: React.ReactNode; +}; + +export type TooltipProps = { + Root: TooltipRootProps; + Content: TooltipContentProps; + Trigger: TooltipTriggerProps; +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 2e6246d..1dd0d11 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -23,6 +23,10 @@ export { Text } from "./components/Text"; export * from "./components/Text/types"; export type { TextProps } from "./components/Text/types"; +export { Tooltip } from "./components/Tooltip"; +export * from "./components/Tooltip/types"; +export type { TooltipProps } from "./components/Tooltip/types"; + export * from "./designSystemTokens"; export { shadcnPreset } from "./tailwind/shadcn-preset"; diff --git a/packages/ui/src/tailwind/shadcn-plugin.ts b/packages/ui/src/tailwind/shadcn-plugin.ts index 239376b..92529bf 100644 --- a/packages/ui/src/tailwind/shadcn-plugin.ts +++ b/packages/ui/src/tailwind/shadcn-plugin.ts @@ -147,6 +147,22 @@ export const shadcnPlugin = plugin( transform: "translateY(0px)", }, }, + "slide-down-and-fade": { + from: { opacity: "0", transform: "translateY(-5px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + "slide-left-and-fade": { + from: { opacity: "0", transform: "translateX(5px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, + "slide-up-and-fade": { + from: { opacity: "0", transform: "translateY(5px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + "slide-right-and-fade": { + from: { opacity: "0", transform: "translateX(-5px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, @@ -161,6 +177,10 @@ export const shadcnPlugin = plugin( "fade-down": "fade-down 0.5s", "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "slide-down-and-fade": "slide-down-and-fade 0.2s ease-out", + "slide-up-and-fade": "slide-up-and-fade 0.2s ease-out", + "slide-left-and-fade": "slide-left-and-fade 0.2s ease-out", + "slide-right-and-fade": "slide-right-and-fade 0.2s ease-out", }, }, }, diff --git a/packages/ui/vite.config.plugin.ts b/packages/ui/vite.config.plugin.ts index 6717908..9eaa53a 100644 --- a/packages/ui/vite.config.plugin.ts +++ b/packages/ui/vite.config.plugin.ts @@ -14,6 +14,7 @@ export default defineConfig({ "@biomejs/biome", "@radix-ui/react-checkbox", "@radix-ui/react-slot", + "@radix-ui/react-tooltip", "@tailwindcss/typography", "@types/fs-extra", "@types/node", diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 549870e..ccb9300 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ "@biomejs/biome", "@radix-ui/react-checkbox", "@radix-ui/react-slot", + "@radix-ui/react-tooltip", "@tailwindcss/typography", "@types/fs-extra", "@types/node",