mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 12:33:18 +00:00
feat: layout component
This commit is contained in:
@@ -1,4 +1,10 @@
|
|||||||
.root {
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
z-index: auto;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
padding: 12px var(--horizontal-padding);
|
padding: 12px var(--horizontal-padding);
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
.email {
|
.email {
|
||||||
color: var(--color-dark-100);
|
color: var(--color-dark-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-decoration: underline dashed var(--color-light-100) 1px;
|
||||||
|
text-underline-offset: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Logout, PersonOutline } from '@/assets'
|
import { Logout, PersonOutline } from '@/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -27,9 +26,12 @@ export const UserDropdown = ({ avatar, email, onLogout, userName }: UserDropdown
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button rounded variant={'icon'}>
|
<button className={s.trigger}>
|
||||||
|
<Typography className={s.name} variant={'subtitle1'}>
|
||||||
|
{userName}
|
||||||
|
</Typography>
|
||||||
<Avatar src={avatar} />
|
<Avatar src={avatar} />
|
||||||
</Button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
|||||||
1
src/components/layout/index.ts
Normal file
1
src/components/layout/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './layout.tsx'
|
||||||
14
src/components/layout/layout.module.scss
Normal file
14
src/components/layout/layout.module.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.layout {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--header-height) var(--horizontal-padding) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
36
src/components/layout/layout.stories.tsx
Normal file
36
src/components/layout/layout.stories.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import { Layout } from './'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
argTypes: {
|
||||||
|
onLogout: { action: 'logout' },
|
||||||
|
},
|
||||||
|
component: Layout,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
title: 'Components/Layout',
|
||||||
|
} satisfies Meta<typeof Layout>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const LoggedIn: Story = {
|
||||||
|
// @ts-expect-error onLogout is required but it is provided through argTypes
|
||||||
|
args: {
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4',
|
||||||
|
children: 'Hello World',
|
||||||
|
email: 'johndoe@gmail.com',
|
||||||
|
isLoggedIn: true,
|
||||||
|
userName: 'John Doe',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoggedOut: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Hello World',
|
||||||
|
isLoggedIn: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
16
src/components/layout/layout.tsx
Normal file
16
src/components/layout/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { Header, HeaderProps } from '@/components'
|
||||||
|
|
||||||
|
import s from './layout.module.scss'
|
||||||
|
|
||||||
|
export type LayoutProps = { children: ReactNode } & HeaderProps
|
||||||
|
|
||||||
|
export const Layout = ({ children, ...headerProps }: LayoutProps) => {
|
||||||
|
return (
|
||||||
|
<div className={s.layout}>
|
||||||
|
<Header {...headerProps} />
|
||||||
|
<div className={s.content}>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -93,23 +93,3 @@
|
|||||||
color: var(--color-accent-500);
|
color: var(--color-accent-500);
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
|
||||||
all: unset;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: var(--outline-focus);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rounded {
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import { Logout } from '@/assets'
|
||||||
|
|
||||||
import { Button } from './'
|
import { Button } from './'
|
||||||
import { Camera } from '@/assets'
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
@@ -19,10 +20,17 @@ export default meta
|
|||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Primary Button',
|
||||||
|
disabled: false,
|
||||||
|
variant: 'primary',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const PrimaryWithIcon: Story = {
|
||||||
args: {
|
args: {
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
Turn Camera On <Camera />
|
Sign out <Logout />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -46,7 +54,7 @@ export const Tertiary: Story = {
|
|||||||
}
|
}
|
||||||
export const Link: Story = {
|
export const Link: Story = {
|
||||||
args: {
|
args: {
|
||||||
children: 'Tertiary Button',
|
children: 'Link Button',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
variant: 'link',
|
variant: 'link',
|
||||||
},
|
},
|
||||||
@@ -63,7 +71,7 @@ export const FullWidth: Story = {
|
|||||||
|
|
||||||
export const AsLink: Story = {
|
export const AsLink: Story = {
|
||||||
args: {
|
args: {
|
||||||
as: 'button',
|
as: 'a',
|
||||||
children: 'Link that looks like a button',
|
children: 'Link that looks like a button',
|
||||||
href: 'https://google.com',
|
href: 'https://google.com',
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export type ButtonProps<T extends ElementType = 'button'> = {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
rounded?: boolean
|
variant?: 'link' | 'primary' | 'secondary' | 'tertiary'
|
||||||
variant?: 'icon' | 'link' | 'primary' | 'secondary' | 'tertiary'
|
|
||||||
} & ComponentPropsWithoutRef<T>
|
} & ComponentPropsWithoutRef<T>
|
||||||
|
|
||||||
const ButtonPolymorph = <T extends ElementType = 'button'>(props: ButtonProps<T>, ref: any) => {
|
const ButtonPolymorph = <T extends ElementType = 'button'>(props: ButtonProps<T>, ref: any) => {
|
||||||
@@ -32,13 +31,7 @@ const ButtonPolymorph = <T extends ElementType = 'button'>(props: ButtonProps<T>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={clsx(
|
className={clsx(s.button, s[variant], fullWidth && s.fullWidth, className)}
|
||||||
s.button,
|
|
||||||
s[variant],
|
|
||||||
fullWidth && s.fullWidth,
|
|
||||||
className,
|
|
||||||
rounded && s.rounded
|
|
||||||
)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
.arrow {
|
.arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3.75px;
|
top: -3.75px;
|
||||||
right: calc(50% - 3px);
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
|
|
||||||
width: 9px;
|
width: 9px;
|
||||||
|
|||||||
@@ -9,29 +9,22 @@ const DropdownMenu = DropdownMenuPrimitive.Root
|
|||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuContent = forwardRef<
|
const DropdownMenuContent = forwardRef<
|
||||||
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ children, className, sideOffset = 8, ...props }, ref) => (
|
>(({ align = 'end', children, className, sideOffset = 8, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
|
align={align}
|
||||||
className={clsx(s.content, className)}
|
className={clsx(s.content, className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div>
|
<DropdownMenuPrimitive.Arrow asChild>
|
||||||
<DropdownMenuPrimitive.Arrow asChild className={s.arrowBox}>
|
<div className={s.arrow} />
|
||||||
<div className={s.arrow} />
|
</DropdownMenuPrimitive.Arrow>
|
||||||
</DropdownMenuPrimitive.Arrow>
|
<div className={s.itemsBox}>{children}</div>
|
||||||
<div className={s.itemsBox}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuPrimitive.Content>
|
</DropdownMenuPrimitive.Content>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
))
|
||||||
@@ -68,11 +61,8 @@ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,31 @@ html {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-style: inherit;
|
||||||
|
color: inherit;
|
||||||
|
user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: var(--outline-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button,
|
|
||||||
select,
|
select,
|
||||||
textarea,
|
textarea,
|
||||||
optgroup,
|
optgroup,
|
||||||
|
|||||||
Reference in New Issue
Block a user