Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions frontend/documentation/components/Field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import Field from 'components/base/forms/Field'
import Input from 'components/base/forms/Input'

const meta: Meta<typeof Field> = {
component: Field,
parameters: { layout: 'padded' },
title: 'Components/Forms/Field',
}
export default meta

type Story = StoryObj<typeof Field>

export const Default: Story = {
render: () => (
<Field label='Email' htmlFor='field-default'>
<Input id='field-default' placeholder='you@example.com' />
</Field>
),
}

export const WithError: Story = {
render: () => (
<Field label='Email' htmlFor='field-error' error='Enter a valid email.'>
<Input id='field-error' value='nope' isValid={false} autoValidate />
</Field>
),
}

export const RequiredWithTooltip: Story = {
render: () => (
<Field
label='Email'
htmlFor='field-required'
required
tooltip='We never share your email.'
>
<Input id='field-required' placeholder='you@example.com' />
</Field>
),
}

// Field wraps any control, not just Input (this replaces InputGroup's
// `component=`).
export const CustomControl: Story = {
render: () => (
<Field label='Notes' htmlFor='field-custom'>
<textarea
id='field-custom'
className='input full-width'
rows={3}
placeholder='Any control goes here'
/>
</Field>
),
}
29 changes: 29 additions & 0 deletions frontend/documentation/components/FieldError.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldError from 'components/base/forms/FieldError'

const meta: Meta<typeof FieldError> = {
component: FieldError,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldError',
}
export default meta

type Story = StoryObj<typeof FieldError>

export const Default: Story = {
render: () => <FieldError error='This field is required.' />,
}

export const RichMessage: Story = {
render: () => (
<FieldError
error={
<>
Must be a valid <strong>email address</strong>.
</>
}
/>
),
}
33 changes: 33 additions & 0 deletions frontend/documentation/components/FieldLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldLabel from 'components/base/forms/FieldLabel'

const meta: Meta<typeof FieldLabel> = {
component: FieldLabel,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldLabel',
}
export default meta

type Story = StoryObj<typeof FieldLabel>

export const Default: Story = {
render: () => <FieldLabel htmlFor='email'>Email</FieldLabel>,
}

export const Required: Story = {
render: () => (
<FieldLabel htmlFor='email' required>
Email
</FieldLabel>
),
}

export const WithTooltip: Story = {
render: () => (
<FieldLabel htmlFor='email' tooltip='We never share your email.'>
Email
</FieldLabel>
),
}
79 changes: 79 additions & 0 deletions frontend/documentation/components/InputField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import InputField from 'components/base/forms/InputField'

const meta: Meta<typeof InputField> = {
component: InputField,
parameters: { layout: 'padded' },
title: 'Components/Forms/InputField',
}
export default meta

type Story = StoryObj<typeof InputField>

