@beydesign/storybook 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import { TabMenuProps } from './types';
3
+ export declare const TabMenu: React.FC<TabMenuProps>;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useLayoutEffect, useRef, useState } from 'react';
3
+ import { Box } from '@mui/material';
4
+ import { Typography, TypographySize, TypographyWeight } from '../Typography';
5
+ import { TabMenuSize } from './types';
6
+ const SIZE_TOKENS = {
7
+ [TabMenuSize.md]: {
8
+ containerGap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)',
9
+ containerPadding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)',
10
+ tabPaddingY: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)',
11
+ tabRadius: 'var(--spacing-tokens-size-in-px-radius-radius-xs)',
12
+ tabHeight: '40px',
13
+ dividerHeight: '24px',
14
+ textSize: TypographySize.TextM,
15
+ textWeight: TypographyWeight.SemiBold,
16
+ },
17
+ [TabMenuSize.sm]: {
18
+ containerGap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
19
+ containerPadding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
20
+ tabPaddingY: '5px',
21
+ tabRadius: 'var(--spacing-tokens-size-in-px-radius-radius-sm)',
22
+ tabHeight: 'auto',
23
+ dividerHeight: '20px',
24
+ textSize: TypographySize.TextS,
25
+ textWeight: TypographyWeight.Medium,
26
+ },
27
+ };
28
+ const normalizeTab = (tab) => typeof tab === 'string' ? { label: tab } : tab;
29
+ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.md, showDividers = true, animated = true, style, tabStyle, testId, }) => {
30
+ const tokens = SIZE_TOKENS[size];
31
+ const containerRef = useRef(null);
32
+ const tabRefs = useRef([]);
33
+ const [indicator, setIndicator] = useState(null);
34
+ useLayoutEffect(() => {
35
+ const measure = () => {
36
+ const container = containerRef.current;
37
+ const tab = tabRefs.current[activeTab];
38
+ if (!container || !tab)
39
+ return;
40
+ const containerRect = container.getBoundingClientRect();
41
+ const tabRect = tab.getBoundingClientRect();
42
+ setIndicator({
43
+ left: tabRect.left - containerRect.left,
44
+ top: tabRect.top - containerRect.top,
45
+ width: tabRect.width,
46
+ height: tabRect.height,
47
+ });
48
+ };
49
+ measure();
50
+ window.addEventListener('resize', measure);
51
+ return () => window.removeEventListener('resize', measure);
52
+ }, [activeTab, size, showDividers, tabs]);
53
+ return (_jsxs(Box, { ref: containerRef, "data-testid": testId, position: 'relative', display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: tokens.containerGap, padding: tokens.containerPadding, bgcolor: 'var(--color-background-bg-component)', border: '1px solid var(--color-border-border-component)', borderRadius: 'var(--spacing-tokens-size-in-px-radius-radius-md)', sx: style, children: [indicator && (_jsx(Box, { "aria-hidden": true, position: 'absolute', bgcolor: 'var(--color-background-bg-brand-transparent)', borderRadius: tokens.tabRadius, sx: {
54
+ left: 0,
55
+ top: 0,
56
+ width: indicator.width,
57
+ height: indicator.height,
58
+ transform: `translate(${indicator.left}px, ${indicator.top}px)`,
59
+ transition: animated
60
+ ? 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)'
61
+ : 'none',
62
+ pointerEvents: 'none',
63
+ zIndex: 0,
64
+ } })), tabs.map((tab, index) => {
65
+ const { label } = normalizeTab(tab);
66
+ const isActive = index === activeTab;
67
+ return (_jsxs(React.Fragment, { children: [showDividers && index > 0 && (_jsx(Box, { width: '1px', height: tokens.dividerHeight, bgcolor: 'var(--color-border-border-divider)', flexShrink: 0, sx: { zIndex: 1 } })), _jsx(Box, { ref: (el) => {
68
+ tabRefs.current[index] = el;
69
+ }, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', flexShrink: 0, height: tokens.tabHeight, paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-4xl)', paddingY: tokens.tabPaddingY, borderRadius: tokens.tabRadius, onClick: onTabChange ? () => onTabChange(index) : undefined, sx: {
70
+ zIndex: 1,
71
+ cursor: onTabChange ? 'pointer' : 'default',
72
+ '&:hover': onTabChange && !isActive
73
+ ? {
74
+ bgcolor: 'var(--color-background-bg-brand-transparent-hover)',
75
+ transition: 'background-color 0.15s ease',
76
+ }
77
+ : undefined,
78
+ ...tabStyle,
79
+ }, children: _jsx(Typography, { size: tokens.textSize, weight: tokens.textWeight, color: isActive
80
+ ? 'var(--color-text-text-clicable)'
81
+ : 'var(--color-text-text-subtle)', textStyle: {
82
+ transition: animated ? 'color 0.25s ease' : 'none',
83
+ }, children: label }) })] }, index));
84
+ })] }));
85
+ };
@@ -0,0 +1,9 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { TabMenu } from './TabMenu';
3
+ declare const meta: Meta<typeof TabMenu>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof TabMenu>;
6
+ export declare const Medium: Story;
7
+ export declare const Small: Story;
8
+ export declare const MediumStatic: Story;
9
+ export declare const SmallStatic: Story;
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { TabMenu } from './TabMenu';
4
+ import { TabMenuSize } from './types';
5
+ const meta = {
6
+ title: 'Design System/Components/UI/TabMenu',
7
+ component: TabMenu,
8
+ parameters: {
9
+ layout: 'centered',
10
+ design: {
11
+ type: 'figspec',
12
+ url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=6516-41431',
13
+ },
14
+ },
15
+ tags: ['autodocs'],
16
+ argTypes: {
17
+ tabs: {
18
+ control: 'object',
19
+ description: 'Tabs to render (labels or { label, value } objects)',
20
+ table: {
21
+ type: { summary: 'string[] | TabMenuTab[]' },
22
+ },
23
+ },
24
+ activeTab: {
25
+ control: 'number',
26
+ description: 'Index of the active tab',
27
+ table: {
28
+ type: { summary: 'number' },
29
+ defaultValue: { summary: '0' },
30
+ },
31
+ },
32
+ onTabChange: {
33
+ description: 'Called with the index of the clicked tab',
34
+ table: {
35
+ type: { summary: 'function' },
36
+ },
37
+ },
38
+ size: {
39
+ control: 'select',
40
+ options: Object.values(TabMenuSize),
41
+ description: 'Size variant of the tab menu',
42
+ table: {
43
+ type: { summary: 'string' },
44
+ defaultValue: { summary: 'md' },
45
+ },
46
+ },
47
+ showDividers: {
48
+ control: 'boolean',
49
+ description: 'Show the vertical dividers between tabs',
50
+ table: {
51
+ type: { summary: 'boolean' },
52
+ defaultValue: { summary: 'true' },
53
+ },
54
+ },
55
+ animated: {
56
+ control: 'boolean',
57
+ description: 'Animate the active highlight when switching tabs',
58
+ table: {
59
+ type: { summary: 'boolean' },
60
+ defaultValue: { summary: 'true' },
61
+ },
62
+ },
63
+ testId: {
64
+ control: 'text',
65
+ description: 'Test ID of the tab menu',
66
+ table: {
67
+ type: { summary: 'string' },
68
+ },
69
+ },
70
+ },
71
+ };
72
+ export default meta;
73
+ const TABS = ['Tab name', 'Tab name', 'Tab name', 'Tab name', 'Tab name'];
74
+ const InteractiveTabMenu = (args) => {
75
+ const [activeTab, setActiveTab] = useState(args.activeTab ?? 0);
76
+ return _jsx(TabMenu, { ...args, activeTab: activeTab, onTabChange: setActiveTab });
77
+ };
78
+ export const Medium = {
79
+ render: (args) => _jsx(InteractiveTabMenu, { ...args }),
80
+ args: {
81
+ tabs: TABS,
82
+ activeTab: 0,
83
+ size: TabMenuSize.md,
84
+ showDividers: true,
85
+ animated: true,
86
+ testId: 'tab-menu',
87
+ },
88
+ };
89
+ export const Small = {
90
+ render: (args) => _jsx(InteractiveTabMenu, { ...args }),
91
+ args: {
92
+ ...Medium.args,
93
+ size: TabMenuSize.sm,
94
+ },
95
+ };
96
+ export const MediumStatic = {
97
+ render: (args) => _jsx(InteractiveTabMenu, { ...args }),
98
+ args: {
99
+ ...Medium.args,
100
+ animated: false,
101
+ },
102
+ };
103
+ export const SmallStatic = {
104
+ render: (args) => _jsx(InteractiveTabMenu, { ...args }),
105
+ args: {
106
+ ...Medium.args,
107
+ size: TabMenuSize.sm,
108
+ animated: false,
109
+ },
110
+ };
@@ -12,3 +12,4 @@ export * from './SkeletonLoader';
12
12
  export * from './Topbar';
