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

Build out the <FormField /> and <TextInput /> components, and add other features in preparation for new v2 pages #1653

Merged
merged 14 commits into from
Aug 9, 2024
5 changes: 5 additions & 0 deletions .changeset/wise-buses-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@repo/ui': minor
---

Create <Card />'s subcomponents; create <FormField /> and <TextInput />; add some features re: disabled fields
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const AssetsPage = () => {
});

return (
<div className='flex flex-col gap-1'>
<>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The div was never needed, since it only had one child

{balancesByAccount?.map(account => (
<Table key={account.account} layout='fixed' title={<TableTitle account={account} />}>
<Table.Thead>
Expand Down Expand Up @@ -71,6 +71,6 @@ export const AssetsPage = () => {
</Table.Tbody>
</Table>
))}
</div>
</>
);
};
18 changes: 8 additions & 10 deletions apps/minifront/src/components/v2/dashboard-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ export const DashboardLayout = () => {

<Grid tablet={8} desktop={6} xl={4}>
<Card title={CARD_TITLE_BY_PATH[v2PathPrefix(pagePath)]}>
<div className='flex flex-col gap-4'>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper is no longer needed, since <Card /> now applies a spacing of 4 to all its direct children

<Tabs
value={v2PathPrefix(pagePath)}
onChange={value => navigate(value)}
options={TABS_OPTIONS}
actionType='accent'
/>

<Outlet />
</div>
<Tabs
value={v2PathPrefix(pagePath)}
onChange={value => navigate(value)}
options={TABS_OPTIONS}
actionType='accent'
/>

<Outlet />
</Card>
</Grid>

Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/Button/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DefaultTheme } from 'styled-components';
import { Priority, ActionType } from '../utils/button';
import { Priority } from '../utils/button';
import { ActionType } from '../utils/ActionType';