const Interactive = () => {
const [value, setValue] = useState('')
return (
<InputField
label='Email'
placeholder='you@example.com'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}

export const Default: Story = {
render: () => <Interactive />,
}

export const Required: Story = {
render: () => (
<InputField label='Email' required placeholder='you@example.com' />
),
}

export const WithError: Story = {
render: () => (
<InputField
label='Email'
value='not-an-email'
error='Enter a valid email address.'
/>
),
}

export const Disabled: Story = {
render: () => <InputField label='Email' disabled value='you@example.com' />,
}

export const Sizes: Story = {
render: () => (
<div className='d-flex flex-column gap-3'>
<InputField label='Default' placeholder='Default' />
<InputField label='Small' size='small' placeholder='Small' />
<InputField label='Extra small' size='xSmall' placeholder='Extra small' />
</div>
),
}

export const ReadOnly: Story = {
render: () => (
<InputField label='Environment key' readOnly value='ser.abc123' />
),
}

export const WithoutLabel: Story = {
render: () => <InputField placeholder='No label' />,
}

export const WithTooltip: Story = {
render: () => (
<InputField
label='Email'
tooltip='We never share your email.'
placeholder='you@example.com'
/>
),
}
42 changes: 42 additions & 0 deletions frontend/documentation/components/PasswordInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import PasswordInput from 'components/base/forms/PasswordInput'

const meta: Meta<typeof PasswordInput> = {
component: PasswordInput,
parameters: { layout: 'padded' },
title: 'Components/Forms/PasswordInput',
}
export default meta

type Story = StoryObj<typeof PasswordInput>

const Interactive = () => {
const [value, setValue] = useState('hunter2')
return (
<PasswordInput
placeholder='Password'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}

export const Default: Story = {
render: () => <Interactive />,
}

export const Disabled: Story = {
render: () => <PasswordInput value='hunter2' disabled />,
}

export const Sizes: Story = {
render: () => (
<div className='d-flex flex-column gap-3'>
<PasswordInput value='hunter2' placeholder='Default' />
<PasswordInput value='hunter2' size='small' placeholder='Small' />
<PasswordInput value='hunter2' size='xSmall' placeholder='Extra small' />
</div>
),
}
44 changes: 44 additions & 0 deletions frontend/documentation/components/SearchInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import SearchInput from 'components/base/forms/SearchInput'

const meta: Meta<typeof SearchInput> = {
component: SearchInput,
parameters: { layout: 'padded' },
title: 'Components/Forms/SearchInput',
}
export default meta

type Story = StoryObj<typeof SearchInput>

const Interactive = () => {
const [value, setValue] = useState('')
return (
<SearchInput
placeholder='Search…'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}

export const Default: Story = {
render: () => <Interactive />,
}

export const Sizes: Story = {
render: () => (
<div className='d-flex flex-column gap-3'>
<SearchInput placeholder='Default' />
<SearchInput size='small' placeholder='Small' />
<SearchInput size='xSmall' placeholder='Extra small' />
</div>
),
}

export const Disabled: Story = {
render: () => (
<SearchInput value='read-only' disabled placeholder='Search…' />
),
}
58 changes: 58 additions & 0 deletions frontend/documentation/components/TextAreaField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import TextAreaField from 'components/base/forms/TextAreaField'

const meta: Meta<typeof TextAreaField> = {
component: TextAreaField,
parameters: { layout: 'padded' },
title: 'Components/Forms/TextAreaField',
}
export default meta

type Story = StoryObj<typeof TextAreaField>

const Interactive = () => {
const [value, setValue] = useState('')
return (
<TextAreaField
label='Description'
rows={3}
placeholder='Describe…'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}

export const Default: Story = {
render: () => <Interactive />,
}

export const Required: Story = {
render: () => (
<TextAreaField
label='Description'
required
rows={3}
placeholder='Describe…'
/>
),
}

export const WithError: Story = {
render: () => (
<TextAreaField
label='Description'
rows={3}
value='Too short'
error='Please add more detail.'
/>
),
}

export const Disabled: Story = {
render: () => (
<TextAreaField label='Description' rows={3} disabled value='Read only' />
),
}
8 changes: 3 additions & 5 deletions frontend/web/components/BreadcrumbSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'
import { IonIcon } from '@ionic/react'
import { checkmarkCircle, chevronDown, chevronUp } from 'ionicons/icons'
import InlineModal from './InlineModal'
import Input from './base/forms/Input'
import SearchInput from './base/forms/SearchInput'
import {
Environment,
Organisation,
Expand Down Expand Up @@ -332,13 +332,12 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
onMouseEnter={() => setHoveredSection('organisation')}
style={{ maxWidth: 'calc(50vw - 10px)', width: 260 }}
>
<Input
<SearchInput
autoFocus={focus === 'organisation'}
onKeyDown={(e) => navigateOrganisations(e, organisations)}
onChange={(e) => {
setOrganisationSearch(Utils.safeParseEventValue(e))
}}
search
inputClassName='border-0 bg-transparent border-bottom-1'
size='xSmall'
className='full-width'
Expand Down Expand Up @@ -387,13 +386,12 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
'border-left-1',
)}
>
<Input
<SearchInput
onChange={(e) => {
setProjectSearch(Utils.safeParseEventValue(e))
}}
autoFocus={focus === 'project'}
onKeyDown={(e) => navigateProjects(e)}
search
className='full-width'
inputClassName='border-0 bg-transparent border-bottom-1'
size='xSmall'
Expand Down
Loading
Loading