Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Davidrab/utima layout app #6

Merged
merged 18 commits into from
Mar 15, 2024
10,709 changes: 3,008 additions & 7,701 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"rollup-plugin-node-externals": "^7.0.1",
"rollup-plugin-typescript2": "^0.36.0",
"tailwindcss": "^3.4.1",
"turbo": "^1.12.4",
"turbo": "^1.12.5",
"typescript": "^5.3.3"
}
}
42 changes: 42 additions & 0 deletions packages/ui/src/components/layout/Layout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Menu } from 'lucide-react';

import * as Layout from '.';

// custom width sidebar a custom height header context
const meta: Meta<typeof Layout.Root> = {
component: Layout.Root,
tags: ['autodocs'],
title: 'Components/Layout',
parameters: {
layout: 'fullscreen',
},
args: {
children: (
<Layout.Root withSidebar withHeader>
<Layout.Header>
<Layout.SidebarTrigger>
<Menu />
</Layout.SidebarTrigger>
<h1>ahoj</h1>
</Layout.Header>
<Layout.SideBar>
<h1>Hi</h1>
</Layout.SideBar>
<Layout.Content>
<div className='h-screen'>Hi</div>
<div className='h-screen'>Hi</div>
<div className='h-screen'>Hi</div>
<div className='h-screen'>Hi</div>
<div className='h-screen'>Hi</div>
</Layout.Content>
</Layout.Root>
),
},
};

export default meta;

type Story = StoryObj<typeof Layout.Root>;

export const Basic: Story = {};
21 changes: 21 additions & 0 deletions packages/ui/src/components/layout/Layout.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { twOverrides } from '@/overrides';

export const layoutDef = twOverrides(
{
header:
'fixed top-0 z-50 w-full h-16 bg-primary text-primary-fg flex flex-row justify-between items-center',
root: '',
sideBar: {
wrapper:
'fixed top-0 left-0 z-40 w-64 max-w-full h-screen transition-transform -translate-x-full sm:translate-x-0',
content: 'h-full px-3 pb-4 overflow-y-auto bg-primary text-primary-fg',
},
content: {
base: '',
withSidebar: 'sm:ml-64',
withHeader: 'mt-16',
overlay: 'fixed z-30 t-0 l-0 h-full w-full bg-black/75',
},
},
'layout',
);
33 changes: 33 additions & 0 deletions packages/ui/src/components/layout/LayoutContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { memo, type ComponentPropsWithoutRef } from 'react';

import { cn } from '@/utils';

import { layoutDef } from './Layout.styles';
import { useLayoutContext } from './useLayoutContext';

export const LayoutContent = memo(function LayoutContent({
className,
...restProps
}: ComponentPropsWithoutRef<'div'>) {
const { withSidebar, withHeader, open, setOpen } = useLayoutContext();

return (
<>
{open && (
<div
onClick={() => setOpen(false)}
className={cn(layoutDef.content.overlay)}
/>
)}
<div
className={cn(
layoutDef.content.base,
withSidebar && layoutDef.content.withSidebar,
withHeader && layoutDef.content.withHeader,
className,
)}
{...restProps}
/>
</>
);
});
21 changes: 21 additions & 0 deletions packages/ui/src/components/layout/LayoutHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { memo, type ComponentPropsWithoutRef } from 'react';

import { cn } from '@/utils';

import { layoutDef } from './Layout.styles';
import { useLayoutContext } from './useLayoutContext';

export const LayoutHeader = memo(function LayoutHeader({
className,
...restProps
}: ComponentPropsWithoutRef<'header'>) {
const { headerHeight } = useLayoutContext();

return (
<header
className={cn(layoutDef.header, className)}
style={{ height: headerHeight ? `${headerHeight}px` : '64px' }}
{...restProps}
/>
);
});
41 changes: 41 additions & 0 deletions packages/ui/src/components/layout/LayoutRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type ComponentPropsWithoutRef, useMemo, useState } from 'react';

import { cn } from '@/utils';

import { layoutDef } from './Layout.styles';
import { LayoutContext, type LayoutContextType } from './useLayoutContext';

interface LayoutRootProps extends ComponentPropsWithoutRef<'div'> {
withSidebar?: boolean;
withHeader?: boolean;
headerHeight?: number;
sideBarWidth?: number;
}