export const getBackgroundColor = (
actionType: ActionType,
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { MouseEventHandler } from 'react';
import styled, { css, DefaultTheme } from 'styled-components';
import { asTransientProps } from '../utils/asTransientProps';
import { Priority, ActionType, focusOutline, overlays, buttonBase } from '../utils/button';
import { Priority, focusOutline, overlays, buttonBase } from '../utils/button';
import { getBackgroundColor } from './helpers';
import { button } from '../utils/typography';
import { LucideIcon } from 'lucide-react';
import { Density } from '../types/Density';
import { useDensity } from '../hooks/useDensity';
import { ActionType } from '../utils/ActionType';

const iconOnlyAdornment = css<StyledButtonProps>`
border-radius: ${props => props.theme.borderRadius.full};
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ButtonGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LucideIcon } from 'lucide-react';
import { MouseEventHandler } from 'react';
import { ActionType } from '../utils/button';
import { ActionType } from '../utils/ActionType';
import { Button } from '../Button';
import styled from 'styled-components';
import { media } from '../utils/media';
Expand Down
58 changes: 56 additions & 2 deletions packages/ui/src/Card/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { Card } from '.';
import storiesBg from './storiesBg.jpg';
import styled from 'styled-components';
import { Text } from '../Text';
import { FormField } from '../FormField';
import { TextInput } from '../TextInput';
import { useState } from 'react';
import { Button } from '../Button';
import { Tabs } from '../Tabs';
import { Send } from 'lucide-react';

const BgWrapper = styled.div`
padding: ${props => props.theme.spacing(20)};
Expand Down Expand Up @@ -48,10 +54,58 @@ export const Basic: Story = {
},

render: function Render({ as, title }) {
const [tab, setTab] = useState('one');
const [textInput, setTextInput] = useState('');

return (
<Card as={as} title={title}>
<Text p>This is the card content.</Text>
<Text p>Here is some more content.</Text>
<Tabs
value={tab}
onChange={setTab}
options={[
{ label: 'One', value: 'one' },
{ label: 'Two', value: 'two' },
]}
/>

<div>
<Text p>
This is the card content. Note that each top-level item inside the card is spaced apart
with a spacing of <Text technical>4</Text>. Hence the distance between the tabs and this
paragraph, and the distance between this paragraph and the stack below.
</Text>
</div>

<Card.Stack>
<Card.Section>
<Text>
This is a <Text technical>&lt;Card.Stack /&gt;</Text> comprised of several{' '}
<Text technical>&lt;Card.Section /&gt;</Text>s. Note that the top and bottom of the
entire stack have rounded corners.
</Text>
</Card.Section>
<Card.Section>
<Text>
Card sections in a stack are useful for forms: each field of the form can be wrapped
in a <Text technical>&lt;Card.Section /&gt;</Text>.
</Text>
</Card.Section>
<Card.Section>
<FormField
label='Sample form field'
helperText="Here's an example of a form field inside a card section."
>
<TextInput
value={textInput}
onChange={setTextInput}
placeholder='Type something...'
/>
</FormField>
</Card.Section>
</Card.Stack>
<Button actionType='accent' icon={Send}>
Send
</Button>
</Card>
);
},
Expand Down
63 changes: 62 additions & 1 deletion packages/ui/src/Card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const Content = styled.div`
backdrop-filter: blur(${props => props.theme.blur.lg});
border-radius: ${props => props.theme.borderRadius.xl};
padding: ${props => props.theme.spacing(3)};

display: flex;
flex-direction: column;
gap: ${props => props.theme.spacing(4)};
`;

export interface CardProps {
Expand All @@ -30,13 +34,48 @@ export interface CardProps {
*
* @example
* ```tsx
* <Card as='section'>This is a section element with card styling</Card>
* <Card as='main'>This is a main element with card styling</Card>
* ```
*/
as?: WebTarget;
title?: ReactNode;
}

/**
* `<Card />`s are rectangular sections of a page set off from the rest of the
* page by a background and an optional title. They're useful for presenting
* data, or for wrapping a form.
*
* A `<Card />` wraps its children in a flex column with a spacing of `4`
* between each top-level HTML element. This results in a standard card layout
* no matter what its contents are.
*
* If you wish to pass children to `<Card />` that should not be spaced apart in
* that way, simply pass a single HTML element as the root of the `<Card />`'s
* children. That way, the built-in flex column will have no effect:
*
* ```tsx
* <Card title="This is the card title">
* <div>
* <span>These two elements...</span>
* <span>...will not appear in a flex column, but rather inline beside each
* other.</span>
* </div>
* </Card>
* ```
*
* You can also use `<Card.Stack />` and `<Card.Section />` to create a stack of
* sections, which are useful for wrapping individual form fields.
*
* ```tsx
* <Card title="This is the card title">
* <Card.Stack>
* <Card.Section><Text>Section one</Text></Card.Section>
* <Card.Section><Text>Section two</Text></Card.Section>
* </Card.Stack>
* </Card>
* ```
*/
export const Card = ({ children, as = 'section', title }: CardProps) => {
return (
<Root as={as}>
Expand All @@ -46,3 +85,25 @@ export const Card = ({ children, as = 'section', title }: CardProps) => {
</Root>
);
};

const StyledStack = styled.div`
display: flex;
flex-direction: column;
gap: ${props => props.theme.spacing(1)};

border-radius: ${props => props.theme.borderRadius.sm};
overflow: hidden; /** To enforce the border-radius */
`;
const Stack = ({ children }: { children?: ReactNode }) => {
return <StyledStack>{children}</StyledStack>;
};
Card.Stack = Stack;

const StyledSection = styled.div`
background-color: ${props => props.theme.color.other.tonalFill5};
padding: ${props => props.theme.spacing(3)};
`;
const Section = ({ children }: { children?: ReactNode }) => (
<StyledSection>{children}</StyledSection>
);
Card.Section = Section;
2 changes: 1 addition & 1 deletion packages/ui/src/Colors.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const Color = <T extends Exclude<TColor, 'action' | 'other' | 'base'>>({ color }
<Grid mobile={6} tablet={10}>
<Variants>
{color === 'text'
? (['primary', 'secondary', 'disabled', 'special'] as const).map(variant => (
? (['primary', 'secondary', 'muted', 'special'] as const).map(variant => (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(due to the updated theme values)

<Variant key={variant} $color={color} $colorVariant={variant}>
<Text technical>{variant}</Text>
</Variant>
Expand Down
59 changes: 59 additions & 0 deletions packages/ui/src/FormField/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FormField } from '.';
import { TextInput } from '../TextInput';
import { useState } from 'react';
import { SegmentedControl } from '../SegmentedControl';

const meta: Meta<typeof FormField> = {
component: FormField,
tags: ['autodocs', '!dev'],
argTypes: {
children: { control: false },
},
};
export default meta;

type Story = StoryObj<typeof FormField>;

export const TextInputExample: Story = {
args: {
label: "Recipient's address",
helperText: 'The recipient can find their address via the Receive tab.',
disabled: false,
},

render: function Render(props) {
const [recipient, setRecipient] = useState('');

return (
<FormField {...props}>
<TextInput value={recipient} onChange={setRecipient} placeholder='penumbra1abc123...' />
</FormField>
);
},
};

export const SegmentedControlExample: Story = {
args: {
label: 'Fee Tier',
disabled: false,
},

render: function Render(props) {
const [feeTier, setFeeTier] = useState('low');

return (
<FormField {...props}>
<SegmentedControl
value={feeTier}
options={[
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
]}
onChange={setFeeTier}
/>
</FormField>
);
},
};
73 changes: 73 additions & 0 deletions packages/ui/src/FormField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import styled from 'styled-components';
import { small, strong } from '../utils/typography';
import { ReactNode } from 'react';
import { DisabledContext } from '../utils/DisabledContext';

const Root = styled.label`
display: flex;
flex-direction: column;
gap: ${props => props.theme.spacing(2)};
position: relative;
`;

const HelperText = styled.div<{ $disabled: boolean }>`
color: ${props =>
props.$disabled ? props.theme.color.text.muted : props.theme.color.text.secondary};

${small}
`;

const LabelText = styled.div<{ $disabled: boolean }>`
${strong}

color: ${props =>
props.$disabled ? props.theme.color.text.muted : props.theme.color.text.primary};
`;

export interface FormFieldProps {
label: string;
/**
* Setting this to `true` will render the `<FormField />` as disabled, and
* will also disable whatever input component you pass as children (if that
* component uses the `useDisabled()` hook).
*
* Thus, you can simply set `disabled` on the `<FormField />`, and don't need
* to _also_ set it on the child input component.
*/
disabled?: boolean;
helperText?: string;
/**
* The form control to render for this field, whether a `<TextInput />`,
* `<SegmentedControl />`, etc.
*/
children: ReactNode;
}

/**
* A wrapper around a field in a form. Provides a standardized presentation for
* any inputs, such as `<TextInput />`, `<SegmentedControl />`, etc.
*
* ```tsx
* <FormField
* label="Field label"
* helperText="This is the helper text."
* disabled={disabled}
* >
* <TextInput value={value} onChange={onChange} />
* </FormField>
* ```
*
* Note that, in the example above, you can simply pass the `disabled` prop to
* `<FormField />`, and it will take care of disabling its child input component
* via context (assuming the child input component uses the `useDisabled()`
* hook).
*/
export const FormField = ({ label, disabled = false, helperText, children }: FormFieldProps) => (
<Root>
<LabelText $disabled={disabled}>{label}</LabelText>

<DisabledContext.Provider value={disabled}>{children}</DisabledContext.Provider>

{helperText && <HelperText $disabled={disabled}>{helperText}</HelperText>}
</Root>
);
Loading
Loading