Skip to content

Commit

Permalink
feat: approver pattern component
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Mar 5, 2024
1 parent 769ac8f commit 6a2a9ff
Show file tree
Hide file tree
Showing 20 changed files with 700 additions and 170 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
"ecdsa-sig-formatter": "1.0.11",
"ecpair": "2.1.0",
"formik": "2.4.5",
"framer-motion": "11.0.8",
"jotai": "2.2.1",
"jotai-redux": "0.2.1",
"jsontokens": "4.0.1",
Expand Down Expand Up @@ -224,7 +225,7 @@
"react-intersection-observer": "9.5.2",
"react-lottie": "1.2.4",
"react-redux": "8.1.3",
"react-router-dom": "6.22.1",
"react-router-dom": "6.22.2",
"react-virtuoso": "4.7.1",
"redux-persist": "6.0.0",
"rxjs": "7.8.1",
Expand All @@ -246,7 +247,7 @@
"@leather-wallet/prettier-config": "0.0.1",
"@ls-lint/ls-lint": "2.2.2",
"@mdx-js/loader": "3.0.0",
"@pandacss/dev": "0.32.0",
"@pandacss/dev": "0.33.0",
"@playwright/test": "1.40.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@redux-devtools/cli": "4.0.0",
Expand Down Expand Up @@ -293,7 +294,7 @@
"bip32": "4.0.0",
"blns": "2.0.4",
"browserslist": "4.23.0",
"chromatic": "10.9.6",
"chromatic": "11.0.0",
"chrome-webstore-upload-cli": "2.2.2",
"clean-webpack-plugin": "4.0.0",
"concurrently": "8.2.2",
Expand All @@ -316,7 +317,7 @@
"html-webpack-plugin": "5.6.0",
"jsdom": "22.1.0",
"postcss": "8.4.35",
"postcss-loader": "8.1.0",
"postcss-loader": "8.1.1",
"prettier": "3.2.5",
"process": "0.11.10",
"progress-bar-webpack-plugin": "2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/common/has-children.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode } from 'react';

export interface HasChildren {
children: ReactNode;
children?: ReactNode;
}
22 changes: 22 additions & 0 deletions src/app/common/hooks/use-element-height-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect } from 'react';

export function useElementHeightListener(
ref: React.RefObject<HTMLDivElement>,
listener: (height: number) => void
) {
useEffect(() => {
if (!ref.current) return;

const resizeObserver = new ResizeObserver(entries => {
const observedHeight = entries[0].contentRect.height;
listener(observedHeight);
});

resizeObserver.observe(ref.current);

return () => {
// Cleanup the observer when the component is unmounted
resizeObserver.disconnect();
};
}, [listener, ref]);
}
19 changes: 19 additions & 0 deletions src/app/common/hooks/use-register-children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from 'react';

export function useRegisterChildren<T extends string>() {
const [children, setChildren] = useState<Record<T, number>>({} as Record<T, number>);

function registerChild(child: T) {
setChildren(children => ({ ...children, [child]: (children[child] || 0) + 1 }));
}

function deregisterChild(child: T) {
setChildren(children => ({ ...children, [child]: (children[child] || 0) - 1 }));
}

function hasChild(child: T) {
return children[child] > 0;
}

return { children: Object.keys(children) as T[], registerChild, deregisterChild, hasChild };
}
101 changes: 101 additions & 0 deletions src/app/features/approver/approver-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Box, Circle, styled } from 'leather-styles/jsx';

import { Button } from '@app/ui/components/button/button';
import { Callout } from '@app/ui/components/callout/callout';
import { Flag } from '@app/ui/components/flag/flag';
import { ItemInteractive } from '@app/ui/components/item/item-interactive';
import { ItemLayout } from '@app/ui/components/item/item.layout';

import { Approver } from './approver';

export function ApproverDemo() {
return (
<styled.div minH="100vh">
<Approver>
<Approver.Header title="Some prompt that breaks two lines" requester="gamma.io" />
<Callout title="Some callout">Hey watch out for this sketchy app</Callout>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />} align="top">
<Box width="90%" height="16px" backgroundColor="ink.border-default" />
<Box width="75%" height="12px" backgroundColor="ink.border-default" mt="space.02" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 2</Approver.Subheader>
<ItemLayout
titleLeft="Example"
titleRight="Example"
captionLeft="Example"
captionRight="Example"
flagImg={<Circle size="40px" backgroundColor="ink.border-default" />}
/>
</Approver.Section>
<Approver.Advanced>
<Approver.Section>
Section 3
<ItemInteractive onClick={() => {}} mt="space.03">
<ItemLayout
captionLeft="Example"
captionRight="Example"
flagImg={<Circle size="40px" backgroundColor="ink.border-default" />}
titleLeft="Example"
titleRight="Example"
/>
</ItemInteractive>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Demo section 1</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
</Approver.Advanced>
<Approver.Actions
actions={
<>
<Button variant="outline">Cancel</Button>
<Button>Approve</Button>
</>
}
/>
</Approver>
</styled.div>
);
}
21 changes: 21 additions & 0 deletions src/app/features/approver/approver.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext } from 'react';

export type ApproverChildren = 'header' | 'actions' | 'advanced' | 'section' | 'subheader';

interface ApproverContext {
isDisplayingAdvancedView: boolean;
setIsDisplayingAdvancedView(val: boolean): void;
registerChild(child: ApproverChildren): void;
deregisterChild(child: ApproverChildren): void;
hasChild(child: ApproverChildren): boolean;
}

