diff --git a/.storybook/preview.ts b/.storybook/preview.tsx similarity index 61% rename from .storybook/preview.ts rename to .storybook/preview.tsx index ff58bbda..f55d4544 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.tsx @@ -1,6 +1,16 @@ import type { Preview } from "@storybook/react"; +import * as React from "react"; + +import { Page } from "../src"; const preview: Preview = { + decorators: [ + (Story) => ( + + + + ), + ], parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { @@ -9,6 +19,7 @@ const preview: Preview = { date: /Date$/i, }, }, + layout: "fullscreen", }, }; diff --git a/package-lock.json b/package-lock.json index 1072ce01..d18c09d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@floating-ui/react": "^0.26.5", "@heroicons/react": "^2.1.1", "clsx": "^2.1.0" }, @@ -2135,7 +2136,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.5.3", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.0" @@ -2143,16 +2143,28 @@ }, "node_modules/@floating-ui/dom": { "version": "1.5.4", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/core": "^1.5.3", "@floating-ui/utils": "^0.2.0" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.5.tgz", + "integrity": "sha512-LJeSQa+yOwV0Tdpc/C3Vr92QMrwRqRMTk4yOwsRJKc57x3Lcw317GE0EV+ECM7+Z89yEAPBe7nzbDEWfkWCrBA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.5", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.0.5", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.5.4" @@ -2164,7 +2176,6 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.1", - "dev": true, "license": "MIT" }, "node_modules/@hapi/hoek": { @@ -19172,7 +19183,6 @@ }, "node_modules/react-dom": { "version": "18.2.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -20087,7 +20097,6 @@ }, "node_modules/scheduler": { "version": "0.23.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -20931,6 +20940,11 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tapable": { "version": "2.2.1", "dev": true, diff --git a/package.json b/package.json index 254e3182..18a39ed0 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "readme": "ERROR: No README data found!", "_id": "maykin-ui@0.0.0", "dependencies": { + "@floating-ui/react": "^0.26.5", "@heroicons/react": "^2.1.1", "clsx": "^2.1.0" } diff --git a/src/components/button/button.scss b/src/components/button/button.scss index 3b7e41bb..215ef03e 100644 --- a/src/components/button/button.scss +++ b/src/components/button/button.scss @@ -6,6 +6,7 @@ --mykn-button-color-text: var(--theme-shade-0); --mykn-button-offset: 0px; + align-items: center; appearance: none; margin: 0; padding: var(--spacing-v-s) var(--spacing-h-s); @@ -16,9 +17,12 @@ box-sizing: border-box; color: var(--mykn-button-color-text); cursor: pointer; - display: inline-block; + display: inline-flex; + flex-wrap: wrap; + gap: 0.5em; font-family: Inter, sans-serif; font-size: var(--typography-font-size-body-s); + justify-content: center; line-height: var(--typography-line-height-body-s); text-align: center; text-decoration: none; @@ -33,6 +37,7 @@ --mykn-button-offset: -2px; } + &[aria-expanded=true], &:active { --mykn-button-color-background: var(--theme-color-primary-800); --mykn-button-offset: 0px; @@ -50,7 +55,9 @@ --mykn-button-offset: -2px; } + &[aria-expanded=true], &:active { + --mykn-button-color-background: var(--typography-color-background); --mykn-button-offset: 0px; } } diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx index ca329cbb..f5ad249f 100644 --- a/src/components/button/button.tsx +++ b/src/components/button/button.tsx @@ -1,73 +1,56 @@ import clsx from "clsx"; -import React from "react"; +import React, { LegacyRef } from "react"; import "./button.scss"; -export type ButtonProps = React.PropsWithChildren<{ +type BaseButtonProps = { variant?: "primary" | "transparent"; -}>; - -/** - * Higher-order component (HOC) applying button logic to WrappedComponent - * @param WrappedComponent - * @private - */ -const withButtonBehavior =

( - WrappedComponent: React.ComponentType

, -) => { - // Define the enhanced component - const EnhancedButton: React.FC

= ({ - children, - variant = "primary", - ...props - }) => { - return ( - - {children} - - ); - }; - - return EnhancedButton; }; -/** - * Base template for Button component - * @private - */ -const BaseButton: React.FC< - ButtonProps & React.ButtonHTMLAttributes -> = (props) => { - return ; -}; +export type ButtonProps = React.ButtonHTMLAttributes & + BaseButtonProps; -/** - * Base template for ButtonLink component - * @private - */ -const BaseButtonLink: React.FC< - ButtonProps & React.AnchorHTMLAttributes -> = (props) => { - return {props.children}; -}; +export type ButtonLinkProps = React.AnchorHTMLAttributes & + BaseButtonProps; /** * Button component - * @param children - * @param type + * @param variant * @param props * @constructor */ -export const Button = withButtonBehavior(BaseButton); +export const Button = React.forwardRef( + ({ variant = "primary", ...props }, ref) => { + return ( + + ); + }, +); +Button.displayName = "Button"; /** - * Anchor () version of button component - * @param children - * @param type + * Button component + * @param variant * @param props * @constructor */ -export const ButtonLink = withButtonBehavior(BaseButtonLink); +export const ButtonLink = React.forwardRef( + ({ variant, ...props }, ref) => { + return ( + } + className={clsx("mykn-button", `mykn-button--variant-${variant}`)} + {...props} + > + {props.children} + + ); + }, +); +ButtonLink.displayName = "ButtonLink"; diff --git a/src/components/dropdown/dropdown.scss b/src/components/dropdown/dropdown.scss new file mode 100644 index 00000000..15223fc4 --- /dev/null +++ b/src/components/dropdown/dropdown.scss @@ -0,0 +1,45 @@ +@use '../../settings/style'; + +.mykn-dropdown { + display: inline-flex; + position: relative; + justify-content: center; + + &--open > .mykn-button { + outline: 1px solid var(--theme-color-primary-800) + } + + + .mykn-a, + .mykn-button, + .mykn-dropdown { + width: 100%; + } + + &__dropdown { + width: fit-content; + border: 1px solid var(--theme-color-primary-800); + border-radius: 4px; + box-sizing: border-box; + padding: 2px; + } + + @media screen and (max-width: style.$breakpoint-desktop - 1px) { + &__dropdown { + width: 100%; // Fallback. + width: min(100%, max(calc(100vw - 2 * var(--spacing-h-xl)), 100cqw)); + } + } + + @media screen and (min-width: style.$breakpoint-desktop) { + &__dropdown .mykn-toolbar--direction-horizontal { + width: min-content; + } + } + + @media screen and (min-width: style.$breakpoint-desktop) { + &__dropdown .mykn-toolbar--direction-vertical { + width: max-content; + } + } +} diff --git a/src/components/dropdown/dropdown.stories.tsx b/src/components/dropdown/dropdown.stories.tsx new file mode 100644 index 00000000..46868542 --- /dev/null +++ b/src/components/dropdown/dropdown.stories.tsx @@ -0,0 +1,287 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, within } from "@storybook/test"; +import React from "react"; + +import { Button, ButtonLink } from "../button"; +import { Outline } from "../icon"; +import { Toolbar } from "../toolbar"; +import { Dropdown } from "./dropdown"; + +const meta = { + title: "Controls/Dropdown", + component: Dropdown, + parameters: { + layout: "fullscreen", + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByText("Click me!"); + + // Click opens, escape closes. + await userEvent.click(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + userEvent.keyboard("{Escape}"); + expect(await canvas.findByRole("dialog")).not.toBeVisible(); + await userEvent.click(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + + // Tab focuses items. + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + + const buttons = await canvas.findAllByRole("button"); + expect(buttons[buttons.length - 1]).toBe(document.activeElement); + }, +} satisfies Meta; + +const DEFAULT_CHILDREN = ( + + + + + + + + Admin + + + + +); + +export default meta; +type Story = StoryObj; + +export const DropdownComponent: Story = { + args: { + label: ( + <> + Click me! + + + ), + children: DEFAULT_CHILDREN, + }, +}; + +export const DropdownOnMobile: Story = { + ...DropdownComponent, + parameters: { + viewport: { defaultViewport: "mobile1" }, + }, +}; + +export const ActivateOnHover: Story = { + args: { + activateOnHover: true, + label: ( + <> + Hover me! + + + ), + children: DEFAULT_CHILDREN, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByText("Hover me!"); + + // Click opens, escape closes. + await userEvent.hover(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + userEvent.keyboard("{Escape}"); + expect(await canvas.findByRole("dialog")).not.toBeVisible(); + await userEvent.hover(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + + // Tab focuses items. + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + + const buttons = await canvas.findAllByRole("button"); + expect(buttons[buttons.length - 1]).toBe(document.activeElement); + }, +}; + +export const ActivateOnFocus: Story = { + ...ActivateOnHover, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Click opens, escape closes. + await userEvent.tab({ delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + userEvent.keyboard("{Escape}"); + expect(await canvas.findByRole("dialog")).not.toBeVisible(); + await userEvent.tab({ shift: true, delay: 10 }); + await userEvent.tab({ delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + + // Tab focuses items. + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + + const buttons = await canvas.findAllByRole("button"); + expect(buttons[buttons.length - 1]).toBe(document.activeElement); + }, +}; + +export const DropdownVariant: Story = { + args: { + label: ( + <> + Click me! + + + ), + variant: "transparent", + children: DEFAULT_CHILDREN, + }, +}; + +export const HorizontalDropdown: Story = { + args: { + label: ( + <> + Click me! + + + ), + children: ( + + + + + + + + Admin + + + + + ), + }, +}; + +export const DropdownInToolbar: Story = { + args: { + label: ( + <> + Click me! + + + ), + children: DEFAULT_CHILDREN, + }, + render: (args) => ( + + + + + + + + Admin + + + + + + ), +}; + +export const NestedDropdown: Story = { + args: { + label: ( + <> + Click me! + + + ), + variant: "primary", + children: ( + + + + + + + + Hover me! + + } + placement="right" + variant="transparent" + > + + + + Admin + + + + + + + ), + }, +}; diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx new file mode 100644 index 00000000..2200c4c9 --- /dev/null +++ b/src/components/dropdown/dropdown.tsx @@ -0,0 +1,120 @@ +import { + FloatingFocusManager, + Side, + autoUpdate, + flip, + offset, + safePolygon, + shift, + useClick, + useDismiss, + useFloating, + useFocus, + useHover, + useInteractions, + useRole, +} from "@floating-ui/react"; +import clsx from "clsx"; +import React, { useEffect, useState } from "react"; + +import { Button, ButtonProps } from "../button"; +import { ToolbarProps } from "../toolbar"; +import "./dropdown.scss"; + +export type DropdownProps = ButtonProps & { + /** Button content. */ + label: React.ReactNode; + + /** If true, the dropdown is opened when hovered. */ + activateOnHover?: boolean; + + /** Preferred placement position. */ + placement?: Side; + + /** Whether the dropdown should be open. */ + open?: boolean; + + /** `Children` should be a `Toolbar` component containing the individual items. */ + children: React.ReactElement; +}; + +/** + * Dropdown component, behaves much like a button except it's label should be + * passed using the `label` prop. `Children` should be a `Toolbar` component containing + * the individual items. + * @param children + * @param hover + * @param label + * @param open + * @param placement + * @param props + * @constructor + */ +export const Dropdown: React.FC = ({ + children, + activateOnHover = false, + label, + open = false, + placement = "bottom", + ...props +}) => { + const [isOpen, setIsOpen] = useState(false); + + /** + * Sync isOpen with open prop. + */ + useEffect(() => setIsOpen(open), [open]); + + /** + * Initialize Floating UI. + */ + const { refs, floatingStyles, context } = useFloating({ + middleware: [offset(6), flip(), shift({ padding: 20 })], + open: isOpen, + onOpenChange: setIsOpen, + placement: placement, + strategy: "fixed", + whileElementsMounted: autoUpdate, + }); + + const focus = useFocus(context, { + enabled: activateOnHover, + }); + const hover = useHover(context, { + enabled: activateOnHover, + handleClose: safePolygon(), + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + const role = useRole(context); + + // Merge all the interactions into prop getters + const { getReferenceProps, getFloatingProps } = useInteractions([ + click, + focus, + hover, + dismiss, + role, + ]); + + return ( +

+ + {isOpen && ( + +
+ {children} +
+
+ )} +
+ ); +}; diff --git a/src/components/dropdown/index.ts b/src/components/dropdown/index.ts new file mode 100644 index 00000000..22dfbe30 --- /dev/null +++ b/src/components/dropdown/index.ts @@ -0,0 +1 @@ +export * from "./dropdown"; diff --git a/src/components/icon/icon.scss b/src/components/icon/icon.scss index 202258c4..87bf9681 100644 --- a/src/components/icon/icon.scss +++ b/src/components/icon/icon.scss @@ -2,9 +2,4 @@ height: 1.2em; vertical-align: middle; width: 1.2em; - margin: 0 0.5em; - - &:first-child { - margin-inline-start: 0; - } } diff --git a/src/components/index.ts b/src/components/index.ts index 3fd9914f..97edb121 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,10 @@ // Auto-generated file. Do not modify manually. export * from "./button"; +export * from "./dropdown"; export * from "./icon"; export * from "./layout"; export * from "./logo"; +export * from "./navbar"; export * from "./page"; export * from "./toolbar"; export * from "./typography"; diff --git a/src/components/layout/layout.stories.tsx b/src/components/layout/layout.stories.tsx index 35cc91d5..1f5c0525 100644 --- a/src/components/layout/layout.stories.tsx +++ b/src/components/layout/layout.stories.tsx @@ -7,9 +7,6 @@ import { Grid } from "./grid"; const meta = { title: "Layout/Reference", - parameters: { - layout: "fullscreen", - }, render: (args) => ( diff --git a/src/components/logo/logo.scss b/src/components/logo/logo.scss index 8c77a252..8317acc8 100644 --- a/src/components/logo/logo.scss +++ b/src/components/logo/logo.scss @@ -1,9 +1,11 @@ .mykn-logo { + height: 34px; &__image { - width: 100%; - object-fit: contain; + height: 100%; max-width: 155px; + object-fit: contain; overflow: visible; + width: 100%; } & &__handle { diff --git a/src/components/navbar/index.ts b/src/components/navbar/index.ts new file mode 100644 index 00000000..e4e192d5 --- /dev/null +++ b/src/components/navbar/index.ts @@ -0,0 +1 @@ +export * from "./navbar"; diff --git a/src/components/navbar/navbar.scss b/src/components/navbar/navbar.scss new file mode 100644 index 00000000..709e2b05 --- /dev/null +++ b/src/components/navbar/navbar.scss @@ -0,0 +1,4 @@ +.mykn-navbar { + float: right; // Fallback. + float: inline-end +} diff --git a/src/components/navbar/navbar.stories.tsx b/src/components/navbar/navbar.stories.tsx new file mode 100644 index 00000000..b616c1af --- /dev/null +++ b/src/components/navbar/navbar.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, within } from "@storybook/test"; +import React from "react"; + +import { Button, ButtonLink } from "../button"; +import { Outline } from "../icon"; +import { Navbar } from "./navbar"; + +const meta = { + title: "Controls/Navbar", + component: Navbar, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const NavbarComponent: Story = { + args: { + children: ( + <> + + + + + + + Admin + + + + + ), + }, +}; + +export const NavbarOnMobile: Story = { + ...NavbarComponent, + parameters: { + viewport: { defaultViewport: "mobile1" }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole("button"); + + // Click opens, escape closes. + await userEvent.click(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + userEvent.keyboard("{Escape}"); + expect(await canvas.findByRole("dialog")).not.toBeVisible(); + await userEvent.click(button, { delay: 10 }); + expect(await canvas.findByRole("dialog")).toBeVisible(); + + // Tab focuses items. + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + await userEvent.tab({ delay: 10 }); + + const buttons = await canvas.findAllByRole("button"); + expect(buttons[buttons.length - 1]).toBe(document.activeElement); + }, +}; diff --git a/src/components/navbar/navbar.tsx b/src/components/navbar/navbar.tsx new file mode 100644 index 00000000..db92e079 --- /dev/null +++ b/src/components/navbar/navbar.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from "react"; + +import { Dropdown } from "../dropdown"; +import { Outline } from "../icon"; +import { Toolbar, ToolbarProps } from "../toolbar"; +import "./navbar.scss"; + +export type NavbarProps = ToolbarProps; + +/** + * Shows a toolbar or dropdown based on mobile/desktop. + * @param children + * @param props + * @constructor + */ +export const Navbar: React.FC = ({ children, ...props }) => { + const [isMobile, setIsMobile] = useState( + window?.matchMedia("(max-width: 767px)").matches, + ); + + /** + * Updates `isMobile` on resize. + */ + useEffect(() => { + const onResize = () => + setIsMobile(window?.matchMedia("(max-width: 767px)").matches); + + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }); + + /** + * Renders the toolbar. + */ + const renderToolbar = () => ( + + {children} + + ); + + return ( +
+ {isMobile ? ( + } + variant="transparent" + aria-label="Menu openen/sluiten" + > + {renderToolbar()} + + ) : ( + renderToolbar() + )} +
+ ); +}; diff --git a/src/components/page/page.scss b/src/components/page/page.scss index 8704c87a..c6aad0f8 100644 --- a/src/components/page/page.scss +++ b/src/components/page/page.scss @@ -1,7 +1,15 @@ +@use '../../settings/style'; + .mykn-page { background-color: var(--theme-color-primary-200); + container-name: page; + container-type: size; box-sizing: border-box; + padding: var(--spacing-h-xl); width: 100%; height: 100%; - padding: 50px 20px; + + @media screen and (min-width: style.$breakpoint-desktop) { + padding: var(--spacing-v-xl) var(--spacing-h-xl); + } } diff --git a/src/components/page/page.stories.tsx b/src/components/page/page.stories.tsx index 6a0bc8a8..e4d915a6 100644 --- a/src/components/page/page.stories.tsx +++ b/src/components/page/page.stories.tsx @@ -6,7 +6,7 @@ import { Outline } from "../icon"; import { Container, Grid } from "../layout"; import { Column } from "../layout/column"; import { Logo } from "../logo"; -import { Toolbar } from "../toolbar"; +import { Navbar } from "../navbar"; import { Page } from "./page"; type PagePropsAndCustomArgs = React.ComponentProps & { @@ -38,16 +38,14 @@ export const SamplePage: Story = { - + - - - + - + ), }; + +export const SamplePageOnMobile: Story = { + ...SamplePage, + parameters: { + viewport: { defaultViewport: "mobile1" }, + }, +}; diff --git a/src/components/toolbar/toolbar.scss b/src/components/toolbar/toolbar.scss index 879c0965..09a98a27 100644 --- a/src/components/toolbar/toolbar.scss +++ b/src/components/toolbar/toolbar.scss @@ -2,37 +2,73 @@ .mykn-toolbar { $self: &; + background-color: var(--typography-color-background); column-gap: 24px; display: flex; flex-direction: column; justify-content: flex-start; + padding: var(--spacing-v-s) 0; row-gap: 12px; text-align: start; - &--align-center { - justify-content: center; - text-align: center; + &--variant-transparent { + background-color: transparent; } - &--align-end { - justify-content: end; - text-align: end; - } + @media screen and (min-width: style.$breakpoint-desktop) { + &--align-start { + align-items: start; + justify-content: start; + text-align: start; + } - &--align-center { - justify-content: center; - text-align: center; - } + &--align-end { + align-items: end; + justify-content: end; + text-align: end; + } + + &--align-center { + align-items: center; + justify-content: center; + text-align: center; + } - @media screen and (min-width: style.$breakpoint-desktop) { &--direction-horizontal { align-items: center; flex-direction: row; } + + &--direction-vertical#{&}--align-start .mykn-a, + &--direction-vertical#{&}--align-start .mykn-button, + &--direction-vertical#{&}--align-start .mykn-dropdown { + display: flex; + justify-content: start; + text-align: start; + } + + &--direction-vertical#{&}--align-end .mykn-a, + &--direction-vertical#{&}--align-end .mykn-button, + &--direction-vertical#{&}--align-end .mykn-dropdown { + display: flex; + justify-content: end; + text-align: end; + } + + &--direction-vertical#{&}--align-center .mykn-a, + &--direction-vertical#{&}--align-center .mykn-button, + &--direction-vertical#{&}--align-center .mykn-dropdown { + width: 100%; + } } &--pad-a .mykn-a { + box-sizing: border-box; padding: var(--spacing-v-s) var(--spacing-h-s); + + @media screen and (max-width: style.$breakpoint-desktop - 1px) { + width: 100%; + } } .mykn-a { diff --git a/src/components/toolbar/toolbar.stories.tsx b/src/components/toolbar/toolbar.stories.tsx index ec6dae02..93d88f25 100644 --- a/src/components/toolbar/toolbar.stories.tsx +++ b/src/components/toolbar/toolbar.stories.tsx @@ -47,78 +47,34 @@ export const ToolbarComponent: Story = { }, }; -export const ToolbarOnMobile = { - args: { - align: "end", - children: ( - <> - - - - - - - Admin - - - - - ), - }, +export const ToolbarOnMobile: Story = { + ...ToolbarComponent, parameters: { viewport: { defaultViewport: "mobile1" }, }, }; +export const TransparentToolbar: Story = { + ...ToolbarComponent, + args: { + ...ToolbarComponent.args, + variant: "transparent", + }, +}; + export const VerticalToolbar: Story = { + ...ToolbarComponent, args: { + ...ToolbarComponent.args, direction: "vertical", - children: ( - <> - - - - - - - Admin - - - - - ), + variant: "transparent", }, }; export const ToolbarWithLinks: Story = { args: { direction: "vertical", + variant: "transparent", children: ( <> @@ -163,9 +119,10 @@ export const ToolbarWithLinks: Story = { export const MixedToolbar: Story = { args: { - align: "center", + align: "start", direction: "vertical", padA: true, + variant: "transparent", children: ( <> diff --git a/src/components/toolbar/toolbar.tsx b/src/components/toolbar/toolbar.tsx index a0334c75..294cf9cd 100644 --- a/src/components/toolbar/toolbar.tsx +++ b/src/components/toolbar/toolbar.tsx @@ -13,12 +13,15 @@ export type ToolbarProps = React.PropsWithChildren< /** When set to true, padding is applied to A components to match Button component's height. */ padA?: boolean; + + /** The variant (style) of the toolbar. */ + variant?: "normal" | "transparent"; } >; /** * A flexible and customizable toolbar component for arranging and aligning - * various interactive elements such as buttons, links, or other components. + * various interactive elements such as `A`, `Button`, `ButtonLink` and `Dropdown`. * @param children * @param align * @param direction @@ -31,6 +34,7 @@ export const Toolbar: React.FC = ({ align = "start", direction = "horizontal", padA = false, + variant = "normal", ...props }) => (