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
}) => (