export function LayoutRoot({
className,
withSidebar,
withHeader,
headerHeight,
sideBarWidth,
...restProps
}: LayoutRootProps) {
const [open, setOpen] = useState(false);
const contextValue = useMemo<LayoutContextType>(
() => ({
open,
setOpen,
withSidebar: !!(withSidebar ?? true),
withHeader: !!(withHeader ?? true),
headerHeight: headerHeight ?? 64,
sideBarWidth: sideBarWidth ?? 256,
}),
[open, withSidebar, withHeader, headerHeight, sideBarWidth],
);

return (
<LayoutContext.Provider value={contextValue}>
<div className={cn(layoutDef.root, className)} {...restProps} />
</LayoutContext.Provider>
);
}
38 changes: 38 additions & 0 deletions packages/ui/src/components/layout/LayoutSideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useContext, type ComponentPropsWithoutRef, memo } from 'react';

import { cn } from '@/utils';

import { layoutDef } from './Layout.styles';
import { LayoutContext, useLayoutContext } from './useLayoutContext';

interface LayoutSideBarProps extends ComponentPropsWithoutRef<'aside'> {
open?: boolean;
}

export const LayoutSideBar = memo(function LayoutSideBar({
children,
className,
open: forceOpen,
...restProps
}: LayoutSideBarProps) {
const { open } = useContext(LayoutContext);

const { withHeader, sideBarWidth, headerHeight } = useLayoutContext();

return (
<aside
className={cn(
layoutDef.sideBar.wrapper,
(forceOpen ?? open) && 'transform-none',
className,
)}
style={{
width: sideBarWidth ? `${sideBarWidth}px` : '256px',
paddingTop: withHeader ? `${headerHeight}px` : '0px',
}}
{...restProps}
>
<div className={cn(layoutDef.sideBar.content)}>{children}</div>
</aside>
);
});
27 changes: 27 additions & 0 deletions packages/ui/src/components/layout/LayoutSideBarTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Slot } from '@radix-ui/react-slot';
import { memo, type ComponentPropsWithoutRef } from 'react';

import { cn } from '@/utils';

import { useLayoutContext } from './useLayoutContext';

interface LayoutSideBarTriggerProps extends ComponentPropsWithoutRef<'button'> {
asChild?: boolean;
}

export const LayoutSideBarTrigger = memo(function LayoutSideBarTrigger({
className,
asChild,
...restProps
}: LayoutSideBarTriggerProps) {
const { setOpen } = useLayoutContext();
const Comp = asChild ? Slot : 'button';

return (
<Comp
className={cn('sm:hidden')}
onClick={() => setOpen(v => !v)}
{...restProps}
/>
);
});
5 changes: 5 additions & 0 deletions packages/ui/src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { LayoutRoot as Root } from './LayoutRoot';
export { LayoutSideBar as SideBar } from './LayoutSideBar';
export { LayoutHeader as Header } from './LayoutHeader';
export { LayoutContent as Content } from './LayoutContent';
export { LayoutSideBarTrigger as SidebarTrigger } from './LayoutSideBarTrigger';
34 changes: 34 additions & 0 deletions packages/ui/src/components/layout/useLayoutContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
createContext,
useContext,
type Dispatch,
type SetStateAction,
} from 'react';

export type LayoutContextType = {
withSidebar: boolean;
withHeader: boolean;
headerHeight: number;
sideBarWidth: number;
setOpen: Dispatch<SetStateAction<boolean>>;
open: boolean;
};

/**
* Holds layout specific settings and sidebar handlers,
* which are propagated to child layout components.
*/
export const LayoutContext = createContext<LayoutContextType>(undefined!);

/**
* Helper hook to access current layout state.
*/
export function useLayoutContext() {
const context = useContext(LayoutContext);

if (!context) {
throw new Error('No LayoutContext found when calling useLayoutContext');
}

return context;
}
8 changes: 8 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,11 @@ export * as Breadcrumb from './components/breadcrumb';

// Kbd
export { Kbd, type KbdProps } from './components/kbd/Kbd';

// Layout
export * as Layout from './components/layout';
export {
LayoutContext,
type LayoutContextType,
useLayoutContext,
} from './components/layout/useLayoutContext';
2 changes: 2 additions & 0 deletions packages/ui/src/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { dropdownDef } from './components/dropdown/Dropdown.styles';
import type { globalDef } from './components/global.styles';
import type { inputDef } from './components/input/Input.styles';
import type { labelDef } from './components/label/Label.styles';
import type { layoutDef } from './components/layout/Layout.styles';
import type { popoverDef } from './components/popover/Popover.styles';
import type { selectDef } from './components/select/select.styles';
import type { separatorDef } from './components/separator/Separator.styles';
Expand Down Expand Up @@ -45,6 +46,7 @@ type ComponentOverridesDef = {
checkbox: typeof checkboxDef;
command: typeof commandDef;
dialog: typeof dialogDef;
layout: typeof layoutDef;
breadcrumb: typeof breadcrumbDef;
};

Expand Down
Loading