13
13
  export * from './types';
14
14
  export * from './Stepper';
15
+ export * from './TabMenu';
@@ -12,3 +12,4 @@ export * from './SkeletonLoader';
12
12
  export * from './Topbar';
13
13
  export * from './types';
14
14
  export * from './Stepper';
15
+ export * from './TabMenu';
@@ -444,3 +444,42 @@ export interface StepperProps {
444
444
  /** Connector width of the stepper */
445
445
  connectorWidth?: string | number;
446
446
  }
447
+ /**
448
+ * TabMenu size variants
449
+ */
450
+ export declare enum TabMenuSize {
451
+ md = "md",
452
+ sm = "sm"
453
+ }
454
+ /**
455
+ * A single tab within a {@link TabMenu}.
456
+ */
457
+ export interface TabMenuTab {
458
+ /** Label shown on the tab */
459
+ label: string;
460
+ /** Optional value identifying the tab. Defaults to the label. */
461
+ value?: string;
462
+ }
463
+ /**
464
+ * TabMenu component props — a bordered segmented control of selectable tabs.
465
+ */
466
+ export interface TabMenuProps {
467
+ /** Tabs to render. Accepts labels or `{ label, value }` objects. */
468
+ tabs: (string | TabMenuTab)[];
469
+ /** Index of the active tab. Defaults to `0`. */
470
+ activeTab?: number;
471
+ /** Called with the index of the tab that was clicked. */
472
+ onTabChange?: (index: number) => void;
473
+ /** Size variant. Defaults to `Md`. */
474
+ size?: TabMenuSize;
475
+ /** Show the vertical dividers between tabs. Defaults to `true`. */
476
+ showDividers?: boolean;
477
+ /** Animate the active highlight when switching tabs. Defaults to `true`. */
478
+ animated?: boolean;
479
+ /** Custom style of the container. */
480
+ style?: SxProps<Theme>;
481
+ /** Custom style applied to each tab. */
482
+ tabStyle?: SxProps<Theme>;
483
+ /** Test ID of the tab menu. */
484
+ testId?: string;
485
+ }
@@ -119,3 +119,11 @@ export var FeatureBannerMedia;
119
119
  FeatureBannerMedia["Image"] = "Image";
120
120
  FeatureBannerMedia["None"] = "None";
121
121
  })(FeatureBannerMedia || (FeatureBannerMedia = {}));
122
+ /**
123
+ * TabMenu size variants
124
+ */
125
+ export var TabMenuSize;
126
+ (function (TabMenuSize) {
127
+ TabMenuSize["md"] = "md";
128
+ TabMenuSize["sm"] = "sm";
129
+ })(TabMenuSize || (TabMenuSize = {}));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beydesign/storybook",
3
3
  "private": false,
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",