diff --git a/packages/react/.storybook/main.ts b/packages/react/.storybook/main.ts index 7e8d77c724..123839c788 100644 --- a/packages/react/.storybook/main.ts +++ b/packages/react/.storybook/main.ts @@ -54,6 +54,7 @@ const config: StorybookConfig = { }, }, }, + getAbsolutePath("@storybook/addon-designs"), ], framework: { name: getAbsolutePath("@storybook/react-vite"), diff --git a/packages/react/.storybook/preview.tsx b/packages/react/.storybook/preview.tsx index aab4880407..422aebf86b 100644 --- a/packages/react/.storybook/preview.tsx +++ b/packages/react/.storybook/preview.tsx @@ -55,7 +55,9 @@ export const F0 = (Story: StoryFn, { parameters }: StoryContext) => { action("Link clicked")(event, ...args) props?.onClick?.(event, ...args) event.preventDefault() - if (props.href) setCurrentPath(props.href) + if (props.href) { + setCurrentPath(props.href) + } }} /> ), diff --git a/packages/react/docs/components/BareColor.tsx b/packages/react/docs/components/BareColor.tsx index 72a6236b61..a8f3794d16 100644 --- a/packages/react/docs/components/BareColor.tsx +++ b/packages/react/docs/components/BareColor.tsx @@ -1,7 +1,7 @@ import { useState } from "react" +import { F0Button } from "@/components/F0Button" import { CopyIcon } from "lucide-react" -import { Button } from "../../src/components/Actions/Button" type Props = { name: string @@ -30,7 +30,7 @@ export function BareColor({ name }: Props) {
-
-
), } @@ -128,78 +173,75 @@ export const IconVariants: Story = {
With icon
-
Only icon
-
Only emoji
-
@@ -211,9 +253,9 @@ export const Sizes: Story = { parameters: withSnapshot({}), render: (args) => (
-
), } @@ -244,7 +286,7 @@ export const AsyncAction: Story = { alert("Changes saved!") } - return - {appendOutside &&
{append}
} + {appendOutside &&
{appendOutside}
} ), })) @@ -42,11 +41,11 @@ vi.mock("@/icons/app", () => ({ ChevronDown: () =>
, })) -vi.mock("@/components/Utilities/Icon", () => ({ - Icon: ({ icon: IconComponent }) => , +vi.mock("@/components/F0Icon", () => ({ + F0Icon: ({ icon: IconComponent }) => , })) -describe("OneDropdownButton", () => { +describe("F0ButtonDropdown", () => { const openDropdown = async (user: ReturnType) => { user.click(screen.getByTestId("button-menu")) @@ -77,7 +76,7 @@ describe("OneDropdownButton", () => { }) it("renders with default selection (first item)", () => { - render() + render() expect(screen.getByTestId("button-main")).toBeDefined() expect(screen.getByTestId("dropdown")).toBeDefined() @@ -87,11 +86,7 @@ describe("OneDropdownButton", () => { it("renders with provided value", () => { render( - + ) expect(screen.getByTestId("button-main")).toBeDefined() @@ -100,11 +95,7 @@ describe("OneDropdownButton", () => { it("passes dropdown items excluding the selected item", () => { render( - + ) const dropdownElement = screen.getByTestId("dropdown") @@ -124,11 +115,7 @@ describe("OneDropdownButton", () => { it("calls onClick with correct values when button is clicked", async () => { const user = userEvent.setup() render( - + ) await user.click(screen.getByTestId("button-main")) @@ -140,7 +127,7 @@ describe("OneDropdownButton", () => { it("does not open dropdown when disabled", async () => { const user = userEvent.setup() render( - { it.skip("changes selected value when dropdown item is clicked", async () => { const user = userEvent.setup() render( - + ) await openDropdown(user) diff --git a/packages/react/src/components/F0ButtonDropdown/index.ts b/packages/react/src/components/F0ButtonDropdown/index.ts new file mode 100644 index 0000000000..788c6ced9f --- /dev/null +++ b/packages/react/src/components/F0ButtonDropdown/index.ts @@ -0,0 +1,2 @@ +export * from "./F0ButtonDropdown" +export * from "./types" diff --git a/packages/react/src/components/Actions/OneDropdownButton/theme.ts b/packages/react/src/components/F0ButtonDropdown/theme.ts similarity index 75% rename from packages/react/src/components/Actions/OneDropdownButton/theme.ts rename to packages/react/src/components/F0ButtonDropdown/theme.ts index b9e6ae10fb..12e7286e4d 100644 --- a/packages/react/src/components/Actions/OneDropdownButton/theme.ts +++ b/packages/react/src/components/F0ButtonDropdown/theme.ts @@ -1,7 +1,7 @@ -import { OneDropdownButtonVariant } from "./types.ts" +import { ButtonDropdownVariant } from "./types.ts" export const internalButtonVariantsStyles = ( - variant?: OneDropdownButtonVariant + variant?: ButtonDropdownVariant ) => { const variants = { default: { borderColor: "hsl(var(--accent-60))" }, diff --git a/packages/react/src/components/F0ButtonDropdown/types.ts b/packages/react/src/components/F0ButtonDropdown/types.ts new file mode 100644 index 0000000000..b1f36a48b3 --- /dev/null +++ b/packages/react/src/components/F0ButtonDropdown/types.ts @@ -0,0 +1,69 @@ +import { IconType } from "@/components/F0Icon" +import { actionSizes } from "@/ui/Action" + +export const buttonDropdownVariants = ["default", "outline", "neutral"] as const +export type ButtonDropdownVariant = (typeof buttonDropdownVariants)[number] +export const buttonDropdownSizes = actionSizes +export type ButtonDropdownSize = (typeof buttonDropdownSizes)[number] + +export type ButtonDropdownItem = { + /** + * The value of the item. + */ + value: T + /** + * The label of the item. + */ + label: string + /** + * The icon of the item. + */ + icon?: IconType + /** + * Whether the item is critical. + * @default false + */ + critical?: boolean +} + +export type F0ButtonDropdownProps = { + /** + * The size of the button. + * @default "md" + */ + size?: ButtonDropdownSize + /** + * The items to display in the dropdown. + */ + items: ButtonDropdownItem[] + /** + * The variant of the button. + * @default "default" + */ + variant?: ButtonDropdownVariant + /** + * The value of the button. + */ + value?: T + /** + * The disabled state of the button. + * @default false + */ + disabled?: boolean + /** + * The loading state of the button. + * @default false + */ + loading?: boolean + /** + * The tooltip of the button. + * @default undefined + */ + tooltip?: string + /** + * The callback function to be called when the button is clicked. + * @param value The value of the item that was clicked. + * @param item The item that was clicked. + */ + onClick: (value: T, item: ButtonDropdownItem) => void +} diff --git a/packages/react/src/components/F0ButtonToggle/F0ButtonToggle.tsx b/packages/react/src/components/F0ButtonToggle/F0ButtonToggle.tsx new file mode 100644 index 0000000000..2aa9e0f747 --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/F0ButtonToggle.tsx @@ -0,0 +1,104 @@ +import { F0Icon } from "@/components/F0Icon" +import { cn, focusRing } from "@/lib/utils" +import { actionVariants, buttonSizeVariants } from "@/ui/Action/variants" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { AnimatePresence, motion } from "motion/react" +import { forwardRef, useEffect, useMemo, useState } from "react" +import { F0ButtonToggleProps } from "./types" + +export const F0ButtonToggle = forwardRef< + HTMLButtonElement, + F0ButtonToggleProps +>( + ( + { + onSelectedChange = () => {}, + selected = false, + label, + disabled = false, + icon, + size = "md", + ...props + }, + ref + ) => { + const singleIcon = !Array.isArray(icon) + const [iconOff, iconOn] = singleIcon ? [icon, icon] : icon + + const sizeClass = { + sm: "h-6", + md: "h-8", + lg: "h-10", + } + + const animationProps = useMemo( + () => + singleIcon + ? undefined + : { + initial: { opacity: 0, scale: 0.8 }, + animate: { opacity: 1, scale: 1 }, + exit: { opacity: 0, scale: 0.6 }, + transition: { duration: 0.25, ease: "easeOut" }, + }, + [singleIcon] + ) + + const [localSelected, setLocalSelected] = useState(selected) + + const handleChange = (pressed: boolean) => { + setLocalSelected(pressed) + onSelectedChange?.(pressed) + } + + useEffect(() => { + if (localSelected === selected) { + return + } + setLocalSelected(selected) + // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this when the selected prop changes + }, [selected]) + + return ( + + + {localSelected ? ( + + + + ) : ( + + + + )} + + + ) + } +) + +F0ButtonToggle.displayName = "F0ButtonToggle" diff --git a/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.mdx b/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.mdx new file mode 100644 index 0000000000..be216ff451 --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.mdx @@ -0,0 +1,109 @@ +import { Canvas, Meta, Controls, Unstyled } from "@storybook/addon-docs/blocks" +import { DoDonts } from "@/lib/storybook-utils/do-donts" + +import * as F0ButtonToggleStories from "./F0ButtonToggle.stories" + + + +# F0ButtonToggle + +## Purpose of this document + +This documentation provides comprehensive guidance for designers and developers +on using the F0ButtonToggle component, including design specifications, +interaction patterns, and implementation details. + +## Introduction + +### Definition + +The F0ButtonToggle is a two-state button component that allows users to switch +between two modes or states. It functions similarly to a checkbox but with a +more prominent visual representation suitable for primary actions. + +### Purpose + +- Provide binary state control for user preferences or settings +- Enable quick switching between two related modes (e.g., microphone on/off) +- Offer clear visual feedback for the current state +- Support accessibility requirements for toggle interactions + +## Anatomy + + + + +The F0ButtonToggle consists of: + +1. **Container**: The button boundary that responds to user interaction +2. **Icon Area**: Visual indicator that changes based on state +3. **Label**: Accessible text description (hidden visually but available to + screen readers) +4. **State Indicator**: Visual feedback showing current selected/unselected + state + +### Interactive States + +- **Default**: Unselected state with primary styling +- **Selected**: Active state with emphasized styling +- **Disabled**: Non-interactive state with reduced opacity +- **Hover**: Enhanced visual feedback on mouse over +- **Focus**: Keyboard navigation indicator + +## Variants + +### Single Icon + +Use when the same icon represents both states, with visual styling indicating +the current state. + + + +### Dual Icon + +Use when different icons better represent the two states (e.g., microphone +on/off, visibility toggle). + + + +### Size Variants + + + +Available sizes: + +- **sm**: Compact toggle for dense interfaces +- **md**: Standard size for most use cases +- **lg**: Prominent toggle for primary actions + +## Guidelines + +### Content best practices + +- Labels should clearly describe the action or state +- Use consistent icon metaphors that users understand +- Provide immediate visual feedback when state changes +- Ensure the purpose is clear from context + +### Design best practices + + diff --git a/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.stories.tsx b/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.stories.tsx new file mode 100644 index 0000000000..b18f81c2fb --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/__stories__/F0ButtonToggle.stories.tsx @@ -0,0 +1,114 @@ +import { Microphone, MicrophoneNegative } from "@/icons/app" +import { withSnapshot } from "@/lib/storybook-utils/parameters" +import type { Meta, StoryObj } from "@storybook/react-vite" +import { buttonToggleSizes, F0ButtonToggle } from "../index" + +const meta = { + title: "ButtonToggle", + component: F0ButtonToggle, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A button that can be toggled between two states. Works like a checkbox", + }, + }, + design: { + type: "figma", + url: "https://www.figma.com/design/pZzg1KTe9lpKTSGPUZa8OJ/Components?node-id=13235-148548&p=f&t=u27xbp3PH7jll0ic-0", + }, + }, + tags: ["autodocs"], + args: { + onSelectedChange: (selected) => { + console.log("Button toggle clicked", selected) + }, + label: "Toggle me", + size: "md", + selected: false, + disabled: false, + }, + argTypes: { + selected: { + control: "boolean", + description: "Whether the button is in selected/active state.", + }, + size: { + control: "select", + options: buttonToggleSizes, + table: { + type: { + summary: buttonToggleSizes.join(" | "), + }, + }, + }, + label: { + control: "text", + description: + "The accessible label for the button. Required for accessibility.", + }, + icon: { + table: { + type: { + summary: "IconType | [IconType, IconType]", + }, + }, + }, + disabled: { + control: "boolean", + description: + "The button is inactive and does not respond to user interaction.", + }, + onSelectedChange: { + action: "selected", + description: "Callback fired when the button is selected.", + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + label: "Default Toggle", + icon: [MicrophoneNegative, Microphone], + }, +} + +export const SingleIcon: Story = { + args: { + label: "Single Icon Toggle", + icon: Microphone, + }, +} + +export const Snapshot: Story = { + parameters: withSnapshot({}), + args: { + label: "Toggle me", + icon: [MicrophoneNegative, Microphone], + }, + render: () => ( +
+ {buttonToggleSizes.map((size) => ( + <> + + + + ))} +
+ ), +} diff --git a/packages/react/src/components/F0ButtonToggle/__tests__/F0ButtonToggle.test.tsx b/packages/react/src/components/F0ButtonToggle/__tests__/F0ButtonToggle.test.tsx new file mode 100644 index 0000000000..3fcc20ac3a --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/__tests__/F0ButtonToggle.test.tsx @@ -0,0 +1,427 @@ +import { zeroRender } from "@/testing/test-utils" +import { fireEvent, screen } from "@testing-library/react" +import React from "react" +import { describe, expect, it, vi } from "vitest" +import { F0ButtonToggle } from "../F0ButtonToggle" + +// Mock icons for easier testing +const MockIconOff = React.forwardRef((props, ref) => ( + + Mock Icon Off + +)) +MockIconOff.displayName = "MockIconOff" + +const MockIconOn = React.forwardRef((props, ref) => ( + + Mock Icon On + +)) +MockIconOn.displayName = "MockIconOn" + +const MockSingleIcon = React.forwardRef((props, ref) => ( + + Mock Single Icon + +)) +MockSingleIcon.displayName = "MockSingleIcon" + +describe("F0ButtonToggle", () => { + describe("Basic Rendering", () => { + it("renders with single icon", () => { + zeroRender() + + const button = screen.getByRole("button", { name: "Test toggle" }) + expect(button).toBeInTheDocument() + expect(button).toHaveAttribute("aria-label", "Test toggle") + + // Check that the icon is rendered + expect(screen.getByTestId("mock-single-icon")).toBeInTheDocument() + }) + + it("renders with dual icons (array)", () => { + zeroRender( + + ) + + const button = screen.getByRole("button", { name: "Dual icon toggle" }) + expect(button).toBeInTheDocument() + + // Should show the "off" icon when not selected + expect(screen.getByTestId("mock-icon-off")).toBeInTheDocument() + expect(screen.queryByTestId("mock-icon-on")).not.toBeInTheDocument() + }) + + it("applies correct default classes and structure", () => { + zeroRender() + + const button = screen.getByRole("button") + expect(button).toHaveClass("aspect-square") + expect(button).toHaveClass("h-8") // default medium size + }) + }) + + describe("Toggle State", () => { + it("starts unselected by default", () => { + zeroRender() + + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "false") + }) + + it("respects initial selected state", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "true") + }) + + it("toggles state when clicked", () => { + zeroRender() + + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "false") + + fireEvent.click(button) + expect(button).toHaveAttribute("aria-pressed", "true") + + fireEvent.click(button) + expect(button).toHaveAttribute("aria-pressed", "false") + }) + }) + + describe("Callback Functions", () => { + it("calls onSelectedChange when toggled", () => { + const onSelectedChange = vi.fn() + + zeroRender( + + ) + + // Clear the initial call that happens on mount + onSelectedChange.mockClear() + + const button = screen.getByRole("button") + fireEvent.click(button) + + expect(onSelectedChange).toHaveBeenCalledWith(true) + }) + + it("calls onSelectedChange with correct values on multiple toggles", () => { + const onSelectedChange = vi.fn() + + zeroRender( + + ) + + // Clear the initial call that happens on mount + onSelectedChange.mockClear() + + const button = screen.getByRole("button") + + fireEvent.click(button) + expect(onSelectedChange).toHaveBeenLastCalledWith(true) + + fireEvent.click(button) + expect(onSelectedChange).toHaveBeenLastCalledWith(false) + + expect(onSelectedChange).toHaveBeenCalledTimes(2) + }) + + it("works without onSelectedChange callback", () => { + expect(() => { + zeroRender() + + const button = screen.getByRole("button") + fireEvent.click(button) + }).not.toThrow() + }) + }) + + describe("Disabled State", () => { + it("respects disabled prop", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toBeDisabled() + }) + + it("does not toggle when disabled", () => { + const onSelectedChange = vi.fn() + + zeroRender( + + ) + + // Clear the initial call that happens on mount + onSelectedChange.mockClear() + + const button = screen.getByRole("button") + fireEvent.click(button) + + expect(button).toHaveAttribute("aria-pressed", "false") + expect(onSelectedChange).not.toHaveBeenCalled() + }) + }) + + describe("Accessibility", () => { + it("has proper aria-label", () => { + zeroRender( + + ) + + const button = screen.getByRole("button", { name: "Toggle microphone" }) + expect(button).toHaveAttribute("aria-label", "Toggle microphone") + }) + + it("has proper aria-pressed attribute", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "true") + }) + + it("is focusable and keyboard accessible", () => { + zeroRender() + + const button = screen.getByRole("button") + button.focus() + expect(button).toHaveFocus() + }) + }) + + describe("Size Variants", () => { + it("applies small size classes", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveClass("h-6") + }) + + it("applies medium size classes (default)", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveClass("h-8") + }) + + it("applies large size classes", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveClass("h-10") + }) + + it("defaults to medium size when size prop is not provided", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + expect(button).toHaveClass("h-8") + }) + }) + + describe("Icon Handling", () => { + it("displays single icon consistently", () => { + const { rerender } = zeroRender( + + ) + + let button = screen.getByRole("button") + expect(button).toBeInTheDocument() + // Single icon should always be visible regardless of state + expect(screen.getByTestId("mock-single-icon")).toBeInTheDocument() + + // Toggle state and check icon remains consistent + rerender( + + ) + + button = screen.getByRole("button") + expect(button).toBeInTheDocument() + // Same icon should still be visible after state change + expect(screen.getByTestId("mock-single-icon")).toBeInTheDocument() + }) + + it("handles dual icons correctly based on state", () => { + const { rerender } = zeroRender( + + ) + + let button = screen.getByRole("button") + expect(button).toBeInTheDocument() + + // When not selected, should show the first icon (off state) + expect(screen.getByTestId("mock-icon-off")).toBeInTheDocument() + + // Toggle to selected state + rerender( + + ) + + button = screen.getByRole("button") + expect(button).toBeInTheDocument() + + // When selected, should show the second icon (on state) + expect(screen.getByTestId("mock-icon-on")).toBeInTheDocument() + }) + + it("switches icons when user clicks dual icon toggle", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + + // Initially should show off icon + expect(screen.getByTestId("mock-icon-off")).toBeInTheDocument() + expect(button).toHaveAttribute("aria-pressed", "false") + + // Click to toggle + fireEvent.click(button) + + // Should now show on icon and be in pressed state + expect(screen.getByTestId("mock-icon-on")).toBeInTheDocument() + expect(button).toHaveAttribute("aria-pressed", "true") + + // Click to toggle back + fireEvent.click(button) + + // Should show off icon again and not be pressed + expect(screen.getByTestId("mock-icon-off")).toBeInTheDocument() + expect(button).toHaveAttribute("aria-pressed", "false") + }) + }) + + describe("State Management", () => { + it("updates local state when selected prop changes", () => { + const { rerender } = zeroRender( + + ) + + let button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "false") + + rerender( + + ) + + button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-pressed", "true") + }) + + it("maintains internal state for uncontrolled usage", () => { + zeroRender( + + ) + + const button = screen.getByRole("button") + + expect(button).toHaveAttribute("aria-pressed", "false") + + fireEvent.click(button) + expect(button).toHaveAttribute("aria-pressed", "true") + + fireEvent.click(button) + expect(button).toHaveAttribute("aria-pressed", "false") + }) + }) + + describe("Edge Cases", () => { + it("handles undefined onSelectedChange gracefully", () => { + expect(() => { + zeroRender( + + ) + + const button = screen.getByRole("button") + fireEvent.click(button) + }).not.toThrow() + }) + + it("forwards additional props to the underlying element", () => { + zeroRender( + + ) + + const button = screen.getByTestId("custom-toggle") + expect(button).toBeInTheDocument() + }) + }) +}) diff --git a/packages/react/src/components/F0ButtonToggle/index.tsx b/packages/react/src/components/F0ButtonToggle/index.tsx new file mode 100644 index 0000000000..c388adf107 --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/index.tsx @@ -0,0 +1,2 @@ +export * from "./F0ButtonToggle" +export * from "./types" diff --git a/packages/react/src/components/F0ButtonToggle/types.ts b/packages/react/src/components/F0ButtonToggle/types.ts new file mode 100644 index 0000000000..46484b0c4e --- /dev/null +++ b/packages/react/src/components/F0ButtonToggle/types.ts @@ -0,0 +1,32 @@ +import { IconType } from "@/components/F0Icon" + +export const buttonToggleSizes = ["sm", "md", "lg"] as const +export type ButtonToggleSize = (typeof buttonToggleSizes)[number] + +export interface F0ButtonToggleProps { + /** + * Whether the button is in selected/active state. + */ + selected?: boolean + /** + * Callback fired when the button is selected. + */ + onSelectedChange?: (selected: boolean) => void + /** + * The accessible label for the button. + */ + label: string + /** + * Whether the button is disabled. + */ + disabled?: boolean + /** + * The icon to display in the button. Can be a single icon or an array of two icons the first for the non-selected state and the second for the selected state. + */ + icon: IconType | [IconType, IconType] + /** + * The size of the button. + * @default "md" + */ + size?: ButtonToggleSize +} diff --git a/packages/react/src/components/F0Card/CardInternal.tsx b/packages/react/src/components/F0Card/CardInternal.tsx index 86c9010990..47379f6f0b 100644 --- a/packages/react/src/components/F0Card/CardInternal.tsx +++ b/packages/react/src/components/F0Card/CardInternal.tsx @@ -1,4 +1,4 @@ -import { Link } from "@/components/Actions/Link" +import { F0Link } from "@/components/F0Link" import { Image } from "@/components/Utilities/Image" import { DropdownItem } from "@/experimental/Navigation/Dropdown" import { cn, focusRing } from "@/lib/utils" @@ -160,14 +160,13 @@ export const CardInternal = forwardRef( ref={ref} > {link && !disableOverlayLink && ( - + > +   + )} {image && ( diff --git a/packages/react/src/components/F0Card/components/CardActions.tsx b/packages/react/src/components/F0Card/components/CardActions.tsx index c42f9f17f0..d0506b9df2 100644 --- a/packages/react/src/components/F0Card/components/CardActions.tsx +++ b/packages/react/src/components/F0Card/components/CardActions.tsx @@ -1,6 +1,6 @@ -import { Button } from "@/components/Actions/Button" -import { Link, type LinkProps } from "@/components/Actions/Link" +import { F0Button } from "@/components/F0Button" import { IconType } from "@/components/F0Icon" +import { F0Link, type F0LinkProps } from "@/components/F0Link" import { cn } from "@/lib/utils" import { CardFooter } from "@/ui/Card" import { useMediaQuery } from "usehooks-ts" @@ -18,7 +18,7 @@ export interface CardSecondaryAction { } export interface CardSecondaryLink - extends Pick { + extends Pick { label: string } @@ -52,33 +52,32 @@ export function CardActions({
{Array.isArray(secondaryActions) ? ( secondaryActions.map((action, index) => ( -
)} {primaryAction && (
-
{isCompactMode && (
-
- + ) : ( - { - console.log("Button toggle clicked", selected) - }, - label: "Toggle me", - size: "md", - selected: false, - disabled: false, - }, - argTypes: { - selected: { - control: "boolean", - description: "Whether the button is in selected/active state.", - }, - size: { - control: "select", - options: ["sm", "md", "lg"], - description: - "Sets the button size. 'md' for desktop, 'sm' for compact/secondary actions.", - }, - label: { - control: "text", - description: - "The accessible label for the button. Required for accessibility.", - }, - icon: { - description: "Icon to display in the button. Required prop.", - }, - disabled: { - control: "boolean", - description: - "The button is inactive and does not respond to user interaction.", - }, - onSelectedChange: { - action: "selected", - description: "Callback fired when the button is selected.", - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Default: Story = { - args: { - label: "Default Toggle", - icon: Placeholder, - }, - render: (args) => { - const [selected, setSelected] = useState(false) - - return ( - - ) - }, -} diff --git a/packages/react/src/experimental/Actions/F0ButtonToggle/index.tsx b/packages/react/src/experimental/Actions/F0ButtonToggle/index.tsx deleted file mode 100644 index dc71387073..0000000000 --- a/packages/react/src/experimental/Actions/F0ButtonToggle/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { F0Icon, IconType } from "@/components/F0Icon" -import { cn, focusRing } from "@/lib/utils" -import { actionVariants, buttonSizeVariants } from "@/ui/Action/variants" -import * as TogglePrimitive from "@radix-ui/react-toggle" -import { forwardRef } from "react" - -interface F0ButtonToggleProps { - selected?: boolean - onSelectedChange?: (selected: boolean) => void - label: string - disabled?: boolean - icon: IconType - size?: "sm" | "md" | "lg" -} - -export const F0ButtonToggle = forwardRef< - HTMLButtonElement, - F0ButtonToggleProps ->( - ( - { - onSelectedChange = () => {}, - selected = false, - label, - disabled = false, - icon, - size = "md", - ...props - }, - ref - ) => { - return ( - - - - ) - } -) - -F0ButtonToggle.displayName = "F0ButtonToggle" diff --git a/packages/react/src/experimental/Actions/exports.ts b/packages/react/src/experimental/Actions/exports.ts index aab7d789ff..c2b779b174 100644 --- a/packages/react/src/experimental/Actions/exports.ts +++ b/packages/react/src/experimental/Actions/exports.ts @@ -1 +1 @@ -export * from "./F0ButtonToggle" +export * from "../../components/F0ButtonToggle" diff --git a/packages/react/src/experimental/AiChat/HILActionConfirmation.tsx b/packages/react/src/experimental/AiChat/HILActionConfirmation.tsx index 9cbf61ed05..981086ee71 100644 --- a/packages/react/src/experimental/AiChat/HILActionConfirmation.tsx +++ b/packages/react/src/experimental/AiChat/HILActionConfirmation.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import Check from "@/icons/app/Check" export type HILActionConfirmationProps = { @@ -20,7 +20,7 @@ export const HILActionConfirmation = ({
{text &&

{text}

}
-
-
diff --git a/packages/react/src/experimental/AiChat/components/MessagesContainer.tsx b/packages/react/src/experimental/AiChat/components/MessagesContainer.tsx index 7fbd68f6c0..ec2c5b465b 100644 --- a/packages/react/src/experimental/AiChat/components/MessagesContainer.tsx +++ b/packages/react/src/experimental/AiChat/components/MessagesContainer.tsx @@ -1,4 +1,4 @@ -import { ButtonInternal } from "@/components/Actions/Button/internal" +import { ButtonInternal } from "@/components/F0Button/internal" import { ArrowDown } from "@/icons/app" import { useI18n } from "@/lib/providers/i18n" import { cn } from "@/lib/utils" diff --git a/packages/react/src/experimental/AiChat/components/SuggestionsList.tsx b/packages/react/src/experimental/AiChat/components/SuggestionsList.tsx index 5e5be5b8e2..4d1caf4870 100644 --- a/packages/react/src/experimental/AiChat/components/SuggestionsList.tsx +++ b/packages/react/src/experimental/AiChat/components/SuggestionsList.tsx @@ -1,4 +1,4 @@ -import { ButtonInternal } from "@/components/Actions/Button/internal" +import { ButtonInternal } from "@/components/F0Button/internal" import { RenderSuggestionsListProps } from "@copilotkit/react-ui" export const SuggestionsList = ({ diff --git a/packages/react/src/experimental/AiChat/components/WelcomeScreen.tsx b/packages/react/src/experimental/AiChat/components/WelcomeScreen.tsx index c51aff7710..6778bc7d9d 100644 --- a/packages/react/src/experimental/AiChat/components/WelcomeScreen.tsx +++ b/packages/react/src/experimental/AiChat/components/WelcomeScreen.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, motion } from "motion/react" -import { ButtonInternal } from "@/components/Actions/Button/internal" +import { ButtonInternal } from "@/components/F0Button/internal" import { IconType } from "@/components/F0Icon/F0Icon" import { useCopilotChatInternal } from "@copilotkit/react-core" import { Message, randomId } from "@copilotkit/shared" diff --git a/packages/react/src/experimental/AiChat/markdownRenderers.tsx b/packages/react/src/experimental/AiChat/markdownRenderers.tsx index bac9f5dce3..00ab8843d3 100644 --- a/packages/react/src/experimental/AiChat/markdownRenderers.tsx +++ b/packages/react/src/experimental/AiChat/markdownRenderers.tsx @@ -1,5 +1,5 @@ -import { Button } from "@/components/Actions/Button" -import { Link } from "@/components/Actions/Link/OneLink" +import { F0Button } from "@/components/F0Button" +import { F0Link } from "@/components/F0Link/F0Link" import DownloadIcon from "@/icons/app/Download" import { cn } from "@/lib/utils" import { type AssistantMessageProps } from "@copilotkit/react-ui" @@ -49,9 +49,9 @@ export const markdownRenderers: NonNullable< children, ...props }: React.AnchorHTMLAttributes) => ( - + {children} - + ), strong: ({ children, ...props }: React.HTMLAttributes) => ( @@ -181,7 +181,7 @@ export const markdownRenderers: NonNullable< className={cn("max-w-full rounded-md", props.className)} />
-
{onClose && ( -
-
) diff --git a/packages/react/src/experimental/Information/Communities/Post/CommunityPost/index.tsx b/packages/react/src/experimental/Information/Communities/Post/CommunityPost/index.tsx index 569feca611..40f9a95a19 100644 --- a/packages/react/src/experimental/Information/Communities/Post/CommunityPost/index.tsx +++ b/packages/react/src/experimental/Information/Communities/Post/CommunityPost/index.tsx @@ -1,4 +1,4 @@ -import { Link } from "@/components/Actions/Link" +import { F0Link } from "@/components/F0Link" import { F0AvatarIcon } from "@/components/avatars/F0AvatarIcon" import { F0AvatarPerson } from "@/components/avatars/F0AvatarPerson" import { Reactions, ReactionsProps } from "@/experimental/Information/Reactions" @@ -101,13 +101,17 @@ export const BaseCommunityPost = ({ >
{author ? ( - + - + ) : ( )} @@ -118,7 +122,7 @@ export const BaseCommunityPost = ({
{author ? ( <> - - - + {authorFullName} - + ) : (
@@ -155,14 +159,15 @@ export const BaseCommunityPost = ({ > {inLabel} - {group.title} - + · diff --git a/packages/react/src/experimental/Information/Headers/BaseHeader/index.tsx b/packages/react/src/experimental/Information/Headers/BaseHeader/index.tsx index ab26dfa7ae..8e6a90a42e 100644 --- a/packages/react/src/experimental/Information/Headers/BaseHeader/index.tsx +++ b/packages/react/src/experimental/Information/Headers/BaseHeader/index.tsx @@ -1,8 +1,5 @@ -import { Button, ButtonProps } from "@/components/Actions/Button" -import { - OneDropdownButton, - OneDropdownButtonProps, -} from "@/components/Actions/OneDropdownButton" +import { F0Button } from "@/components/F0Button" +import { F0ButtonDropdown } from "@/components/F0ButtonDropdown" import { AvatarVariant, F0Avatar } from "@/components/avatars/F0Avatar" import { StatusVariant } from "@/components/tags/F0TagStatus" import { Description } from "@/experimental/Information/Headers/BaseHeader/Description" @@ -22,10 +19,12 @@ import { DropdownItem, MobileDropdown, } from "@/experimental/Navigation/Dropdown" -import { Tooltip } from "@/experimental/Overlays/Tooltip" import { cn } from "@/lib/utils" -import { Fragment, memo } from "react" +import { Fragment } from "react" +export type HeaderSecondaryAction = SecondaryAction & { + hideLabel?: boolean +} interface BaseHeaderProps { title: string avatar?: @@ -38,7 +37,7 @@ interface BaseHeaderProps { description?: string primaryAction?: PrimaryActionButton | PrimaryDropdownAction - secondaryActions?: SecondaryAction[] + secondaryActions?: HeaderSecondaryAction[] otherActions?: (DropdownItem & { isVisible?: boolean })[] status?: { label: string @@ -52,42 +51,6 @@ interface BaseHeaderProps { const isVisible = (action: { isVisible?: boolean }) => action.isVisible !== false -const ButtonWithTooltip = memo(function ButtonWithTooltip({ - tooltip, - ...buttonProps -}: ButtonProps & { tooltip?: string }) { - if (tooltip) { - const Wrapper = buttonProps.disabled ? "span" : Fragment - return ( - - -
{link && (
- + {link.label} - +
)}
{action && ( <>
-
- + ) return tooltipContent ? ( diff --git a/packages/react/src/experimental/Information/utils.tsx b/packages/react/src/experimental/Information/utils.tsx index c9a3bea763..7e259dfd70 100644 --- a/packages/react/src/experimental/Information/utils.tsx +++ b/packages/react/src/experimental/Information/utils.tsx @@ -1,5 +1,5 @@ -import { OneDropdownButtonItem } from "../../components/Actions/OneDropdownButton" -import { IconType } from "../../components/F0Icon" +import { ButtonDropdownItem } from "@/components/F0ButtonDropdown" +import { IconType } from "@/components/F0Icon" export interface PrimaryAction { disabled?: boolean @@ -13,9 +13,9 @@ export interface PrimaryActionButton extends PrimaryAction { } export interface PrimaryDropdownAction extends PrimaryAction { - items: OneDropdownButtonItem[] + items: ButtonDropdownItem[] value?: T - onClick: (value: T, item: OneDropdownButtonItem) => void + onClick: (value: T, item: ButtonDropdownItem) => void } export interface SecondaryAction extends PrimaryActionButton { diff --git a/packages/react/src/experimental/Lists/OnePersonListItem/index.tsx b/packages/react/src/experimental/Lists/OnePersonListItem/index.tsx index 061a397507..00790ead7e 100644 --- a/packages/react/src/experimental/Lists/OnePersonListItem/index.tsx +++ b/packages/react/src/experimental/Lists/OnePersonListItem/index.tsx @@ -1,6 +1,6 @@ -import { Button } from "@/components/Actions/Button" import { AvatarBadge } from "@/components/avatars/F0Avatar/types" import { F0AvatarPerson } from "@/components/avatars/F0AvatarPerson" +import { F0Button } from "@/components/F0Button" import { F0Icon, IconType } from "@/components/F0Icon" import { F0TagDot, TagDotProps } from "@/components/tags/F0TagDot" import { F0TagRaw, TagRawProps } from "@/components/tags/F0TagRaw" @@ -101,7 +101,7 @@ const BaseOnePersonListItem = React.forwardRef< {"actions" in props && (
{props.actions?.primary && ( -
{canScrollPrev && ( - + icon={ChevronLeft} + label="Previous" + hideLabel + > )} {canScrollNext && ( - + icon={ChevronRight} + label="Next" + hideLabel + > )}
) diff --git a/packages/react/src/experimental/Navigation/Carousel/index.tsx b/packages/react/src/experimental/Navigation/Carousel/index.tsx index 7e3f52ac7e..9147bcf9d0 100644 --- a/packages/react/src/experimental/Navigation/Carousel/index.tsx +++ b/packages/react/src/experimental/Navigation/Carousel/index.tsx @@ -132,8 +132,8 @@ export const Carousel = ({ {showArrows && ( <> - - + + )}
diff --git a/packages/react/src/experimental/Navigation/DaytimePage/index.tsx b/packages/react/src/experimental/Navigation/DaytimePage/index.tsx index 5986f75220..a6e0f652c0 100644 --- a/packages/react/src/experimental/Navigation/DaytimePage/index.tsx +++ b/packages/react/src/experimental/Navigation/DaytimePage/index.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { F0AvatarPerson } from "@/components/avatars/F0AvatarPerson" import { F0AvatarPulse } from "@/components/avatars/F0AvatarPulse" import { OneSwitch } from "@/experimental/AiChat/OneSwitch" @@ -59,13 +59,12 @@ export function DaytimePage({
{(isSmallScreen || sidebarState === "hidden") && ( - + icon={ChevronDown} + > )}
- +
) : ( icon && ( diff --git a/packages/react/src/experimental/Navigation/Header/Breadcrumbs/index.stories.tsx b/packages/react/src/experimental/Navigation/Header/Breadcrumbs/index.stories.tsx index 6c493756cf..a5873b5bc7 100644 --- a/packages/react/src/experimental/Navigation/Header/Breadcrumbs/index.stories.tsx +++ b/packages/react/src/experimental/Navigation/Header/Breadcrumbs/index.stories.tsx @@ -1,3 +1,4 @@ +import { F0Button } from "@/components/F0Button" import { IconType } from "@/components/F0Icon" import { FiltersDefinition } from "@/components/OneFilterPicker" import { SelectItemProps } from "@/experimental/Forms/Fields/Select/types" @@ -10,7 +11,6 @@ import { } from "@/mocks" import type { Meta, StoryObj } from "@storybook/react-vite" import { useState } from "react" -import { Button } from "../../../../ui/button" import { Breadcrumbs, BreadcrumbsProps } from "./index" const meta: Meta = { @@ -324,7 +324,7 @@ export const Interactive: Story = { return (
- - - + label="Remove Breadcrumb" + > +
(null) return ( - - + ) } diff --git a/packages/react/src/experimental/OneActionBar/index.stories.tsx b/packages/react/src/experimental/OneActionBar/index.stories.tsx index 64139ed4c6..be9a262b78 100644 --- a/packages/react/src/experimental/OneActionBar/index.stories.tsx +++ b/packages/react/src/experimental/OneActionBar/index.stories.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { F0Checkbox } from "@/components/F0Checkbox" import { Delete, @@ -177,7 +177,7 @@ export const NoSelectedItems: Story = { return (
-
{currentGrouping?.field && ( -
-
- {description &&

{description}

} + {description && ( +

{description.toString()}

+ )}
@@ -49,3 +53,21 @@ export function Tooltip({ ) } + +const privateProps = ["delay"] as const + +export type TooltipProps = Omit< + TooltipInternalProps, + (typeof privateProps)[number] +> + +const Tooltip = (props: TooltipProps) => { + const publicProps = privateProps.reduce((acc, key) => { + const { [key]: _, ...rest } = acc + return rest + }, props as TooltipInternalProps) + + return +} + +export { Tooltip } diff --git a/packages/react/src/experimental/Overlays/exports.tsx b/packages/react/src/experimental/Overlays/exports.tsx index aa8f527c7b..b06322348d 100644 --- a/packages/react/src/experimental/Overlays/exports.tsx +++ b/packages/react/src/experimental/Overlays/exports.tsx @@ -1,5 +1,6 @@ import { Component } from "../../lib/component/component" import { Dialog as DialogComponent } from "./Dialog" +export { Tooltip, type TooltipProps } from "./Tooltip" export const Dialog = Component( { diff --git a/packages/react/src/experimental/RichText/CoreEditor/Extensions/AIBlock/index.tsx b/packages/react/src/experimental/RichText/CoreEditor/Extensions/AIBlock/index.tsx index 1811e0313b..e3fc59d43e 100644 --- a/packages/react/src/experimental/RichText/CoreEditor/Extensions/AIBlock/index.tsx +++ b/packages/react/src/experimental/RichText/CoreEditor/Extensions/AIBlock/index.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { IconType } from "@/components/F0Icon" import { F0AiBanner } from "@/experimental/Banners/F0AiBanner" import { LiveCompanionLabels } from "@/experimental/RichText/CoreEditor/Extensions/LiveCompanion" @@ -236,7 +236,7 @@ const AIButtonsSection = ({ )}
{config.buttons?.map((button, index) => ( - + label="Close link popup" + hideLabel + icon={Cross} + >
{ /> )} - + label={labels.linkPaste} + >
- + label={i18n.actions.save} + >
)} diff --git a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarButton/index.tsx b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarButton/index.tsx index 19b8e7879a..f1120e1f9d 100644 --- a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarButton/index.tsx +++ b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarButton/index.tsx @@ -2,7 +2,7 @@ import { F0Icon, IconType } from "@/components/F0Icon" import { Shortcut } from "@/experimental/Information/Shortcut" import { Tooltip } from "@/experimental/Overlays/Tooltip" import { cn } from "@/lib/utils" -import { Button } from "@/ui/button" +import { Action } from "@/ui/Action" import { ComponentProps, forwardRef } from "react" interface ToolbarButtonProps { @@ -34,7 +34,7 @@ export const ToolbarButton = forwardRef( ref ) => { const CustomButton = ( - + ) return tooltip ? ( diff --git a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarDropdown/index.tsx b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarDropdown/index.tsx index 1cd2dc41f5..2f251e3c08 100644 --- a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarDropdown/index.tsx +++ b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/ToolbarDropdown/index.tsx @@ -1,5 +1,5 @@ +import { F0ButtonToggle } from "@/components/F0ButtonToggle" import { IconType } from "@/components/F0Icon" -import { F0ButtonToggle } from "@/experimental/Actions/F0ButtonToggle" import { cn } from "@/lib/utils" import * as Popover from "@radix-ui/react-popover" import { AnimatePresence, motion } from "motion/react" diff --git a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/index.tsx b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/index.tsx index 3823c285d2..7e5cc36bbb 100644 --- a/packages/react/src/experimental/RichText/CoreEditor/Toolbar/index.tsx +++ b/packages/react/src/experimental/RichText/CoreEditor/Toolbar/index.tsx @@ -1,5 +1,5 @@ -import { Button } from "@/components/Actions/Button" -import { F0ButtonToggle } from "@/experimental/Actions/F0ButtonToggle" +import { F0Button } from "@/components/F0Button" +import { F0ButtonToggle } from "@/components/F0ButtonToggle" import { Picker } from "@/experimental/Information/Reactions/Picker" import { AlignTextCenter, @@ -306,7 +306,7 @@ export const Toolbar = ({ return (
{onClose && ( -
{actions?.map((action, index) => ( - - +
diff --git a/packages/react/src/experimental/RichText/RichTextEditor/Enhance/AcceptChanges/index.tsx b/packages/react/src/experimental/RichText/RichTextEditor/Enhance/AcceptChanges/index.tsx index 8396b68ce5..ee85e14209 100644 --- a/packages/react/src/experimental/RichText/RichTextEditor/Enhance/AcceptChanges/index.tsx +++ b/packages/react/src/experimental/RichText/RichTextEditor/Enhance/AcceptChanges/index.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { ToolbarDivider } from "@/experimental/RichText/CoreEditor" import { enhanceLabelsType, @@ -29,7 +29,7 @@ const AcceptChanges = ({ return (
-
) diff --git a/packages/react/src/experimental/RichText/RichTextEditor/Enhance/index.tsx b/packages/react/src/experimental/RichText/RichTextEditor/Enhance/index.tsx index 03503e487d..c9497203ea 100644 --- a/packages/react/src/experimental/RichText/RichTextEditor/Enhance/index.tsx +++ b/packages/react/src/experimental/RichText/RichTextEditor/Enhance/index.tsx @@ -1,7 +1,7 @@ import { F0Icon } from "@/components/F0Icon" import { Ai } from "@/icons/app" import { cn } from "@/lib/utils" -import { Button } from "@/ui/button" +import { Action } from "@/ui/Action" import * as Popover from "@radix-ui/react-popover" import { Editor } from "@tiptap/react" import { AnimatePresence, motion } from "motion/react" @@ -59,7 +59,7 @@ const EnhanceActivator = ({ }} > - +
-
diff --git a/packages/react/src/experimental/RichText/RichTextEditor/Footer/ActionsMenu/index.tsx b/packages/react/src/experimental/RichText/RichTextEditor/Footer/ActionsMenu/index.tsx index 9f5d896ef1..155f5f0284 100644 --- a/packages/react/src/experimental/RichText/RichTextEditor/Footer/ActionsMenu/index.tsx +++ b/packages/react/src/experimental/RichText/RichTextEditor/Footer/ActionsMenu/index.tsx @@ -1,8 +1,8 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { - OneDropdownButton, - OneDropdownButtonItem, -} from "@/components/Actions/OneDropdownButton" + ButtonDropdownItem, + F0ButtonDropdown, +} from "@/components/F0ButtonDropdown" import { Switch } from "@/experimental/Forms/Fields/Switch" import { ToolbarDivider } from "@/experimental/RichText/CoreEditor" import { @@ -33,7 +33,7 @@ const normalizeSecondaryActions = ( const createActionItems = ( primaryAction?: primaryActionType, secondaryActions?: secondaryActionType[] -): OneDropdownButtonItem[] => { +): ButtonDropdownItem[] => { const primaryActionItems = primaryAction ? [ { @@ -142,7 +142,7 @@ const SecondaryActionsButtons = ({ {/* Render button actions - these might be hidden in little mode */} {!shouldHideButtonActions && buttonActions.map((action, index) => ( - - ) + const mainElement = isAnchor(props) ? ( + } + href={href} + target={target} + rel={target === "_blank" ? "noopener noreferrer" : undefined} + aria-disabled={disabled} + role="link" + > + {innerContent} + + ) : ( + + ) - if (prependOutside || appendOutside) { - return ( -
- {prependOutside && prepend} - {mainElement} - {appendOutside && append} -
- ) - } + const element = tooltip ? ( + + {mainElement} + + ) : ( + mainElement + ) - return mainElement + if (prependOutside || appendOutside) { + return ( +
+ {prependOutside} + {element} + {appendOutside} +
+ ) } -) + + return element +}) Action.displayName = "Action" diff --git a/packages/react/src/ui/Action/__stories__/Action.stories.tsx b/packages/react/src/ui/Action/__stories__/Action.stories.tsx index 417f995de1..f2b7f3b068 100644 --- a/packages/react/src/ui/Action/__stories__/Action.stories.tsx +++ b/packages/react/src/ui/Action/__stories__/Action.stories.tsx @@ -2,6 +2,7 @@ import { F0Icon } from "@/components/F0Icon" import { Placeholder } from "@/icons/app" import type { Meta, StoryObj } from "@storybook/react-vite" import { Action } from "../Action" +import { actionSizes } from "../types" const meta: Meta = { title: "Components/Action", @@ -27,7 +28,7 @@ const meta: Meta = { control: { type: "select", }, - options: ["sm", "md", "lg"], + options: actionSizes, }, pressed: { control: { @@ -57,6 +58,7 @@ type Story = StoryObj export const Basic: Story = { args: { children: "Basic Action", + "aria-label": "Basic Action", }, } @@ -66,6 +68,7 @@ export const AsLink: Story = { href: "https://example.com", target: "_blank", append: , + "aria-label": "Link Action", }, } @@ -75,6 +78,7 @@ export const AsLinkWithButtonVariant: Story = { href: "https://example.com", target: "_blank", variant: "default", + "aria-label": "Link with Button Style", }, } @@ -82,6 +86,7 @@ export const AsButtonWithLinkVariant: Story = { args: { children: "Button with Link Style", variant: "link", + "aria-label": "Button with Link Style", }, } @@ -89,6 +94,7 @@ export const Disabled: Story = { args: { children: "Disabled Action", disabled: true, + "aria-label": "Disabled Action", }, } @@ -96,6 +102,7 @@ export const WithPrepend: Story = { args: { children: "Action with Prepend", prepend: , + "aria-label": "Action with Prepend", }, } @@ -104,6 +111,7 @@ export const WithAppendOutside: Story = { children: "Action with Append", append: , appendOutside: true, + "aria-label": "Action with Append", }, } @@ -113,6 +121,7 @@ export const LinkDisabled: Story = { href: "https://example.com", target: "_blank", disabled: true, + "aria-label": "Link Disabled", }, } @@ -153,9 +162,27 @@ export const AllVariants: Story = { export const AllSizes: Story = { render: () => (
- Small - Medium - Large + + Small + + + Medium + + + Large + +
+ ), +} + +export const AllCompact: Story = { + render: () => ( +
+ {actionSizes.map((size) => ( + + + + ))}
), } diff --git a/packages/react/src/ui/Action/index.tsx b/packages/react/src/ui/Action/index.tsx index f3fe360b11..a7278286ec 100644 --- a/packages/react/src/ui/Action/index.tsx +++ b/packages/react/src/ui/Action/index.tsx @@ -1 +1,2 @@ export * from "./Action" +export * from "./types" diff --git a/packages/react/src/ui/Action/internal-types.ts b/packages/react/src/ui/Action/internal-types.ts new file mode 100644 index 0000000000..437ec5b7b3 --- /dev/null +++ b/packages/react/src/ui/Action/internal-types.ts @@ -0,0 +1,4 @@ +import { VariantProps } from "cva" +import { actionVariants } from "./variants" + +export type ActionVariantProps = VariantProps diff --git a/packages/react/src/ui/Action/types.ts b/packages/react/src/ui/Action/types.ts new file mode 100644 index 0000000000..bfa9ce8549 --- /dev/null +++ b/packages/react/src/ui/Action/types.ts @@ -0,0 +1,135 @@ +import { HTMLAttributeAnchorTarget, ReactNode } from "react" + +export const actionButtonVariants = [ + "default", + "outline", + "critical", + "neutral", + "ghost", + "promote", + "outlinePromote", +] as const +export type ActionButtonVariant = (typeof actionButtonVariants)[number] + +export const actionLinkVariants = ["link", "unstyled", "mention"] as const +export type ActionLinkVariant = (typeof actionLinkVariants)[number] + +export const actionVariants = [ + ...actionButtonVariants, + ...actionLinkVariants, +] as const +export type ActionVariant = (typeof actionVariants)[number] + +export const actionSizes = ["sm", "md", "lg"] as const +export type ActionSize = (typeof actionSizes)[number] + +export interface ActionCommonProps { + /** + * Tooltip + */ + tooltip?: string | false + /** + * The variant of the action. + */ + variant?: ActionVariant + /** + * The children of the action. + */ + children: ReactNode + + /** + * The prepend of the action. + */ + prepend?: ReactNode + /** + * The append of the action. + */ + append?: ReactNode + /** + * The prepend outside (next to the button) of the action. + */ + prependOutside?: ReactNode + /** + * The append outside of the action. + */ + appendOutside?: ReactNode + + /** + * The disabled state of the action. + */ + disabled?: boolean + /** + * The loading state of the action. + */ + loading?: boolean + /** + * The pressed state of the action. + */ + pressed?: boolean + + /** + * The class name of the action. + */ + className?: string + /** + * The size of the action. + */ + size?: ActionSize + /** + * The render mode. + * @default "default" + */ + mode?: "default" | "only" + + /** + * The title of the action. + */ + title?: string + + /** + * make the left and right padding of the action smaller. + */ + compact?: boolean + + /** + * The aria label of the action. + */ + "aria-label"?: string + + /** + * The tab index of the action. + */ + tabIndex?: number +} + +export const buttonTypes = ["button", "submit", "reset"] as const +export type ButtonType = (typeof buttonTypes)[number] + +export const navTargets = ["_blank", "_self", "_parent", "_top"] as const + +export type NavTarget = HTMLAttributeAnchorTarget + +export type ActionBaseProps = ActionCommonProps & DataAttributes + +export type ActionLinkProps = ActionBaseProps & { + href: string + target?: NavTarget + rel?: string + onFocus?: (event: React.FocusEvent) => void + onBlur?: (event: React.FocusEvent) => void + onClick?: (event: React.MouseEvent) => void + className?: string +} + +export type ActionButtonProps = ActionBaseProps & { + type?: ButtonType + href?: never + target?: never + onFocus?: (event: React.FocusEvent) => void + onBlur?: (event: React.FocusEvent) => void + onClick?: (event: React.MouseEvent) => void +} + +export type ActionProps = + | ActionLinkProps // as link + | ActionButtonProps // as button diff --git a/packages/react/src/ui/Action/utils.ts b/packages/react/src/ui/Action/utils.ts new file mode 100644 index 0000000000..6080231abf --- /dev/null +++ b/packages/react/src/ui/Action/utils.ts @@ -0,0 +1,7 @@ +import { ActionVariantProps } from "./internal-types" +import { ActionLinkVariant, actionLinkVariants } from "./types" + +export const isLinkStyled = ( + variant: ActionVariantProps["variant"] +): variant is ActionLinkVariant => + actionLinkVariants.includes(variant as unknown as ActionLinkVariant) diff --git a/packages/react/src/ui/Action/variants.ts b/packages/react/src/ui/Action/variants.ts index 2b3557b32b..6ecf59782e 100644 --- a/packages/react/src/ui/Action/variants.ts +++ b/packages/react/src/ui/Action/variants.ts @@ -57,6 +57,7 @@ export const actionVariants = cva({ baseLink, "text-f1-foreground underline decoration-f1-border-hover decoration-1 underline-offset-[5px] visited:text-f1-foreground hover:text-f1-foreground hover:decoration-f1-border-bold active:text-f1-foreground" ), + unstyled: cn(baseLink, "text-inherit no-underline"), mention: cn( baseLink, "bg-f1-background-accent !px-1.5 font-medium text-f1-foreground-accent" @@ -116,6 +117,7 @@ export const iconVariants = cva({ link: "", mention: "", selected: "", + unstyled: "", }, mode: { default: "", @@ -169,6 +171,11 @@ export const iconVariants = cva({ mode: "default", class: "[&>svg]:text-f1-icon-accent", }, + { + variant: "unstyled", + mode: "default", + class: "[&>svg]:text-f1-icon-accent", + }, { variant: "default", mode: "only", @@ -210,6 +217,11 @@ export const iconVariants = cva({ mode: "only", class: "[&>svg]:text-f1-icon", }, + { + variant: "unstyled", + mode: "only", + class: "[&>svg]:text-f1-icon", + }, { variant: "mention", mode: "default", @@ -221,3 +233,24 @@ export const iconVariants = cva({ mode: "default", }, }) + +export const loadingVariants = cva({ + base: "rounded-full border-solid border-t-transparent will-change-transform", + variants: { + size: { + sm: "h-3 w-3 border-[1px]", + md: "h-4 w-4 border-2", + lg: "h-5 w-5 border-2", + }, + variant: { + default: "border-f1-foreground-inverse border-t-transparent", + outline: "border-f1-foreground border-t-transparent", + neutral: "border-f1-foreground border-t-transparent", + critical: "border-f1-icon-critical border-t-transparent", + ghost: "border-f1-foreground border-t-transparent", + promote: "border-f1-icon-promote border-t-transparent", + outlinePromote: "border-f1-icon-promote border-t-transparent", + unstyled: "", + }, + }, +}) diff --git a/packages/react/src/components/Actions/Button/copy.tsx b/packages/react/src/ui/ButtonCopy/ButtonCopy.tsx similarity index 74% rename from packages/react/src/components/Actions/Button/copy.tsx rename to packages/react/src/ui/ButtonCopy/ButtonCopy.tsx index fe6318abd8..368f3aa1f0 100644 --- a/packages/react/src/components/Actions/Button/copy.tsx +++ b/packages/react/src/ui/ButtonCopy/ButtonCopy.tsx @@ -1,25 +1,27 @@ +import { F0Icon } from "@/components/F0Icon" import { Check, LayersFront } from "@/icons/app" import { useI18n } from "@/lib/providers/i18n" -import { Button as ShadcnButton } from "@/ui/button" +import { Action, ActionButtonProps, ActionButtonVariant } from "@/ui/Action" import { AnimatePresence, motion } from "motion/react" -import { - ComponentProps, - forwardRef, - MouseEventHandler, - useEffect, - useState, -} from "react" -import { F0Icon } from "../../F0Icon" -import { iconOnlyVariants } from "./internal" +import { forwardRef, MouseEventHandler, useEffect, useState } from "react" +import { iconOnlyVariants } from "../../components/F0Button/internal" -export type CopyButtonProps = Omit< - ComponentProps, - "onClick" | "children" | "title" | "label" | "hideLabel" | "icon" | "round" +export type ButtonCopyProps = Omit< + ActionButtonProps, + | "onClick" + | "children" + | "title" + | "label" + | "hideLabel" + | "icon" + | "target" + | "aria-label" > & { valueToCopy: string copiedTooltipLabel?: string copyTooltipLabel?: string - onCopy?: MouseEventHandler + onCopy?: MouseEventHandler + variant?: ActionButtonVariant } const copyIconMotionVariants = { @@ -29,7 +31,7 @@ const copyIconMotionVariants = { } const copyIconTransition = { duration: 0.15, ease: "easeOut" } -export const CopyButton = forwardRef( +export const ButtonCopy = forwardRef( ( { valueToCopy, @@ -63,7 +65,7 @@ export const CopyButton = forwardRef( } }, [isCopying]) - const handleCopyClick: MouseEventHandler = (event) => { + const handleCopyClick: MouseEventHandler = (event) => { event.stopPropagation() window.navigator.clipboard.writeText(valueToCopy) setIsCopying(true) @@ -71,16 +73,16 @@ export const CopyButton = forwardRef( } return ( - ( alignItems: "center", justifyContent: "center", verticalAlign: "middle", - width: "1em", - height: "1em", }} > - + ) } ) -CopyButton.displayName = "CopyButton" +ButtonCopy.displayName = "ButtonCopy" diff --git a/packages/react/src/ui/ButtonCopy/__stories__/ButtonCopy.stories.tsx b/packages/react/src/ui/ButtonCopy/__stories__/ButtonCopy.stories.tsx new file mode 100644 index 0000000000..3fe2582d10 --- /dev/null +++ b/packages/react/src/ui/ButtonCopy/__stories__/ButtonCopy.stories.tsx @@ -0,0 +1,297 @@ +import { withSnapshot } from "@/lib/storybook-utils/parameters" +import type { Meta, StoryObj } from "@storybook/react-vite" +import React, { useEffect } from "react" +import { expect, userEvent, within } from "storybook/test" +import { ButtonCopy } from "../ButtonCopy" +import { buttonCopySizes } from "../types" + +const meta = { + title: "Components/ButtonCopy", + component: ButtonCopy, + parameters: { + layout: "centered", + }, + tags: ["autodocs", "internal"], + args: { + valueToCopy: "Hello World!", + variant: "neutral", + size: "sm", + }, + argTypes: { + valueToCopy: { + control: "text", + description: + "The text value that will be copied to clipboard when the button is clicked.", + }, + variant: { + control: "select", + options: [ + "default", + "critical", + "neutral", + "ghost", + "outline", + "promote", + "outlinePromote", + ], + description: "Visual style variant of the copy button.", + }, + size: { + control: "select", + options: buttonCopySizes, + description: + "Sets the button size. Copy buttons are typically used in 'sm' size.", + }, + copyTooltipLabel: { + control: "text", + description: + "Custom tooltip label shown before copying. Defaults to translation for 'Copy'.", + }, + copiedTooltipLabel: { + control: "text", + description: + "Custom tooltip label shown after copying. Defaults to 'Copied'.", + }, + disabled: { + control: "boolean", + description: + "The button is inactive and does not respond to user interaction.", + }, + onCopy: { + action: "copied", + description: "Callback fired when the copy action is performed.", + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// Basic Stories +export const Default: Story = { + args: { + valueToCopy: "This text will be copied!", + }, + play: async ({ canvasElement, step, args }) => { + const canvas = within(canvasElement) + + let clipboard = "" + navigator.clipboard.writeText = (text: string) => { + clipboard = text + return Promise.resolve() + } + + await step("Verify initial state", async () => { + const button = canvas.getByRole("button") + expect(button).toBeInTheDocument() + expect(button.getAttribute("aria-label")).toBe("Copy") + expect(button.getAttribute("title")).toBe("Copy") + }) + + await step("Click copy button", async () => { + const button = canvas.getByRole("button") + await userEvent.click(button) + }) + + await step("Verify copied state", async () => { + const button = canvas.getByRole("button") + expect(button.getAttribute("aria-label")).toBe("Copied") + expect(button.getAttribute("title")).toBe("Copied") + expect(clipboard).toBe(args.valueToCopy) + }) + }, +} + +export const Variants: Story = { + parameters: withSnapshot({}), + render: (args) => ( +
+ + + + + + +
+ ), +} + +export const Sizes: Story = { + parameters: withSnapshot({}), + render: (args) => ( +
+ + + +
+ ), +} + +export const CustomLabels: Story = { + args: { + valueToCopy: "Custom labels example", + copyTooltipLabel: "Click to copy text", + copiedTooltipLabel: "Text copied successfully!", + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("Verify custom copy label", async () => { + const button = canvas.getByRole("button") + expect(button.getAttribute("aria-label")).toBe("Click to copy text") + }) + + await step("Click and verify custom copied label", async () => { + const button = canvas.getByRole("button") + await userEvent.click(button) + expect(button.getAttribute("aria-label")).toBe( + "Text copied successfully!" + ) + }) + }, +} + +export const DifferentValues: Story = { + parameters: withSnapshot({}), + render: () => ( +
+
+ Email: + +
+
+ URL: + +
+
+ Token: + +
+
+ Code: + +
+
+ ), +} + +export const States: Story = { + parameters: withSnapshot({}), + render: (args) => ( +
+ + +
+ ), +} + +export const InteractiveCopyTest: Story = { + render: (args) => { + const [copyCount, setCopyCount] = React.useState(0) + + const handleCopy = () => { + setCopyCount((prev) => prev + 1) + } + + return ( +
+ +

Copied {copyCount} times

+
+ ) + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("Test multiple copy interactions", async () => { + const button = canvas.getByRole("button") + const counter = canvas.getByText(/Copied \d+ times/) + + // Initial state + expect(counter).toHaveTextContent("Copied 0 times") + + // First copy + await userEvent.click(button) + expect(counter).toHaveTextContent("Copied 1 times") + + // Wait for animation to complete before next click + await new Promise((resolve) => setTimeout(resolve, 1100)) + + // Second copy + await userEvent.click(button) + expect(counter).toHaveTextContent("Copied 2 times") + }) + }, +} + +export const AnimationStates: Story = { + render: (args) => { + const [showCopied, setShowCopied] = React.useState(false) + + const triggerCopy = () => { + setShowCopied(true) + setTimeout(() => setShowCopied(false), 1000) + } + + useEffect(() => { + console.log("showCopied", showCopied) + }, [showCopied]) + + return ( +
+
+ + +
+

+ Click either button to see the copy → check animation +

+
+ ) + }, +} + +export const InContext: Story = { + parameters: withSnapshot({}), + render: (args) => ( +
+

Share this link

+
+ + https://factorial.com/shared/abc123 + + +
+
+ ), +} diff --git a/packages/react/src/ui/ButtonCopy/index.ts b/packages/react/src/ui/ButtonCopy/index.ts new file mode 100644 index 0000000000..d9c4c65ad0 --- /dev/null +++ b/packages/react/src/ui/ButtonCopy/index.ts @@ -0,0 +1 @@ +export * from "./ButtonCopy" diff --git a/packages/react/src/ui/ButtonCopy/types.ts b/packages/react/src/ui/ButtonCopy/types.ts new file mode 100644 index 0000000000..29b1093eb4 --- /dev/null +++ b/packages/react/src/ui/ButtonCopy/types.ts @@ -0,0 +1,4 @@ +import { actionSizes } from "@/ui/Action" + +export const buttonCopySizes = actionSizes +export type ButtonCopySize = (typeof buttonCopySizes)[number] diff --git a/packages/react/src/ui/Card/Card.tsx b/packages/react/src/ui/Card/Card.tsx index 60e05ce688..839f2e7950 100644 --- a/packages/react/src/ui/Card/Card.tsx +++ b/packages/react/src/ui/Card/Card.tsx @@ -1,9 +1,9 @@ import * as React from "react" -import { cn } from "../../lib/utils" +import { cn } from "@/lib/utils" -import ChevronRight from "../../icons/app/ChevronRight" -import InfoCircleLine from "../../icons/app/InfoCircleLine" +import ChevronRight from "@/icons/app/ChevronRight" +import InfoCircleLine from "@/icons/app/InfoCircleLine" import { F0Icon, IconType } from "@/components/F0Icon" import { Link } from "@/lib/linkHandler" diff --git a/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.test.tsx b/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.test.tsx index 7a4caa771e..a76cde7f16 100644 --- a/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.test.tsx +++ b/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.test.tsx @@ -1,8 +1,8 @@ -import { render, screen } from "@testing-library/react" +import { zeroRender as render } from "@/testing/test-utils" +import { screen } from "@testing-library/react" + import userEvent from "@testing-library/user-event" -import React from "react" import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" -import { defaultTranslations, I18nProvider } from "../../lib/providers/i18n" import { OneDatePickerPopup } from "./OneDatePickerPopup" import { DatePreset } from "./types" @@ -15,10 +15,6 @@ beforeAll(() => { } }) -const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - {children} -) - describe("OneDatePickerPopup", () => { const mockOnSelect = vi.fn() const mockOnOpenChange = vi.fn() @@ -47,21 +43,13 @@ describe("OneDatePickerPopup", () => { }) it("renders the trigger button", () => { - render( - - - - ) + render() expect(screen.getByText("Open Date Picker")).toBeInTheDocument() }) it("shows presets when provided", async () => { const user = userEvent.setup() - render( - - - - ) + render() await user.click(screen.getByText("Open Date Picker")) expect(screen.getByRole("option", { name: "Today" })).toBeInTheDocument() @@ -72,11 +60,7 @@ describe("OneDatePickerPopup", () => { it("calls onSelect when a preset is selected", async () => { const user = userEvent.setup() - render( - - - - ) + render() await user.click(screen.getByText("Open Date Picker")) await user.click(screen.getByRole("option", { name: "Today" })) @@ -88,12 +72,10 @@ describe("OneDatePickerPopup", () => { it("shows granularity selector when multiple granularities are provided", async () => { const user = userEvent.setup() render( - - - + ) await user.click(screen.getByText("Open Date Picker")) @@ -105,11 +87,7 @@ describe("OneDatePickerPopup", () => { it("shows custom range mode when custom preset is selected", async () => { const user = userEvent.setup() - render( - - - - ) + render() await user.click(screen.getByText("Open Date Picker")) await user.click(screen.getByRole("option", { name: "Custom" })) @@ -123,13 +101,11 @@ describe("OneDatePickerPopup", () => { const maxDate = new Date("2024-12-31") render( - - - + ) await user.click(screen.getByText("Open Date Picker")) @@ -139,11 +115,7 @@ describe("OneDatePickerPopup", () => { }) it("handles disabled state correctly", async () => { - render( - - - - ) + render() const trigger = screen.getByText("Open Date Picker") // Try to click the trigger diff --git a/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.tsx b/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.tsx index fe12b0ebd4..01d799cb59 100644 --- a/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.tsx +++ b/packages/react/src/ui/DatePickerPopup/OneDatePickerPopup.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/Actions/Button" +import { F0Button } from "@/components/F0Button" import { GranularityDefinitionKey, OneCalendar, @@ -223,11 +223,10 @@ export function OneDatePickerPopup({ {(presets.length > 0 || granularities.length > 1) && (
{presets.length > 0 && ( - - - {appendButton && ( -
button]:rounded-l-none [&>div:after]:rounded-l-none [&>div]:rounded-l-none", - size === "sm" && "h-6 w-6", - size === "md" && "h-8 w-8", - size === "lg" && "h-10 w-10" - )} - > - {appendButton} -
- )} - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } diff --git a/packages/react/src/ui/carousel.tsx b/packages/react/src/ui/carousel.tsx index f861a3253f..9e77e945ba 100644 --- a/packages/react/src/ui/carousel.tsx +++ b/packages/react/src/ui/carousel.tsx @@ -1,15 +1,15 @@ "use client" +import { ArrowLeft, ArrowRight } from "@/icons/app" import useEmblaCarousel, { type UseEmblaCarouselType, } from "embla-carousel-react" import * as React from "react" -import { F0Icon } from "../components/F0Icon" -import { ArrowLeft, ArrowRight } from "../icons/app" -import { SPACE_FOR_WIDGET_SHADOW } from "../experimental/Navigation/Carousel/DynamicCarousel" -import { cn } from "../lib/utils" -import { Button } from "./button" +import { ButtonInternal } from "@/components/F0Button/internal" +import { ButtonInternalProps } from "@/components/F0Button/internal-types" +import { SPACE_FOR_WIDGET_SHADOW } from "@/experimental/Navigation/Carousel/DynamicCarousel" +import { cn } from "@/lib/utils" type CarouselApi = UseEmblaCarouselType[1] type UseCarouselParameters = Parameters @@ -213,7 +213,7 @@ CarouselItem.displayName = "CarouselItem" const CarouselPrevious = React.forwardRef< HTMLButtonElement, - React.ComponentProps + ButtonInternalProps >(({ className, variant = "outline", ...props }, ref) => { const { orientation, scrollPrev, canScrollPrev } = useCarousel() @@ -227,60 +227,55 @@ const CarouselPrevious = React.forwardRef< : "-top-3 left-1/2 -translate-x-1/2 rotate-90" )} > - + label="Previous" + icon={ArrowLeft} + hideLabel + />
) }) CarouselPrevious.displayName = "CarouselPrevious" -const CarouselNext = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", ...props }, ref) => { - const { orientation, scrollNext, canScrollNext } = useCarousel() +const CarouselNext = React.forwardRef( + ({ className, variant = "outline", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() - return ( -
- -
- ) -}) + + + ) + } +) CarouselNext.displayName = "CarouselNext" const CarouselDots = React.forwardRef< diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d76149143..3b89d66d5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -461,6 +461,9 @@ importers: '@storybook/addon-a11y': specifier: ^9.1.3 version: 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))) + '@storybook/addon-designs': + specifier: ^10.0.2 + version: 10.0.2(@storybook/addon-docs@9.1.3(@types/react@18.3.18)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))) '@storybook/addon-docs': specifier: ^9.1.3 version: 9.1.3(@types/react@18.3.18)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))) @@ -2498,6 +2501,14 @@ packages: '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@figspec/components@1.0.3': + resolution: {integrity: sha512-fBwHzJ4ouuOUJEi+yBZIrOy+0/fAjB3AeTcIHTT1PRxLz8P63xwC7R0EsIJXhScIcc+PljGmqbbVJCjLsnaGYA==} + + '@figspec/react@1.0.4': + resolution: {integrity: sha512-jaPvkIef4d6NjsRiw91OZabrfdPH9FtoPGYcY5mpXjYEcdUqIq1aHtLq3SkMVyVysEapTEJ6yS8amy93MyXBEQ==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@floating-ui/core@1.6.9': resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} @@ -3236,6 +3247,15 @@ packages: peerDependencies: '@langchain/core': '>=0.2.21 <0.4.0' + '@lit-labs/react@1.2.1': + resolution: {integrity: sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==} + + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@1.6.3': + resolution: {integrity: sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -4746,6 +4766,21 @@ packages: peerDependencies: storybook: ^8.6.12 + '@storybook/addon-designs@10.0.2': + resolution: {integrity: sha512-MP7av/of6QMPH7bRjwjTC34GySy2bfyh3WwLWeztQOuNWMEFUOWzjh9iIyCiOBl7VADSXeL4SOJGIiGzQznFZw==} + peerDependencies: + '@storybook/addon-docs': ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 + peerDependenciesMeta: + '@storybook/addon-docs': + optional: true + react: + optional: true + react-dom: + optional: true + '@storybook/addon-docs@9.1.3': resolution: {integrity: sha512-iCzuHRyUgir2+ExqPO4ouxm90zW+6dkNuB4lyyFwNU10slJhVT8yGPk8PVOT6LhXMIii+7Hqc4dB0tj+kLOW/A==} peerDependencies: @@ -9347,6 +9382,15 @@ packages: linkifyjs@4.2.0: resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} + lit-element@3.3.3: + resolution: {integrity: sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==} + + lit-html@2.8.0: + resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==} + + lit@2.8.0: + resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -15511,6 +15555,16 @@ snapshots: '@fastify/busboy@3.1.1': {} + '@figspec/components@1.0.3': + dependencies: + lit: 2.8.0 + + '@figspec/react@1.0.4(react@18.3.1)': + dependencies: + '@figspec/components': 1.0.3 + '@lit-labs/react': 1.2.1 + react: 18.3.1 + '@floating-ui/core@1.6.9': dependencies: '@floating-ui/utils': 0.2.9 @@ -16080,6 +16134,14 @@ snapshots: transitivePeerDependencies: - encoding + '@lit-labs/react@1.2.1': {} + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@1.6.3': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lukeed/csprng@1.1.0': {} '@lukeed/uuid@2.0.1': @@ -17779,6 +17841,15 @@ snapshots: storybook: 8.6.14(prettier@2.8.8) ts-dedent: 2.2.0 + '@storybook/addon-designs@10.0.2(@storybook/addon-docs@9.1.3(@types/react@18.3.18)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0)))': + dependencies: + '@figspec/react': 1.0.4(react@18.3.1) + storybook: 9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0)) + optionalDependencies: + '@storybook/addon-docs': 9.1.3(@types/react@18.3.18)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0))) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@storybook/addon-docs@9.1.3(@types/react@18.3.18)(storybook@9.1.3(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@22.15.21)(typescript@5.8.2))(prettier@3.5.2)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.43.1)(yaml@2.7.0)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.18)(react@18.3.1) @@ -18807,8 +18878,7 @@ snapshots: '@types/tough-cookie@4.0.5': {} - '@types/trusted-types@2.0.7': - optional: true + '@types/trusted-types@2.0.7': {} '@types/twemoji-parser@13.1.4': {} @@ -23231,6 +23301,22 @@ snapshots: linkifyjs@4.2.0: {} + lit-element@3.3.3: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 1.6.3 + lit-html: 2.8.0 + + lit-html@2.8.0: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@2.8.0: + dependencies: + '@lit/reactive-element': 1.6.3 + lit-element: 3.3.3 + lit-html: 2.8.0 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11