Improve docs

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-15 16:34:53 +00:00
parent c62c91d35a
commit ec62f506b2
16 changed files with 358 additions and 129 deletions
+29 -25
View File
@@ -278,21 +278,7 @@
}
}
.canon-input-root {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-input-label {
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-input-control {
.canon-Input {
border-radius: var(--canon-border-radius-sm);
border: 1px solid var(--canon-border-base);
padding: var(--canon-spacing-sm);
@@ -300,44 +286,62 @@
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-control::placeholder {
.canon-Input::placeholder {
color: var(--canon-text-secondary);
}
.canon-input-control:hover {
.canon-Input:hover {
border-color: var(--canon-border-hover);
}
.canon-input-control:focus-visible {
.canon-Input:focus-visible {
outline-color: var(--canon-border-selected);
border-color: var(--canon-border-selected);
outline-width: 0;
}
.canon-input-control[data-invalid] {
.canon-Input[data-invalid] {
border-color: var(--canon-error);
}
.canon-input-control-sm {
.canon-Input--size-sm {
height: 2rem;
}
.canon-input-control-md {
.canon-Input--size-md {
height: 2.5rem;
}
.canon-input-description {
.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);
margin-top: var(--canon-spacing-3xs);
padding-top: var(--canon-spacing-3xs);
margin: 0;
}
.canon-input-error {
.canon-FieldError {
font-size: var(--canon-font-size-caption);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-error);
margin-top: var(--canon-spacing-3xs);
padding-top: var(--canon-spacing-3xs);
margin: 0;
}
@@ -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>`}
/>
@@ -2,11 +2,7 @@ import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
InputPreview,
InputLabelAndDescription,
InputError,
} from '@/snippets/input';
import { InputPreview, InputSizes } from '@/snippets/input';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
# Input
@@ -37,57 +33,21 @@ A input component tfor your forms.
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-root" />`}
code={`<Input className="canon-Input" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/field" />
<BaseUI href="https://base-ui.com/react/components/input" />
<PropsTable
data={{
label: {
type: 'string',
responsive: false,
},
description: {
type: 'string',
responsive: false,
},
size: {
type: ['sm', 'md'],
responsive: false,
},
placeholder: {
type: 'string',
responsive: false,
},
match: {
type: [
'badInput',
'customError',
'patternMismatch',
'rangeOverflow',
'rangeUnderflow',
'stepMismatch',
'tooLong',
'tooShort',
'typeMismatch',
'valid',
'valueMissing',
],
responsive: false,
},
error: {
type: 'string',
responsive: false,
},
required: {
type: 'boolean',
responsive: false,
},
className: {
type: 'string',
responsive: false,
@@ -101,7 +61,7 @@ A input component tfor your forms.
## Examples
### With label and description
### Sizes
Here's a simple input with a label and description.
@@ -109,18 +69,9 @@ Here's a simple input with a label and description.
align="center"
py={4}
open
preview={<InputLabelAndDescription />}
code={`<Input label="Name" description="Visible on your profile" />`}
/>
### Error
Here's a simple input with an error message.
<Snippet
align="center"
py={4}
open
preview={<InputError />}
code={`<Input label="Name" required invalid />`}
preview={<InputSizes />}
code={`<Grid>
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" />
</Grid>`}
/>
@@ -67,9 +67,9 @@ const Content = () => {
{selectedComponents.find(c => c === 'text') && (
<Line content={<TextPlayground />} title="Text" />
)}
{selectedComponents.find(c => c === 'input') && (
{/* {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>
);
};
+9 -13
View File
@@ -1,30 +1,26 @@
import { Input } from '@backstage/canon';
import { Input, Inline, Grid } from '../../../packages/canon';
export const InputPreview = () => {
return (
<div style={{ width: '300px' }}>
<Input label="Name" placeholder="Enter your name" />
<Input placeholder="Enter your name" />
</div>
);
};
export const InputLabelAndDescription = () => {
export const InputSizes = () => {
return (
<div style={{ width: '300px' }}>
<Input label="Name" description="Visible on your profile" />
</div>
<Grid style={{ width: '300px' }}>
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" />
</Grid>
);
};
export const InputError = () => {
return (
<div style={{ width: '300px' }}>
<Input
label="Name"
errorMessage="Please enter your name"
required
invalid
/>
<Input required />
</div>
);
};
@@ -32,7 +28,7 @@ export const InputError = () => {
export const InputPlayground = () => {
return (
<div style={{ maxWidth: '300px' }}>
<Input label="Name" description="Visible on your profile" />
<Input placeholder="Enter your name" />
</div>
);
};
+5
View File
@@ -77,6 +77,11 @@ export const components: Page[] = [
slug: 'checkbox',
status: 'alpha',
},
{
title: 'Field',
slug: 'field',
status: 'alpha',
},
{
title: 'Heading',
slug: 'heading',
+32
View File
@@ -6,6 +6,7 @@
/// <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';
@@ -181,6 +182,37 @@ export interface ContainerProps {
// @public (undocumented)
export type Display = 'none' | 'flex' | 'block' | 'inline';
// Warning: (ae-missing-release-tag) "Field" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @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,
...props
}: React_2.ComponentPropsWithoutRef<
typeof Field_2.Validity
>) => React_2.JSX.Element;
};
// @public (undocumented)
export type FlexDirection = 'row' | 'column';
@@ -64,3 +64,24 @@ export const WithError: Story = {
</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>
),
};
@@ -43,3 +43,11 @@
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);
}
@@ -68,10 +68,17 @@ FieldError.displayName = FieldPrimitive.Error.displayName;
const FieldValidity = ({
children,
className,
...props
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity>) => (
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity> & {
className?: string;
}) => (
<FieldPrimitive.Validity {...props}>
{validityState => children(validityState)}
{validityState => (
<div className={clsx('canon-FieldValidity', className)}>
{children(validityState)}
</div>
)}
</FieldPrimitive.Validity>
);
@@ -23,6 +23,7 @@
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 {
@@ -14,6 +14,8 @@
* 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';
+1 -1
View File
@@ -38,6 +38,6 @@ export * from './components/Icon';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';
export * from './components/Field';
// Types
export * from './types';