From b71e870ab509222364cf476bc67a20bcc02909dd Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 12 Nov 2024 08:40:51 -0500 Subject: [PATCH 1/2] drawer initial setup --- src/components/index.ts | 1 + src/components/ui/drawer/drawer.tsx | 47 ++++++++++++++++++++++ src/components/ui/drawer/index.ts | 2 + src/components/ui/icon/types.ts | 16 ++++---- src/stories/Drawer.stories.tsx | 18 +++++++++ style.css | 62 +++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 src/components/ui/drawer/drawer.tsx create mode 100644 src/components/ui/drawer/index.ts create mode 100644 src/stories/Drawer.stories.tsx diff --git a/src/components/index.ts b/src/components/index.ts index 2cce731..8cdfcb6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,6 +6,7 @@ export * from './ui/badge'; export * from './ui/button'; export * from './ui/card'; export * from './ui/divider'; +export * from './ui/drawer'; export * from './ui/dropdown'; export * from './ui/form/checkbox'; export * from './ui/form/form-section'; diff --git a/src/components/ui/drawer/drawer.tsx b/src/components/ui/drawer/drawer.tsx new file mode 100644 index 0000000..e7c9062 --- /dev/null +++ b/src/components/ui/drawer/drawer.tsx @@ -0,0 +1,47 @@ +import React, { ReactNode, useEffect, useState, type FC } from 'react'; +import clsx from 'clsx'; +import { Icon } from '../icon'; + +interface DrawerProps { + isOpen?: boolean + defaultOpen?: boolean + onOpenChange?: (isOpen: boolean) => void + children: ReactNode +} +export const Drawer: FC = ({ isOpen: controlledOpen, defaultOpen = false, onOpenChange, children }) => { + const [isControlled, setIsControlled] = useState(controlledOpen !== undefined); + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const isOpen = isControlled ? controlledOpen : uncontrolledOpen; + useEffect(() => { + setIsControlled(controlledOpen !== undefined); + }, [controlledOpen]); + + const handleToggle = () => { + // If controlled, call the callback + if (isControlled && onOpenChange) { + onOpenChange(!isOpen); + } else { + // If uncontrolled, update internal state + setUncontrolledOpen(!isOpen); + } + }; + return ( +
+
+
+
+ +
+
+ { + isOpen &&
{children}
+ } +
+
+ ); +}; diff --git a/src/components/ui/drawer/index.ts b/src/components/ui/drawer/index.ts new file mode 100644 index 0000000..bd440e1 --- /dev/null +++ b/src/components/ui/drawer/index.ts @@ -0,0 +1,2 @@ + +export { Drawer } from './drawer' \ No newline at end of file diff --git a/src/components/ui/icon/types.ts b/src/components/ui/icon/types.ts index a79fbb6..3302a1d 100644 --- a/src/components/ui/icon/types.ts +++ b/src/components/ui/icon/types.ts @@ -88,10 +88,10 @@ import { RiZoomInLine, RiZoomOutLine, RiRefreshLine, - RiCollapseHorizontalLine, - RiExpandHorizontalLine, - RiCollapseVerticalLine, - RiExpandVerticalLine, + // RiCollapseHorizontalLine, + // RiExpandHorizontalLine, + // RiCollapseVerticalLine, + // RiExpandVerticalLine, RiSparklingLine, RiGlobeLine, RiGlobalLine, @@ -203,10 +203,10 @@ export const ICONS = { RiZoomInLine, RiZoomOutLine, RiRefreshLine, - RiCollapseHorizontalLine, - RiExpandHorizontalLine, - RiCollapseVerticalLine, - RiExpandVerticalLine, + // RiCollapseHorizontalLine, + // RiExpandHorizontalLine, + // RiCollapseVerticalLine, + // RiExpandVerticalLine, RiSparklingLine, RiGlobeLine, RiGlobalLine, diff --git a/src/stories/Drawer.stories.tsx b/src/stories/Drawer.stories.tsx new file mode 100644 index 0000000..3f6d722 --- /dev/null +++ b/src/stories/Drawer.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Drawer } from '../components'; + +const meta = { + component: Drawer +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Example = { + args: { + children:
hello
, + isOpen: false + }, + render: (args) => +} satisfies Story; diff --git a/style.css b/style.css index 8b5bc25..e21fe30 100644 --- a/style.css +++ b/style.css @@ -972,6 +972,13 @@ html { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } +.drawer { + position: relative; + display: grid; + grid-auto-columns: max-content auto; + width: 100%; +} + .dropdown { position: relative; display: inline-block; @@ -3427,6 +3434,14 @@ input.tab:checked + .tab-content, margin-top: 100px; } +.mb-4 { + margin-bottom: 1rem; +} + +.mt-4 { + margin-top: 1rem; +} + .box-border { box-sizing: border-box; } @@ -3515,6 +3530,14 @@ input.tab:checked + .tab-content, height: 500px; } +.h-\[100vh\] { + height: 100vh; +} + +.h-full { + height: 100%; +} + .min-h-8 { min-height: 2rem; } @@ -3611,6 +3634,10 @@ input.tab:checked + .tab-content, width: 100vw; } +.w-\[50px\] { + width: 50px; +} + .min-w-8 { min-width: 2rem; } @@ -4024,6 +4051,21 @@ input.tab:checked + .tab-content, background-color: rgb(254 252 232 / var(--tw-bg-opacity)); } +.bg-red-300 { + --tw-bg-opacity: 1; + background-color: rgb(252 165 165 / var(--tw-bg-opacity)); +} + +.bg-blue-200 { + --tw-bg-opacity: 1; + background-color: rgb(191 219 254 / var(--tw-bg-opacity)); +} + +.bg-blue-300 { + --tw-bg-opacity: 1; + background-color: rgb(147 197 253 / var(--tw-bg-opacity)); +} + .bg-opacity-30 { --tw-bg-opacity: 0.3; } @@ -4203,6 +4245,10 @@ input.tab:checked + .tab-content, text-align: left; } +.align-middle { + vertical-align: middle; +} + .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -4434,6 +4480,14 @@ input.tab:checked + .tab-content, transition-duration: 150ms; } +.duration-300 { + transition-duration: 300ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + @keyframes enter { from { opacity: var(--tw-enter-opacity, 1); @@ -4448,6 +4502,14 @@ input.tab:checked + .tab-content, } } +.duration-300 { + animation-duration: 300ms; +} + +.ease-in-out { + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + .\[--tglbg\:\#2563EB\] { --tglbg: #2563EB; } From d89b92a3ab507e8f5b2cc3bf9e7f7cc4576afbe4 Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 26 Nov 2024 16:37:53 -0800 Subject: [PATCH 2/2] feat(drawer): add drawer support --- .../drawer/__snapshots__/drawer.spec.tsx.snap | 30 +++++++ src/components/ui/drawer/drawer.spec.tsx | 55 ++++++++++++ src/components/ui/drawer/drawer.tsx | 70 ++++++++-------- src/components/ui/icon/types.ts | 22 ++--- src/stories/Drawer.stories.tsx | 21 ++++- style.css | 83 +++++++++++-------- 6 files changed, 196 insertions(+), 85 deletions(-) create mode 100644 src/components/ui/drawer/__snapshots__/drawer.spec.tsx.snap create mode 100644 src/components/ui/drawer/drawer.spec.tsx diff --git a/src/components/ui/drawer/__snapshots__/drawer.spec.tsx.snap b/src/components/ui/drawer/__snapshots__/drawer.spec.tsx.snap new file mode 100644 index 0000000..255abef --- /dev/null +++ b/src/components/ui/drawer/__snapshots__/drawer.spec.tsx.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Drawer > matches snapshot 1`] = ` +
+
+
+
+
+
+
+ Drawer Content +
+
+
+
+
+
+
+`; diff --git a/src/components/ui/drawer/drawer.spec.tsx b/src/components/ui/drawer/drawer.spec.tsx new file mode 100644 index 0000000..8d007e7 --- /dev/null +++ b/src/components/ui/drawer/drawer.spec.tsx @@ -0,0 +1,55 @@ +import { expect, it, describe } from 'vitest'; +import { render } from '@testing-library/react'; + +import { Drawer } from './drawer'; +import React from 'react'; + +describe('Drawer', () => { + const subject = (props = {}) => { + const defaultProps = { + isOpen: true, + children:
Drawer Content
, + drawerWidth: 460, + side: 'right' as const + }; + return render(); + }; + + it('matches snapshot', () => { + expect(subject().container).toMatchSnapshot(); + }); + + it('renders with correct width', () => { + const { container } = subject({ drawerWidth: 500 }); + const drawer = container.firstChild as HTMLElement; + expect(drawer).toHaveStyle({ width: '500px' }); + }); + + it('positions on the right by default', () => { + const { container } = subject(); + const drawer = container.firstChild as HTMLElement; + expect(drawer).toHaveClass('right-0'); + expect(drawer).not.toHaveClass('left-0'); + }); + + it('positions on the left when specified', () => { + const { container } = subject({ side: 'left' }); + const drawer = container.firstChild as HTMLElement; + expect(drawer).toHaveClass('left-0'); + expect(drawer).not.toHaveClass('right-0'); + }); + + it('applies correct transform class when open', () => { + const { container } = subject({ isOpen: true }); + const slidePanel = container.querySelector('[class*="translate"]'); + expect(slidePanel).toHaveClass('translate-x-0'); + expect(slidePanel).not.toHaveClass('translate-x-full'); + }); + + it('applies correct transform class when closed', () => { + const { container } = subject({ isOpen: false }); + const slidePanel = container.querySelector('[class*="translate"]'); + expect(slidePanel).toHaveClass('translate-x-full'); + expect(slidePanel).not.toHaveClass('translate-x-0'); + }); +}); \ No newline at end of file diff --git a/src/components/ui/drawer/drawer.tsx b/src/components/ui/drawer/drawer.tsx index e7c9062..24e688b 100644 --- a/src/components/ui/drawer/drawer.tsx +++ b/src/components/ui/drawer/drawer.tsx @@ -1,47 +1,45 @@ -import React, { ReactNode, useEffect, useState, type FC } from 'react'; +import { ReactNode, type FC } from 'react'; import clsx from 'clsx'; -import { Icon } from '../icon'; interface DrawerProps { - isOpen?: boolean - defaultOpen?: boolean - onOpenChange?: (isOpen: boolean) => void + isOpen: boolean children: ReactNode + drawerWidth?: number + side?: 'left' | 'right' } -export const Drawer: FC = ({ isOpen: controlledOpen, defaultOpen = false, onOpenChange, children }) => { - const [isControlled, setIsControlled] = useState(controlledOpen !== undefined); - const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - const isOpen = isControlled ? controlledOpen : uncontrolledOpen; - useEffect(() => { - setIsControlled(controlledOpen !== undefined); - }, [controlledOpen]); - const handleToggle = () => { - // If controlled, call the callback - if (isControlled && onOpenChange) { - onOpenChange(!isOpen); - } else { - // If uncontrolled, update internal state - setUncontrolledOpen(!isOpen); + +export const Drawer: FC = ({ isOpen, children, drawerWidth = 460, side = 'right' }) => { + + return ( +
-
-
-
- + style={{ width: `${drawerWidth}px` }} + > +
+
+
+
{children}
+
- { - isOpen &&
{children}
- }
-
- ); + ); }; diff --git a/src/components/ui/icon/types.ts b/src/components/ui/icon/types.ts index 3302a1d..a76d9a1 100644 --- a/src/components/ui/icon/types.ts +++ b/src/components/ui/icon/types.ts @@ -88,10 +88,10 @@ import { RiZoomInLine, RiZoomOutLine, RiRefreshLine, - // RiCollapseHorizontalLine, - // RiExpandHorizontalLine, - // RiCollapseVerticalLine, - // RiExpandVerticalLine, + RiCollapseHorizontalLine, + RiExpandHorizontalLine, + RiCollapseVerticalLine, + RiExpandVerticalLine, RiSparklingLine, RiGlobeLine, RiGlobalLine, @@ -110,7 +110,8 @@ import { RiArticleLine, RiBillLine, RiCalendar2Line, - RiMailLine + RiMailLine, + RiHistoryLine, } from '@remixicon/react'; export const ICONS = { @@ -203,10 +204,10 @@ export const ICONS = { RiZoomInLine, RiZoomOutLine, RiRefreshLine, - // RiCollapseHorizontalLine, - // RiExpandHorizontalLine, - // RiCollapseVerticalLine, - // RiExpandVerticalLine, + RiCollapseHorizontalLine, + RiExpandHorizontalLine, + RiCollapseVerticalLine, + RiExpandVerticalLine, RiSparklingLine, RiGlobeLine, RiGlobalLine, @@ -225,7 +226,8 @@ export const ICONS = { RiArticleLine, RiBillLine, RiCalendar2Line, - RiMailLine + RiMailLine, + RiHistoryLine }; export enum IconSize { diff --git a/src/stories/Drawer.stories.tsx b/src/stories/Drawer.stories.tsx index 3f6d722..e8df993 100644 --- a/src/stories/Drawer.stories.tsx +++ b/src/stories/Drawer.stories.tsx @@ -8,11 +8,24 @@ const meta = { export default meta; type Story = StoryObj; - -export const Example = { +export const ControlledDrawer = { args: { children:
hello
, - isOpen: false + isOpen: false, + side: 'right', + drawerWidth: 460 }, - render: (args) => + render: (args) => ( +
+
+ the app header +
+
+
+ this is the main content +
+ +
+
+ ) } satisfies Story; diff --git a/style.css b/style.css index e21fe30..347f806 100644 --- a/style.css +++ b/style.css @@ -3434,14 +3434,6 @@ input.tab:checked + .tab-content, margin-top: 100px; } -.mb-4 { - margin-bottom: 1rem; -} - -.mt-4 { - margin-top: 1rem; -} - .box-border { box-sizing: border-box; } @@ -3470,6 +3462,10 @@ input.tab:checked + .tab-content, display: table-row; } +.grid { + display: grid; +} + .h-1\.5 { height: 0.375rem; } @@ -3530,14 +3526,14 @@ input.tab:checked + .tab-content, height: 500px; } -.h-\[100vh\] { - height: 100vh; -} - .h-full { height: 100%; } +.h-screen { + height: 100vh; +} + .min-h-8 { min-height: 2rem; } @@ -3622,6 +3618,10 @@ input.tab:checked + .tab-content, width: 400px; } +.w-\[460px\] { + width: 460px; +} + .w-\[46px\] { width: 46px; } @@ -3634,10 +3634,6 @@ input.tab:checked + .tab-content, width: 100vw; } -.w-\[50px\] { - width: 50px; -} - .min-w-8 { min-width: 2rem; } @@ -3670,6 +3666,26 @@ input.tab:checked + .tab-content, caption-side: bottom; } +.translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-full { + --tw-translate-x: 100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-0 { + --tw-translate-x: -0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-full { + --tw-translate-x: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @@ -3684,6 +3700,10 @@ input.tab:checked + .tab-content, appearance: none; } +.grid-rows-\[4rem_1fr\] { + grid-template-rows: 4rem 1fr; +} + .flex-row { flex-direction: row; } @@ -4051,21 +4071,6 @@ input.tab:checked + .tab-content, background-color: rgb(254 252 232 / var(--tw-bg-opacity)); } -.bg-red-300 { - --tw-bg-opacity: 1; - background-color: rgb(252 165 165 / var(--tw-bg-opacity)); -} - -.bg-blue-200 { - --tw-bg-opacity: 1; - background-color: rgb(191 219 254 / var(--tw-bg-opacity)); -} - -.bg-blue-300 { - --tw-bg-opacity: 1; - background-color: rgb(147 197 253 / var(--tw-bg-opacity)); -} - .bg-opacity-30 { --tw-bg-opacity: 0.3; } @@ -4245,10 +4250,6 @@ input.tab:checked + .tab-content, text-align: left; } -.align-middle { - vertical-align: middle; -} - .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -4451,6 +4452,12 @@ input.tab:checked + .tab-content, box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .shadow-none { --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; @@ -4480,6 +4487,12 @@ input.tab:checked + .tab-content, transition-duration: 150ms; } +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-300 { transition-duration: 300ms; }