This guide will give you an overview to the organisation of the Carbon codebase, its conventions and other tools used.
Our components should follow a consistent file structure as shown below (note the component names here are arbitrary).
src/components/textbox/
|-- textbox.component.(ts|tsx)
|-- textbox.style.(ts|tsx)
|-- textbox.spec.(ts|tsx)
|-- textbox.stories.(ts|tsx)
|-- textbox.stories.mdx
|-- textbox.pw.(ts|tsx)
|-- components.pw.(ts|tsx)
|-- index.ts
|-- sub-component/
|-- ...
|-- __internal__/
|-- ...
Any sub-component that will not be exported directly from Carbon should go in the __internal__/
folder. Sub-components should have the same file structure as root directory of the component.
The root directory of the component should also have a index.ts
file which exports the component, non-internal sub-components and their prop interfaces.
All component prop interfaces (in <component-name>.component.(ts|tsx)
files) should follow this template:
// in textbox.component.tsx
import React from "react";
export interface TextboxProps {
/** Example required prop description */
exampleRequiredProp: string;
/** Example optional prop description */
exampleOptionalProp?: string;
}
Textbox.displayName = "Textbox";
export default Textbox;
Exports in the index.ts
file should follow this template:
// If component is sole export of *.component file, export it as default
export { default } from "./textbox.component";
export { type TextboxProps } from "./textbox.component";
// If component should come with subcomponents - export all as named exports
export { default as Select } from "./select.component";
export { type SelectProps } from "./select.component";
export { default as Option } from "./option/option.component";
export { type OptionProps } from "./option/option.component";
If you find any inconsistencies in module structure you are not obligated to change it. For example, if you need to work on that component to fix an unrelated bug. However, we are committed to provide a consistent interface for our components and will make changes where appropriate. Any changes we do make will be appropriately semantically versioned as well.
We use Storybook for our documentation - which uses example "stories" to demo how our components can be used with certain props or how a particular layout can be achieved. Alongside demoing component behaviour, we also use stories for automated visual testing using Chromatic, so we require stories to be written for all our components.
If you are new to Storybook, we recommend first reading their official docs, particularly this section on how to write stories, so you are familiar with the following patterns.
New stories should be written in TypeScript as a <component>.stories.tsx
file to allow for type checking:
// in button.stories.tsx
import React from "react";
import { Meta, StoryObj } from "@storybook/react";
import Button from ".";
const meta: Meta<typeof Button> = {
title: "Button",
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const PrimaryButton: Story = {
args: {
variant: "primary",
children: "Click me!",
},
};
export const SecondaryButton: Story = {
args: {
variant: "secondary",
children: "Click me!",
},
};
For creating the component docs pages you see on carbon.sage.com, these should be written separately in the MDX format as a <component>.mdx
file. This allows us to decouple our documentation from our story code. We then import our previously written TypeScript stories from <component>.stories.tsx
into this new file, which Storybook uses to build our official docs for the component.
This would typically look like the following:
/* in button.mdx... */
import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
import Button from ".";
import * as ButtonStories from "./button.stories.tsx";
<Meta of={ButtonStories} />
# Button
An accessible button which triggers an action on click.
## Examples
### Primary button
<Canvas>
<Story name="primary button" story={stories.PrimaryButton} />
</Canvas>
### Secondary button
This is an example of a secondary `Button` which has slightly different styling:
<Canvas>
<Story name="secondary button" story={stories.SecondaryButton} />
</Canvas>
## Props
<ArgTypes of={ButtonStories} />
We use ESLint to detect errors and potential problems in our TypeScript code. ESLint is designed to allow the creation of custom linting rules and Carbon's configuration extends the Airbnb TypeScript Style Guide, rules recommended for React and a few of our own custom configurations.
ESLint has been configured to be automatically triggered on your code whenever git commit
to prevent the need to run manually. If required however, you can run ESLint manually with:
npm run lint
Our linting rules are defined in .eslintrc at the root of the project, these rules do change from time-to-time so please check this file and cross reference it against the ESLint Rules.