Merge pull request #28175 from backstage/canon-field
Canon - Add Field + Input component
This commit is contained in:
@@ -277,3 +277,71 @@
|
||||
font-weight: var(--canon-font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
.canon-Input {
|
||||
border-radius: var(--canon-border-radius-sm);
|
||||
border: 1px solid var(--canon-border-base);
|
||||
padding: var(--canon-spacing-sm);
|
||||
background-color: var(--canon-surface-1);
|
||||
font-size: var(--canon-font-size-body);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-primary);
|
||||
width: 100%;
|
||||
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.canon-Input::placeholder {
|
||||
color: var(--canon-text-secondary);
|
||||
}
|
||||
|
||||
.canon-Input:hover {
|
||||
border-color: var(--canon-border-hover);
|
||||
}
|
||||
|
||||
.canon-Input:focus-visible {
|
||||
outline-color: var(--canon-border-selected);
|
||||
border-color: var(--canon-border-selected);
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
.canon-Input[data-invalid] {
|
||||
border-color: var(--canon-error);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-primary);
|
||||
margin-bottom: var(--canon-spacing-3xs);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-secondary);
|
||||
padding-top: var(--canon-spacing-3xs);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-error);
|
||||
padding-top: var(--canon-spacing-3xs);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -5700,7 +5700,7 @@
|
||||
--canon-border-hover: #0003;
|
||||
--canon-border-warning: #e36d05;
|
||||
--canon-border-error: #e22b2b;
|
||||
--canon-border-selected: #1db954;
|
||||
--canon-border-selected: #0006;
|
||||
--canon-error: #f50000;
|
||||
--canon-text-primary: #000;
|
||||
--canon-text-primary-on-accent: #fff;
|
||||
@@ -5747,9 +5747,10 @@
|
||||
--canon-surface-1: #121212;
|
||||
--canon-surface-2: #1a1a1a;
|
||||
--canon-border-base: #fff3;
|
||||
--canon-border-hover: #ffffff4d;
|
||||
--canon-border-warning: #f50000;
|
||||
--canon-border-error: #f87503;
|
||||
--canon-border-selected: #25d262;
|
||||
--canon-border-selected: #fff6;
|
||||
--canon-error: #f50000;
|
||||
--canon-text-primary: #fff;
|
||||
--canon-text-primary-on-accent: #000;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { Tabs } from '@/components/Tabs';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { FieldPreview } from '@/snippets/field';
|
||||
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
|
||||
|
||||
# Field
|
||||
|
||||
A wrapper around `Input` or `Select` component to add label, description and error messages..
|
||||
|
||||
<Snippet align="center" py={4} preview={<FieldPreview />} code={`<Field />`} />
|
||||
|
||||
<Tabs.Root>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab>Usage</Tabs.Tab>
|
||||
<Tabs.Tab>Theming</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel>
|
||||
<CodeBlock
|
||||
code={`import { Field } from '@backstage/canon';
|
||||
|
||||
<Field.Root>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input placeholder="Enter your name" />
|
||||
<Field.Description>Visible on your profile</Field.Description>
|
||||
<Field.Error />
|
||||
</Field.Root>`}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel>
|
||||
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
|
||||
your brand. For additional flexibility, you can use the provided class names for each element listed below.
|
||||
<CodeBlock
|
||||
code={`<Field.Root className='canon-FieldRoot'>
|
||||
<Field.Label className='canon-FieldLabel' />
|
||||
<Field.Description className='canon-FieldDescription' />
|
||||
<Field.Error className='canon-FieldError' />
|
||||
</Field.Root>`}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
|
||||
## API reference
|
||||
|
||||
<BaseUI href="https://base-ui.com/react/components/field" />
|
||||
|
||||
### Field.Root
|
||||
|
||||
Groups all parts of the field. Renders a `<div>` element.
|
||||
|
||||
<PropsTable
|
||||
data={{
|
||||
name: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
responsive: false,
|
||||
},
|
||||
invalid: {
|
||||
type: 'boolean',
|
||||
responsive: false,
|
||||
},
|
||||
validate: {
|
||||
type: '(value) => string | string[] | null | Promise',
|
||||
responsive: false,
|
||||
},
|
||||
validationMode: {
|
||||
type: ['onBlur', 'onChange'],
|
||||
responsive: false,
|
||||
},
|
||||
validationDebounceTime: {
|
||||
type: 'number',
|
||||
responsive: false,
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
style: {
|
||||
type: 'CSSProperties',
|
||||
responsive: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Field.Label
|
||||
|
||||
An accessible label that is automatically associated with the field control. Renders a `<label>` element.
|
||||
|
||||
<PropsTable
|
||||
data={{
|
||||
className: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
style: {
|
||||
type: 'CSSProperties',
|
||||
responsive: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Field.Description
|
||||
|
||||
A paragraph with additional information about the field. Renders a `<p>` element.
|
||||
|
||||
<PropsTable
|
||||
data={{
|
||||
className: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
style: {
|
||||
type: 'CSSProperties',
|
||||
responsive: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Field.Error
|
||||
|
||||
An error message displayed if the field control fails validation. Renders a `<div>` element.
|
||||
|
||||
<PropsTable
|
||||
data={{
|
||||
match: {
|
||||
type: [
|
||||
'badInput',
|
||||
'customError',
|
||||
'patternMismatch',
|
||||
'rangeOverflow',
|
||||
'rangeUnderflow',
|
||||
'stepMismatch',
|
||||
'tooLong',
|
||||
'tooShort',
|
||||
'typeMismatch',
|
||||
'valid',
|
||||
'valueMissing',
|
||||
],
|
||||
responsive: false,
|
||||
},
|
||||
forceShow: {
|
||||
type: 'boolean',
|
||||
responsive: false,
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
style: {
|
||||
type: 'CSSProperties',
|
||||
responsive: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## Examples
|
||||
|
||||
### With Label and Description
|
||||
|
||||
Here's a simple input with a label and description.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<FieldPreview />}
|
||||
code={`<Field>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Field.Description>Visible on your profile</Field.Description>
|
||||
<Input placeholder="Enter your name" />
|
||||
</Field>`}
|
||||
/>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { Tabs } from '@/components/Tabs';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { InputPreview, InputSizes } from '@/snippets/input';
|
||||
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
|
||||
|
||||
# Input
|
||||
|
||||
A input component tfor your forms.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<InputPreview />}
|
||||
code={`<Input label="Name" placeholder="Enter your name" />`}
|
||||
/>
|
||||
|
||||
<Tabs.Root>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab>Usage</Tabs.Tab>
|
||||
<Tabs.Tab>Theming</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel>
|
||||
<CodeBlock
|
||||
code={`import { Input } from '@backstage/canon';
|
||||
|
||||
<Input />
|
||||
`}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel>
|
||||
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
|
||||
your brand. For additional flexibility, you can use the provided class names for each element listed below.
|
||||
<CodeBlock
|
||||
code={`<Input className="canon-Input" />`}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
|
||||
## API reference
|
||||
|
||||
<BaseUI href="https://base-ui.com/react/components/input" />
|
||||
|
||||
<PropsTable
|
||||
data={{
|
||||
size: {
|
||||
type: ['sm', 'md'],
|
||||
responsive: false,
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
style: {
|
||||
type: 'CSSProperties',
|
||||
responsive: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
Here's a simple input with a label and description.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<InputSizes />}
|
||||
code={`<Grid>
|
||||
<Input size="sm" placeholder="Small" />
|
||||
<Input size="md" placeholder="Medium" />
|
||||
</Grid>`}
|
||||
/>
|
||||
@@ -9,6 +9,7 @@ import { ButtonPlayground } from '@/snippets/button';
|
||||
import { CheckboxPlayground } from '@/snippets/checkbox';
|
||||
import { HeadingPlayground } from '@/snippets/heading';
|
||||
import { TextPlayground } from '@/snippets/text';
|
||||
import { InputPlayground } from '@/snippets/input';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
@@ -66,6 +67,9 @@ const Content = () => {
|
||||
{selectedComponents.find(c => c === 'text') && (
|
||||
<Line content={<TextPlayground />} title="Text" />
|
||||
)}
|
||||
{/* {selectedComponents.find(c => c === 'input') && (
|
||||
<Line content={<InputPlayground />} title="Input" />
|
||||
)} */}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,11 +30,15 @@ export const PropsTable = <T extends Record<string, PropData>>({
|
||||
<Chip head>{n}</Chip>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{Array.isArray(data[n].type) ? (
|
||||
data[n].type.map(t => <Chip key={t}>{t}</Chip>)
|
||||
) : (
|
||||
<Chip>{data[n].type}</Chip>
|
||||
)}
|
||||
<div
|
||||
style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}
|
||||
>
|
||||
{Array.isArray(data[n].type) ? (
|
||||
data[n].type.map(t => <Chip key={t}>{t}</Chip>)
|
||||
) : (
|
||||
<Chip>{data[n].type}</Chip>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Chip>{data[n].responsive ? 'Yes' : 'No'}</Chip>
|
||||
|
||||
@@ -15,7 +15,8 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
fontFamily: 'var(--docs-font)',
|
||||
fontSize: '3rem',
|
||||
fontWeight: 'var(--canon-font-weight-bold)',
|
||||
margin: 0,
|
||||
marginTop: '4rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{children as ReactNode}
|
||||
@@ -36,30 +37,30 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
</Box>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<Box marginTop="xl" marginBottom="xs">
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: 'var(--docs-font)',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'var(--canon-font-weight-bold)',
|
||||
}}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</h3>
|
||||
</Box>
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: 'var(--docs-font)',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'var(--canon-font-weight-bold)',
|
||||
marginTop: '2rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<Box marginBottom="sm">
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--docs-font)',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</p>
|
||||
</Box>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--docs-font)',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5',
|
||||
marginTop: '0',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</p>
|
||||
),
|
||||
a: ({ children, href }) => (
|
||||
<a href={href} style={{ color: 'var(--canon-text-primary)' }}>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Input, Field, Grid } from '../../../packages/canon';
|
||||
|
||||
export const FieldPreview = () => {
|
||||
return (
|
||||
<div style={{ width: '300px' }}>
|
||||
<Field.Root>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input placeholder="Enter your name" />
|
||||
<Field.Description>Visible on your profile</Field.Description>
|
||||
</Field.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputPlayground = () => {
|
||||
return (
|
||||
<div style={{ maxWidth: '300px' }}>
|
||||
<Input placeholder="Enter your name" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Input, Inline, Grid } from '../../../packages/canon';
|
||||
|
||||
export const InputPreview = () => {
|
||||
return (
|
||||
<div style={{ width: '300px' }}>
|
||||
<Input placeholder="Enter your name" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputSizes = () => {
|
||||
return (
|
||||
<Grid style={{ width: '300px' }}>
|
||||
<Input size="sm" placeholder="Small" />
|
||||
<Input size="md" placeholder="Medium" />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputError = () => {
|
||||
return (
|
||||
<div style={{ width: '300px' }}>
|
||||
<Input required />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputPlayground = () => {
|
||||
return (
|
||||
<div style={{ maxWidth: '300px' }}>
|
||||
<Input placeholder="Enter your name" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -70,12 +70,17 @@ export const components: Page[] = [
|
||||
{
|
||||
title: 'Button',
|
||||
slug: 'button',
|
||||
status: 'inProgress',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Checkbox',
|
||||
slug: 'checkbox',
|
||||
status: 'inProgress',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Field',
|
||||
slug: 'field',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Heading',
|
||||
@@ -87,6 +92,11 @@ export const components: Page[] = [
|
||||
slug: 'icon',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Input',
|
||||
slug: 'input',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Table',
|
||||
slug: 'table',
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "^1.0.0-alpha.4",
|
||||
"@remixicon/react": "^4.5.0"
|
||||
"@remixicon/react": "^4.5.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Field as Field_2 } from '@base-ui-components/react/field';
|
||||
import { ForwardRefExoticComponent } from 'react';
|
||||
import { Input as Input_2 } from '@base-ui-components/react/input';
|
||||
import { default as React_2 } from 'react';
|
||||
import * as React_3 from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -180,6 +182,36 @@ export interface ContainerProps {
|
||||
// @public (undocumented)
|
||||
export type Display = 'none' | 'flex' | 'block' | 'inline';
|
||||
|
||||
// @public (undocumented)
|
||||
export const Field: {
|
||||
Root: React_2.ForwardRefExoticComponent<
|
||||
Omit<Field_2.Root.Props & React_2.RefAttributes<HTMLDivElement>, 'ref'> &
|
||||
React_2.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
Label: React_2.ForwardRefExoticComponent<
|
||||
Omit<Field_2.Label.Props & React_2.RefAttributes<any>, 'ref'> &
|
||||
React_2.RefAttributes<any>
|
||||
>;
|
||||
Description: React_2.ForwardRefExoticComponent<
|
||||
Omit<
|
||||
Field_2.Description.Props & React_2.RefAttributes<HTMLParagraphElement>,
|
||||
'ref'
|
||||
> &
|
||||
React_2.RefAttributes<HTMLParagraphElement>
|
||||
>;
|
||||
Error: React_2.ForwardRefExoticComponent<
|
||||
Omit<Field_2.Error.Props & React_2.RefAttributes<HTMLDivElement>, 'ref'> &
|
||||
React_2.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
Validity: ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: Field_2.Validity.Props & {
|
||||
className?: string | undefined;
|
||||
}) => React_2.JSX.Element;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FlexDirection = 'row' | 'column';
|
||||
|
||||
@@ -323,6 +355,17 @@ export interface InlineProps extends SpaceProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const Input: React_2.ForwardRefExoticComponent<
|
||||
InputProps & React_2.RefAttributes<HTMLInputElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface InputProps extends Omit<Input_2.Props, 'size'> {
|
||||
// (undocumented)
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type JustifyContent =
|
||||
| 'stretch'
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Field } from './Field';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Field',
|
||||
component: Field.Root,
|
||||
} satisfies Meta<typeof Field.Root>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Field.Root>
|
||||
<Field.Label>Label</Field.Label>
|
||||
<Input />
|
||||
<Field.Error>Error</Field.Error>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithLabelAndDescription: Story = {
|
||||
render: () => (
|
||||
<Field.Root>
|
||||
<Field.Label>Label</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>Description</Field.Description>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => (
|
||||
<Field.Root
|
||||
validate={value =>
|
||||
value !== 'Backstage' ? 'Please enter a different name' : null
|
||||
}
|
||||
validationMode="onChange"
|
||||
>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>
|
||||
An error will show if the value is not Backstage
|
||||
</Field.Description>
|
||||
<Field.Error match="customError">Error</Field.Error>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithValidity: Story = {
|
||||
render: () => (
|
||||
<Field.Root
|
||||
validate={value =>
|
||||
value !== 'Backstage' ? 'Please enter a different name' : null
|
||||
}
|
||||
>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>
|
||||
An error will show if the value is not Backstage
|
||||
</Field.Description>
|
||||
<Field.Validity>
|
||||
{validityState => (
|
||||
<div>{validityState.value ? 'Not Backstage' : 'Backstage'}</div>
|
||||
)}
|
||||
</Field.Validity>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.canon-FieldRoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--canon-font-regular);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-primary);
|
||||
margin-bottom: var(--canon-spacing-3xs);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-secondary);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-spacing-3xs);
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-error);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-spacing-3xs);
|
||||
}
|
||||
|
||||
.canon-FieldValidity {
|
||||
font-size: var(--canon-font-size-caption);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-secondary);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-spacing-3xs);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Field as FieldPrimitive } from '@base-ui-components/react/field';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const FieldRoot = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Root
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldRoot', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldRoot.displayName = FieldPrimitive.Root.displayName;
|
||||
|
||||
const FieldLabel = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Label
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldLabel', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldLabel.displayName = FieldPrimitive.Label.displayName;
|
||||
|
||||
const FieldDescription = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Description
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldDescription', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldDescription.displayName = FieldPrimitive.Description.displayName;
|
||||
|
||||
const FieldError = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Error>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Error>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Error
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldError', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldError.displayName = FieldPrimitive.Error.displayName;
|
||||
|
||||
const FieldValidity = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity> & {
|
||||
className?: string;
|
||||
}) => (
|
||||
<FieldPrimitive.Validity {...props}>
|
||||
{validityState => (
|
||||
<div className={clsx('canon-FieldValidity', className)}>
|
||||
{children(validityState)}
|
||||
</div>
|
||||
)}
|
||||
</FieldPrimitive.Validity>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const Field = {
|
||||
Root: FieldRoot,
|
||||
Label: FieldLabel,
|
||||
Description: FieldDescription,
|
||||
Error: FieldError,
|
||||
Validity: FieldValidity,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './Field';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Input } from './Input';
|
||||
import { Inline } from '../Inline';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Input',
|
||||
component: Input,
|
||||
} satisfies Meta<typeof Input>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <Input className="canon-Input" />,
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<Inline>
|
||||
<Input size="sm" />
|
||||
<Input size="md" />
|
||||
</Inline>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.canon-Input {
|
||||
border-radius: var(--canon-border-radius-sm);
|
||||
border: 1px solid var(--canon-border-base);
|
||||
padding: var(--canon-spacing-sm);
|
||||
background-color: var(--canon-surface-1);
|
||||
font-size: var(--canon-font-size-body);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-text-primary);
|
||||
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canon-Input::placeholder {
|
||||
color: var(--canon-text-secondary);
|
||||
}
|
||||
|
||||
.canon-Input:hover {
|
||||
border-color: var(--canon-border-hover);
|
||||
}
|
||||
|
||||
.canon-Input:focus-visible {
|
||||
outline-color: var(--canon-border-selected);
|
||||
outline-width: 0px;
|
||||
border-color: var(--canon-border-selected);
|
||||
}
|
||||
|
||||
.canon-Input[data-invalid] {
|
||||
border-color: var(--canon-error);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { ElementRef, forwardRef } from 'react';
|
||||
import { Input as InputPrimitive } from '@base-ui-components/react/input';
|
||||
import clsx from 'clsx';
|
||||
import type { InputProps } from './types';
|
||||
|
||||
/** @public */
|
||||
const Input = forwardRef<ElementRef<typeof InputPrimitive>, InputProps>(
|
||||
(props, ref) => {
|
||||
const { size = 'md', className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<InputPrimitive
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'canon-Input',
|
||||
size === 'sm' ? 'canon-Input--size-sm' : 'canon-Input--size-md',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = InputPrimitive.displayName;
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Input } from './Input';
|
||||
export type { InputProps } from './types';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input } from '@base-ui-components/react/input';
|
||||
|
||||
/** @public */
|
||||
export interface InputProps extends Omit<Input.Props, 'size'> {
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
@@ -25,3 +25,5 @@
|
||||
@import '../components/Table/styles.css';
|
||||
@import '../components/Text/styles.css';
|
||||
@import '../components/Heading/styles.css';
|
||||
@import '../components/Input/Input.styles.css';
|
||||
@import '../components/Field/Field.styles.css';
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
--canon-border-hover: rgba(0, 0, 0, 0.2);
|
||||
--canon-border-warning: #e36d05;
|
||||
--canon-border-error: #e22b2b;
|
||||
--canon-border-selected: #1db954;
|
||||
--canon-border-selected: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* States - Add more states */
|
||||
--canon-error: #f50000;
|
||||
@@ -152,9 +152,10 @@
|
||||
|
||||
/* Borders */
|
||||
--canon-border-base: rgba(255, 255, 255, 0.2);
|
||||
--canon-border-hover: rgba(255, 255, 255, 0.3);
|
||||
--canon-border-warning: #f50000;
|
||||
--canon-border-error: #f87503;
|
||||
--canon-border-selected: #25d262;
|
||||
--canon-border-selected: rgba(255, 255, 255, 0.4);
|
||||
|
||||
/* States - Add more states */
|
||||
--canon-error: #f50000;
|
||||
|
||||
@@ -37,6 +37,7 @@ export * from './components/Button';
|
||||
export * from './components/Icon';
|
||||
export * from './components/Checkbox';
|
||||
export * from './components/Table';
|
||||
|
||||
export * from './components/Input';
|
||||
export * from './components/Field';
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
Reference in New Issue
Block a user