Merge pull request #28175 from backstage/canon-field

Canon - Add Field + Input component
This commit is contained in:
Charles de Dreuille
2025-01-15 16:55:45 +00:00
committed by GitHub
25 changed files with 909 additions and 36 deletions
+68
View File
@@ -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;
}
+3 -2
View File
@@ -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>
+24 -23
View File
@@ -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)' }}>
+21
View File
@@ -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>
);
};
+34
View File
@@ -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>
);
};
+12 -2
View File
@@ -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',
+2 -1
View File
@@ -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:^",
+43
View File
@@ -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';
}
+2
View File
@@ -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';
+3 -2
View File
@@ -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;
+2 -1
View File
@@ -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';
+1
View File
@@ -3802,6 +3802,7 @@ __metadata:
"@testing-library/jest-dom": ^6.0.0
"@types/react": ^18.0.0
"@types/react-dom": ^18.0.0
clsx: ^2.1.1
eslint-plugin-storybook: ^0.11.1
globals: ^15.11.0
mini-css-extract-plugin: ^2.9.2