const approverContext = createContext<ApproverContext | null>(null);

export const ApproverProvider = approverContext.Provider;

export function useApproverContext() {
const context = useContext(approverContext);
if (!context) throw new Error('`useApproverContext` must be used within a `ApproverProvider`');
return context;
}
116 changes: 116 additions & 0 deletions src/app/features/approver/approver.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Meta, StoryObj } from '@storybook/react';
import { Box, Circle, Flex } from 'leather-styles/jsx';

import { Button } from '@app/ui/components/button/button';
import { Callout } from '@app/ui/components/callout/callout';
import { Flag } from '@app/ui/components/flag/flag';
import { ItemInteractive } from '@app/ui/components/item/item-interactive';
import { ItemLayout } from '@app/ui/components/item/item.layout';

import { Approver } from './approver';

const meta: Meta<typeof Approver> = {
component: Approver,
tags: ['autodocs'],
title: 'Feature/Approver',

render: ({ children, ...args }) => (
<Flex maxW="390px" h="680px" border="1px solid lightgrey" overflowY="auto">
<Approver {...args}>{children}</Approver>
</Flex>
),
};

export default meta;

type Story = StoryObj<typeof Approver>;

export const ExampleOne: Story = {
args: {
children: (
<>
<Approver.Header title="Some prompt that breaks two lines" requester="gamma.io" />
<Callout title="Some callout">Hey watch out for this sketchy app</Callout>
<Approver.Section>
<Approver.Subheader>Example flag content</Approver.Subheader>
<Flag img={<Circle size="40px" backgroundColor="ink.border-default" />} align="top">
<Box width="90%" height="16px" backgroundColor="ink.border-default" />
<Box width="75%" height="12px" backgroundColor="ink.border-default" mt="space.02" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Example rich content with avatar</Approver.Subheader>
<ItemLayout
titleLeft="Michelly token"
titleRight="100 MICA"
captionLeft="SIP-10"
captionRight="$894,891"
flagImg={<Circle size="40px" backgroundColor="ink.border-default" />}
/>
</Approver.Section>
<Approver.Advanced>
<Approver.Section>
<Approver.Subheader>In the advanced section</Approver.Subheader>
<ItemInteractive onClick={() => {}} mt="space.03" mb="space.03">
<ItemLayout
titleLeft="Pressable"
titleRight="Mr. Clicky"
captionLeft="Interactive item"
captionRight="Click me"
flagImg={<Circle size="40px" backgroundColor="ink.border-default" />}
/>
</ItemInteractive>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Inputs & Outputs</Approver.Subheader>
<Flag align="top" img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Transaction raw data</Approver.Subheader>
<Flag align="top" img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
<Approver.Section>
<Approver.Subheader>Additional info</Approver.Subheader>
<Flag align="top" img={<Circle size="40px" backgroundColor="ink.border-default" />}>
<Box width="100%" height="20px" backgroundColor="ink.border-default" />
</Flag>
</Approver.Section>
</Approver.Advanced>

<Approver.Actions
actions={
<>
<Button variant="outline">Cancel</Button>
<Button>Approve</Button>
</>
}
/>
</>
),
},
};

export const ActionsAlignToBottom: Story = {
args: {
children: (
<>
<Approver.Header
title="Action align to bottom of page"
requester="even when there's no content to push it there"
/>
<Approver.Actions
actions={
<>
<Button variant="outline">Cancel</Button>
<Button>Approve</Button>
</>
}
/>
</>
),
},
};
58 changes: 58 additions & 0 deletions src/app/features/approver/approver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from 'react';

import { css } from 'leather-styles/css';
import { Flex, styled } from 'leather-styles/jsx';

import type { HasChildren } from '@app/common/has-children';
import { useRegisterChildren } from '@app/common/hooks/use-register-children';

import { type ApproverChildren, ApproverProvider } from './approver.context';
import { ApproverActions } from './components/approver-actions';
import { ApproverAdvanced } from './components/approver-advanced';
import { ApproverHeader } from './components/approver-header';
import { ApproverSection } from './components/approver-section';
import { ApproverSubheader } from './components/approver-subheader';

const applyMarginsToLastApproverSection = css({
'& .approver-section:last-child': { mb: 'space.03' },
});

function Approver({ children }: HasChildren) {
const { registerChild, deregisterChild, hasChild } = useRegisterChildren<ApproverChildren>();
const [isDisplayingAdvancedView, setIsDisplayingAdvancedView] = useState(false);

return (
<ApproverProvider
value={{
hasChild,
registerChild,
deregisterChild,
isDisplayingAdvancedView,
setIsDisplayingAdvancedView,
}}
>
<styled.main
display="flex"
flexDir="column"
pos="relative"
minH="100%"
maxW="640px"
mx="auto"
className={applyMarginsToLastApproverSection}
alignItems="center"
>
<Flex flexDir="column" flex={1} background="ink.background-secondary">
{children}
</Flex>
</styled.main>
</ApproverProvider>
);
}

Approver.Header = ApproverHeader;
Approver.Subheader = ApproverSubheader;
Approver.Section = ApproverSection;
Approver.Advanced = ApproverAdvanced;
Approver.Actions = ApproverActions;

export { Approver };
Loading

0 comments on commit 6a2a9ff

Please sign in to comment.