@scripso-homepad/ui 0.3.9 → 0.4.1

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,208 @@
1
+ import { ChevronsUpDown } from 'lucide-react';
2
+ import { useCallback, useId, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
3
+
4
+ import { BuildingIcon } from '../icons/BuildingIcon';
5
+ import { useOnClickOutside } from '../hooks/useOnClickOutside';
6
+ import { cn } from '../utils/cn';
7
+ import type { AdminBuildingOption, AdminHeaderLabels } from './types';
8
+
9
+ export type BuildingSelectProps = {
10
+ options: AdminBuildingOption[];
11
+ value: string;
12
+ labels: Pick<AdminHeaderLabels, 'selectBuilding'>;
13
+ onChange?: (buildingId: string) => void;
14
+ disabled?: boolean;
15
+ className?: string;
16
+ menuClassName?: string;
17
+ buildingIcon?: ReactNode;
18
+ chevronIcon?: ReactNode;
19
+ };
20
+
21
+ export function BuildingSelect({
22
+ options,
23
+ value,
24
+ labels,
25
+ onChange,
26
+ disabled = false,
27
+ className,
28
+ menuClassName,
29
+ buildingIcon,
30
+ chevronIcon,
31
+ }: BuildingSelectProps) {
32
+ const [open, setOpen] = useState(false);
33
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
34
+ const rootRef = useRef<HTMLDivElement>(null);
35
+ const listboxId = useId();
36
+
37
+ const selectedOption = options.find((option) => option.value === value);
38
+ const displayLabel = selectedOption?.label ?? labels.selectBuilding;
39
+ const selectableOptions = options.filter((option) => !option.disabled);
40
+
41
+ const closeMenu = useCallback(() => {
42
+ setOpen(false);
43
+ setHighlightedIndex(-1);
44
+ }, []);
45
+
46
+ const openMenu = useCallback(() => {
47
+ if (disabled || selectableOptions.length === 0) {
48
+ return;
49
+ }
50
+
51
+ const selectedIndex = selectableOptions.findIndex((option) => option.value === value);
52
+ setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0);
53
+ setOpen(true);
54
+ }, [disabled, selectableOptions, value]);
55
+
56
+ const selectOption = useCallback(
57
+ (option: AdminBuildingOption) => {
58
+ if (option.disabled) {
59
+ return;
60
+ }
61
+
62
+ onChange?.(option.value);
63
+ closeMenu();
64
+ },
65
+ [closeMenu, onChange],
66
+ );
67
+
68
+ useOnClickOutside(rootRef, closeMenu, open);
69
+
70
+ const handleTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
71
+ if (disabled) {
72
+ return;
73
+ }
74
+
75
+ switch (event.key) {
76
+ case 'ArrowDown':
77
+ case 'Enter':
78
+ case ' ':
79
+ event.preventDefault();
80
+ openMenu();
81
+ break;
82
+ case 'Escape':
83
+ closeMenu();
84
+ break;
85
+ default:
86
+ break;
87
+ }
88
+ };
89
+
90
+ const handleListKeyDown = (event: KeyboardEvent<HTMLUListElement>) => {
91
+ if (!open || selectableOptions.length === 0) {
92
+ return;
93
+ }
94
+
95
+ switch (event.key) {
96
+ case 'ArrowDown':
97
+ event.preventDefault();
98
+ setHighlightedIndex((current) => (current + 1) % selectableOptions.length);
99
+ break;
100
+ case 'ArrowUp':
101
+ event.preventDefault();
102
+ setHighlightedIndex(
103
+ (current) => (current - 1 + selectableOptions.length) % selectableOptions.length,
104
+ );
105
+ break;
106
+ case 'Enter':
107
+ case ' ':
108
+ event.preventDefault();
109
+ if (highlightedIndex >= 0) {
110
+ selectOption(selectableOptions[highlightedIndex]);
111
+ }
112
+ break;
113
+ case 'Escape':
114
+ event.preventDefault();
115
+ closeMenu();
116
+ break;
117
+ case 'Tab':
118
+ closeMenu();
119
+ break;
120
+ default:
121
+ break;
122
+ }
123
+ };
124
+
125
+ return (
126
+ <div ref={rootRef} className={cn('relative w-57.5', className)}>
127
+ <button
128
+ type="button"
129
+ disabled={disabled}
130
+ aria-haspopup="listbox"
131
+ aria-expanded={open}
132
+ aria-controls={listboxId}
133
+ aria-label={labels.selectBuilding}
134
+ onClick={() => (open ? closeMenu() : openMenu())}
135
+ onKeyDown={handleTriggerKeyDown}
136
+ className={cn(
137
+ 'flex h-11 w-full items-center gap-3 rounded-xl border border-storm-gray-50 bg-white p-3 text-left transition-colors',
138
+ !disabled && 'cursor-pointer hover:bg-storm-gray-50/60',
139
+ open && 'bg-storm-gray-50/60',
140
+ disabled && 'cursor-not-allowed opacity-60',
141
+ )}
142
+ >
143
+ {buildingIcon ?? <BuildingIcon className="shrink-0 text-navy" />}
144
+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-slate-blue">
145
+ {displayLabel}
146
+ </span>
147
+ {chevronIcon ?? (
148
+ <ChevronsUpDown
149
+ size={16}
150
+ strokeWidth={1.75}
151
+ className={cn(
152
+ 'shrink-0 text-storm-gray-100 transition-transform duration-200',
153
+ open && 'rotate-180',
154
+ )}
155
+ aria-hidden
156
+ />
157
+ )}
158
+ </button>
159
+
160
+ {open ? (
161
+ <ul
162
+ id={listboxId}
163
+ role="listbox"
164
+ aria-label={labels.selectBuilding}
165
+ tabIndex={-1}
166
+ onKeyDown={handleListKeyDown}
167
+ className={cn(
168
+ 'absolute top-[calc(100%+4px)] left-0 z-50 flex w-full flex-col gap-1 overflow-hidden rounded-xl border border-storm-gray-50 bg-white p-2 shadow-[0_8px_24px_rgba(21,26,30,0.08)]',
169
+ menuClassName,
170
+ )}
171
+ >
172
+ {options.map((option) => {
173
+ const selectableIndex = selectableOptions.findIndex(
174
+ (selectable) => selectable.value === option.value,
175
+ );
176
+ const isSelected = option.value === value;
177
+ const isHighlighted = selectableIndex === highlightedIndex;
178
+
179
+ return (
180
+ <li key={option.value} role="presentation">
181
+ <button
182
+ type="button"
183
+ role="option"
184
+ aria-selected={isSelected}
185
+ disabled={option.disabled}
186
+ onMouseEnter={() => {
187
+ if (!option.disabled && selectableIndex >= 0) {
188
+ setHighlightedIndex(selectableIndex);
189
+ }
190
+ }}
191
+ onClick={() => selectOption(option)}
192
+ className={cn(
193
+ 'flex w-full rounded-lg px-3 py-2.5 text-left text-sm font-medium text-slate-blue transition-colors',
194
+ !option.disabled && 'cursor-pointer hover:bg-storm-gray-50',
195
+ (isSelected || isHighlighted) && !option.disabled && 'bg-storm-gray-50',
196
+ option.disabled && 'cursor-not-allowed opacity-50',
197
+ )}
198
+ >
199
+ <span className="truncate">{option.label}</span>
200
+ </button>
201
+ </li>
202
+ );
203
+ })}
204
+ </ul>
205
+ ) : null}
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { WebLayoutCanvas } from './story-helpers';
6
+ import { DashboardLayout } from './DashboardLayout';
7
+ import {
8
+ storyBranding,
9
+ storyBuildingOptions,
10
+ storyFooterNavItems,
11
+ storyLabels,
12
+ storyLogoIconDarkSrc,
13
+ storyNavItems,
14
+ storyUser,
15
+ } from './story-fixtures';
16
+
17
+ const meta = {
18
+ title: 'Web Layout/DashboardLayout',
19
+ parameters: {
20
+ layout: 'fullscreen',
21
+ webLayout: true,
22
+ },
23
+ } satisfies Meta;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ function DashboardLayoutDemo() {
29
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
30
+ const [searchQuery, setSearchQuery] = useState('');
31
+ const [selectedBuildingId, setSelectedBuildingId] = useState(storyBuildingOptions[0].value);
32
+
33
+ return (
34
+ <WebLayoutCanvas>
35
+ <DashboardLayout
36
+ sidebar={{
37
+ navItems: storyNavItems,
38
+ footerNavItems: storyFooterNavItems,
39
+ branding: storyBranding,
40
+ labels: storyLabels.sidebar,
41
+ sidebarStorageKey: 'homepad.storybook.sidebar.open',
42
+ user: storyUser,
43
+ mobileOpen: mobileMenuOpen,
44
+ onMobileClose: () => setMobileMenuOpen(false),
45
+ onLogout: fn(),
46
+ onNavItemClick: () => setMobileMenuOpen(false),
47
+ }}
48
+ mobileHeader={{
49
+ branding: {
50
+ logoIconSrc: storyLogoIconDarkSrc,
51
+ logoTitle: storyBranding.logoTitle,
52
+ logoTagline: storyBranding.logoTagline,
53
+ },
54
+ openMenuLabel: storyLabels.sidebar.openMenu,
55
+ onOpenMenu: () => setMobileMenuOpen(true),
56
+ menuOpen: mobileMenuOpen,
57
+ }}
58
+ header={{
59
+ title: 'Dashboard',
60
+ labels: storyLabels.header,
61
+ buildingOptions: storyBuildingOptions,
62
+ selectedBuildingId,
63
+ onBuildingChange: setSelectedBuildingId,
64
+ className: 'hidden md:flex',
65
+ searchValue: searchQuery,
66
+ onSearchChange: setSearchQuery,
67
+ onNotificationsClick: fn(),
68
+ }}
69
+ >
70
+ <div className="rounded-xl border border-storm-gray-50 bg-white p-6">
71
+ <p className="text-sm text-storm-gray-500">Page content goes here.</p>
72
+ </div>
73
+ </DashboardLayout>
74
+ </WebLayoutCanvas>
75
+ );
76
+ }
77
+
78
+ export const Default: Story = {
79
+ render: () => <DashboardLayoutDemo />,
80
+ };
81
+
82
+ export const Mobile: Story = {
83
+ render: () => <DashboardLayoutDemo />,
84
+ parameters: {
85
+ viewport: { defaultViewport: 'mobile1' },
86
+ },
87
+ };
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { AppHeader } from './AppHeader';
4
+ import type { AppHeaderProps } from './AppHeader';
5
+ import { Sidebar } from './Sidebar';
6
+ import type { SidebarProps } from './Sidebar';
7
+ import { SidebarMobileHeader } from './SidebarMobileHeader';
8
+ import type { SidebarMobileHeaderProps } from './SidebarMobileHeader';
9
+
10
+ export type DashboardLayoutProps = {
11
+ sidebar: SidebarProps;
12
+ header: AppHeaderProps;
13
+ mobileHeader: SidebarMobileHeaderProps;
14
+ children: ReactNode;
15
+ className?: string;
16
+ mainClassName?: string;
17
+ };
18
+
19
+ export function DashboardLayout({
20
+ sidebar,
21
+ header,
22
+ mobileHeader,
23
+ children,
24
+ className,
25
+ mainClassName,
26
+ }: DashboardLayoutProps) {
27
+ return (
28
+ <div className={className ?? 'flex min-h-screen bg-storm-gray-0'}>
29
+ <Sidebar {...sidebar} />
30
+ <div className="flex min-w-0 flex-1 flex-col">
31
+ <SidebarMobileHeader {...mobileHeader} />
32
+ <AppHeader {...header} />
33
+ <main className={mainClassName ?? 'flex-1 overflow-auto p-4 md:p-6'}>{children}</main>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { WebLayoutCanvas } from './story-helpers';
6
+ import { Sidebar, type SidebarProps } from './Sidebar';
7
+ import {
8
+ storyBranding,
9
+ storyFooterNavItems,
10
+ storyLabels,
11
+ storyNavItems,
12
+ storyUser,
13
+ } from './story-fixtures';
14
+
15
+ const meta = {
16
+ title: 'Web Layout/Sidebar',
17
+ component: Sidebar,
18
+ parameters: {
19
+ layout: 'fullscreen',
20
+ webLayout: true,
21
+ },
22
+ args: {
23
+ navItems: storyNavItems,
24
+ footerNavItems: storyFooterNavItems,
25
+ branding: storyBranding,
26
+ labels: storyLabels.sidebar,
27
+ sidebarStorageKey: 'homepad.storybook.sidebar.open',
28
+ user: storyUser,
29
+ mobileOpen: true,
30
+ onLogout: fn(),
31
+ onMobileClose: fn(),
32
+ onNavItemClick: fn(),
33
+ },
34
+ } satisfies Meta<typeof Sidebar>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Expanded: Story = {
40
+ render: (args) => (
41
+ <WebLayoutCanvas>
42
+ <div className="flex min-h-screen bg-storm-gray-0">
43
+ <Sidebar {...args} mobileOpen={false} initialCollapsed={false} />
44
+ </div>
45
+ </WebLayoutCanvas>
46
+ ),
47
+ };
48
+
49
+ export const Collapsed: Story = {
50
+ render: (args) => (
51
+ <WebLayoutCanvas>
52
+ <div className="flex min-h-screen bg-storm-gray-0">
53
+ <Sidebar {...args} mobileOpen={false} initialCollapsed />
54
+ </div>
55
+ </WebLayoutCanvas>
56
+ ),
57
+ };
58
+
59
+ function MobileDrawerStory(args: SidebarProps) {
60
+ const [mobileOpen, setMobileOpen] = useState(true);
61
+
62
+ return (
63
+ <WebLayoutCanvas>
64
+ <div className="relative min-h-screen bg-storm-gray-0">
65
+ <Sidebar
66
+ {...args}
67
+ mobileOpen={mobileOpen}
68
+ onMobileClose={() => setMobileOpen(false)}
69
+ />
70
+ </div>
71
+ </WebLayoutCanvas>
72
+ );
73
+ }
74
+
75
+ export const MobileDrawer: Story = {
76
+ render: (args) => <MobileDrawerStory {...args} />,
77
+ parameters: {
78
+ viewport: { defaultViewport: 'mobile1' },
79
+ },
80
+ };
@@ -0,0 +1,244 @@
1
+ import { PanelLeftClose, PanelLeftOpen, X } from 'lucide-react';
2
+ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
3
+ import { useLocation } from 'react-router-dom';
4
+
5
+ import { useMediaQuery } from '../hooks/useMediaQuery';
6
+ import { cn } from '../utils/cn';
7
+ import { SidebarNavItem } from './SidebarNavItem';
8
+ import { SidebarUserCard } from './SidebarUserCard';
9
+ import type { AdminNavItem, AdminSidebarBranding, AdminSidebarLabels, AdminSidebarUser } from './types';
10
+
11
+ export type SidebarProps = {
12
+ navItems: AdminNavItem[];
13
+ footerNavItems: AdminNavItem[];
14
+ branding: AdminSidebarBranding;
15
+ labels: AdminSidebarLabels;
16
+ sidebarStorageKey: string;
17
+ user?: AdminSidebarUser;
18
+ onLogout?: () => void;
19
+ className?: string;
20
+ mobileOpen: boolean;
21
+ onMobileClose?: () => void;
22
+ onNavItemClick?: (item: AdminNavItem) => void;
23
+ collapseIcon?: ReactNode;
24
+ expandIcon?: ReactNode;
25
+ closeIcon?: ReactNode;
26
+ initialCollapsed?: boolean;
27
+ collapsed?: boolean;
28
+ onCollapsedChange?: (collapsed: boolean) => void;
29
+ };
30
+
31
+ function readInitialCollapsedState(storageKey: string, initialCollapsed?: boolean): boolean {
32
+ if (initialCollapsed !== undefined) {
33
+ return initialCollapsed;
34
+ }
35
+
36
+ if (typeof window === 'undefined') {
37
+ return false;
38
+ }
39
+
40
+ const stored = window.localStorage.getItem(storageKey);
41
+ if (stored === null) {
42
+ return false;
43
+ }
44
+
45
+ return stored !== 'true';
46
+ }
47
+
48
+ export function Sidebar({
49
+ navItems,
50
+ footerNavItems,
51
+ branding,
52
+ labels,
53
+ sidebarStorageKey,
54
+ user,
55
+ onLogout,
56
+ className,
57
+ mobileOpen,
58
+ onMobileClose,
59
+ onNavItemClick,
60
+ collapseIcon,
61
+ expandIcon,
62
+ closeIcon,
63
+ initialCollapsed,
64
+ collapsed,
65
+ onCollapsedChange,
66
+ }: SidebarProps) {
67
+ const location = useLocation();
68
+ const isDesktop = useMediaQuery('(min-width: 768px)');
69
+ const [internalCollapsed, setInternalCollapsed] = useState(() =>
70
+ readInitialCollapsedState(sidebarStorageKey, initialCollapsed),
71
+ );
72
+
73
+ const isCollapsed = collapsed ?? internalCollapsed;
74
+ const isExpanded = isDesktop ? !isCollapsed : true;
75
+
76
+ const setCollapsed = useCallback(
77
+ (next: boolean) => {
78
+ if (collapsed === undefined) {
79
+ setInternalCollapsed(next);
80
+ window.localStorage.setItem(sidebarStorageKey, String(!next));
81
+ }
82
+
83
+ onCollapsedChange?.(next);
84
+ },
85
+ [collapsed, onCollapsedChange, sidebarStorageKey],
86
+ );
87
+
88
+ const toggleCollapsed = useCallback(() => {
89
+ setCollapsed(!isCollapsed);
90
+ }, [isCollapsed, setCollapsed]);
91
+
92
+ const handleHeaderAction = useCallback(() => {
93
+ if (isDesktop) {
94
+ toggleCollapsed();
95
+
96
+ return;
97
+ }
98
+
99
+ onMobileClose?.();
100
+ }, [isDesktop, onMobileClose, toggleCollapsed]);
101
+
102
+ const handleNavItemNavigate = useCallback(
103
+ (item: AdminNavItem) => {
104
+ onNavItemClick?.(item);
105
+
106
+ if (!isDesktop) {
107
+ onMobileClose?.();
108
+ }
109
+ },
110
+ [isDesktop, onMobileClose, onNavItemClick],
111
+ );
112
+
113
+ const pathnameRef = useRef(location.pathname);
114
+
115
+ useEffect(() => {
116
+ if (pathnameRef.current === location.pathname) {
117
+ return;
118
+ }
119
+
120
+ pathnameRef.current = location.pathname;
121
+
122
+ if (!isDesktop) {
123
+ onMobileClose?.();
124
+ }
125
+ }, [location.pathname, isDesktop, onMobileClose]);
126
+
127
+ useEffect(() => {
128
+ if (!mobileOpen || isDesktop) {
129
+ return;
130
+ }
131
+
132
+ const previousOverflow = document.body.style.overflow;
133
+ document.body.style.overflow = 'hidden';
134
+
135
+ return () => {
136
+ document.body.style.overflow = previousOverflow;
137
+ };
138
+ }, [mobileOpen, isDesktop]);
139
+
140
+ const visibleNavItems = navItems.filter((item) => !item.hidden);
141
+ const visibleFooterNavItems = footerNavItems.filter((item) => !item.hidden);
142
+
143
+ return (
144
+ <>
145
+ <button
146
+ type="button"
147
+ aria-label={labels.closeMenu}
148
+ onClick={onMobileClose}
149
+ className={cn(
150
+ 'fixed inset-0 z-40 bg-navy-800/60 backdrop-blur-[1px] transition-opacity duration-300 ease-in-out md:hidden',
151
+ mobileOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
152
+ )}
153
+ />
154
+
155
+ <aside
156
+ className={cn(
157
+ 'fixed inset-y-0 left-0 z-50 flex h-screen w-[min(300px,85vw)] flex-col gap-8 overflow-hidden bg-navy p-4 shadow-xl',
158
+ 'transition-[transform,width,padding,gap] duration-300 ease-in-out will-change-[transform,width]',
159
+ mobileOpen ? 'translate-x-0' : '-translate-x-full',
160
+ 'md:relative md:z-auto md:w-[300px] md:translate-x-0 md:shadow-none',
161
+ isExpanded ? 'md:w-[300px]' : 'md:w-[76px] md:items-center md:gap-2 md:px-3 md:py-4',
162
+ className,
163
+ )}
164
+ aria-hidden={!isDesktop && !mobileOpen}
165
+ >
166
+ <div
167
+ className={cn(
168
+ 'flex w-full items-center transition-[justify-content] duration-300 ease-in-out',
169
+ isExpanded ? 'justify-between' : 'justify-center',
170
+ )}
171
+ >
172
+ <div
173
+ className={cn(
174
+ 'flex min-w-0 items-center gap-1.5 overflow-hidden transition-[max-width,opacity] duration-300 ease-in-out',
175
+ isExpanded ? 'max-w-[240px] opacity-100' : 'max-w-0 opacity-0',
176
+ )}
177
+ >
178
+ <img src={branding.logoIconSrc} alt="" className="h-5 w-[29px] shrink-0" />
179
+ <div className="flex min-w-0 flex-col leading-none">
180
+ <span className="text-[21px] font-bold tracking-tight whitespace-nowrap text-white">
181
+ {branding.logoTitle}
182
+ </span>
183
+ <span className="text-[6.5px] font-normal tracking-[-0.26px] text-navy-300 uppercase">
184
+ {branding.logoTagline}
185
+ </span>
186
+ </div>
187
+ </div>
188
+ <button
189
+ type="button"
190
+ onClick={handleHeaderAction}
191
+ className="shrink-0 cursor-pointer text-white transition-[opacity,transform] duration-200 hover:opacity-80 active:scale-95"
192
+ aria-label={
193
+ isDesktop
194
+ ? isExpanded
195
+ ? labels.collapse
196
+ : labels.expand
197
+ : labels.closeMenu
198
+ }
199
+ >
200
+ {isDesktop ? (
201
+ isExpanded ? (
202
+ collapseIcon ?? <PanelLeftClose size={20} strokeWidth={1.75} />
203
+ ) : (
204
+ expandIcon ?? <PanelLeftOpen size={20} strokeWidth={1.75} />
205
+ )
206
+ ) : (
207
+ closeIcon ?? <X size={20} strokeWidth={1.75} />
208
+ )}
209
+ </button>
210
+ </div>
211
+
212
+ <nav className="flex flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden">
213
+ {visibleNavItems.map((item) => (
214
+ <SidebarNavItem
215
+ key={item.to}
216
+ item={item}
217
+ isOpen={isExpanded}
218
+ onNavigate={handleNavItemNavigate}
219
+ />
220
+ ))}
221
+ </nav>
222
+
223
+ <div className="flex w-full flex-col gap-4">
224
+ {visibleFooterNavItems.map((item) => (
225
+ <SidebarNavItem
226
+ key={item.to}
227
+ item={item}
228
+ isOpen={isExpanded}
229
+ onNavigate={handleNavItemNavigate}
230
+ />
231
+ ))}
232
+ {user ? (
233
+ <SidebarUserCard
234
+ user={user}
235
+ isOpen={isExpanded}
236
+ logoutLabel={labels.logout}
237
+ onLogout={onLogout}
238
+ />
239
+ ) : null}
240
+ </div>
241
+ </aside>
242
+ </>
243
+ );
244
+ }
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+
4
+ import { WebLayoutCanvas } from './story-helpers';
5
+ import { SidebarMobileHeader } from './SidebarMobileHeader';
6
+ import { storyBranding, storyLabels, storyLogoIconDarkSrc } from './story-fixtures';
7
+
8
+ const meta = {
9
+ title: 'Web Layout/SidebarMobileHeader',
10
+ component: SidebarMobileHeader,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ webLayout: true,
14
+ },
15
+ args: {
16
+ branding: {
17
+ logoIconSrc: storyLogoIconDarkSrc,
18
+ logoTitle: storyBranding.logoTitle,
19
+ logoTagline: storyBranding.logoTagline,
20
+ },
21
+ openMenuLabel: storyLabels.sidebar.openMenu,
22
+ onOpenMenu: fn(),
23
+ menuOpen: false,
24
+ },
25
+ } satisfies Meta<typeof SidebarMobileHeader>;
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ render: (args) => (
32
+ <WebLayoutCanvas>
33
+ <SidebarMobileHeader {...args} />
34
+ </WebLayoutCanvas>
35
+ ),
36
+ };
37
+
38
+ export const MenuOpen: Story = {
39
+ args: {
40
+ menuOpen: true,
41
+ },
42
+ render: (args) => (
43
+ <WebLayoutCanvas>
44
+ <SidebarMobileHeader {...args} />
45
+ </WebLayoutCanvas>
46
+ ),
47
+ };