mit einem Klick
creating-components
// Use when creating new UI components in packages/ui. Covers component structure, tests, stories, and what to avoid.
// Use when creating new UI components in packages/ui. Covers component structure, tests, stories, and what to avoid.
Use when writing or editing documentation in packages/docs. Covers Gitbook markdown syntax, special blocks, page structure, and the SUMMARY.md table of contents. Trigger phrases include "write docs", "add documentation", "docs page", "gitbook", "user manual".
Use when adding a new domain to Nuclear's plugin system, or implementing a host. Covers the host pattern (how player functionality is exposed to plugins), the host interface and API class structure, how hosts are implemented in the player, error handling conventions, and what files to create and modify. Trigger phrases include "add a domain", "new domain", "host implementation", "host pattern", "createPluginAPI".
Use when writing, scaffolding, or modifying Nuclear plugins. Covers plugin structure, manifest, entry point, provider types, available APIs, and publishing. Trigger phrases include "create a plugin", "write a plugin", "plugin scaffold", "streaming provider", "metadata provider".
| name | creating-components |
| description | Use when creating new UI components in packages/ui. Covers component structure, tests, stories, and what to avoid. |
packages/ui/src/components/MyComponent/
├── MyComponent.tsx # Implementation
├── MyComponent.test.tsx # Tests
├── index.ts # Re-exports
└── __snapshots__/ # Vitest snapshots (auto-generated)
After creating, export from packages/ui/src/components/index.ts.
import { cva, VariantProps } from 'class-variance-authority';
import { ComponentProps, FC } from 'react';
import { cn } from '../../utils';
const variants = cva('base-classes', {
variants: { /* ... */ },
defaultVariants: { /* ... */ },
});
type MyComponentProps = ComponentProps<'div'> & VariantProps<typeof variants>;
export const MyComponent: FC<MyComponentProps> = ({
className,
variant,
...props
}) => (
<div className={cn(variants({ variant, className }))} {...props} />
);
What to test:
What NOT to test:
Consolidate tests. One test can cover multiple related assertions. An exception to that is snapshot tests - one snapshot per variant/state.
Create packages/storybook/src/MyComponent.stories.tsx.
One story can show multiple related variants. For example, it's wasteful to create a story for each variant of a button. Put them all together in one place. Don't create separate stories for each variant:
import { Meta, StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import { MyComponent } from '@nuclearplayer/ui';
const meta = {
title: 'Components/MyComponent',
component: MyComponent,
tags: ['autodocs'],
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const AllVariants: Story = {
render: () => {
const [state, setState] = useState(initialState);
return (
<div className="flex flex-col gap-4">
{/* Show all variants, states, interactions */}
</div>
);
},
};
This isn't an iron rule, sometimes it will make more sense to have separate stories.
Don't run storybook build checks.
Before creating a new component, check if an existing one can be extended.
Pattern: Discriminated unions for mode variants
Example: Instead of creating SingleSelect and MultiSelect components:
type Props =
| { multiple?: false; selected: string; onChange: (id: string) => void }
| { multiple: true; selected: string[]; onChange: (ids: string[]) => void };
TypeScript enforces correct types based on the multiple prop.
Where it's likely that a component will need custom styling, expose a className prop.
If there are many parts that may need styling, consider exposing a classes prop with specific class names for each part. Define a type for the classes prop. Refer to packages/ui/src/components/TrackTable/types.ts for an example.
All user-facing strings go through i18n - no hardcoded UI text.
If a new component in the ui package needs labels and other kinds of localized text, it should accept a labels prop with the relevant strings. The prop should have its own type defined. Refer to packages/ui/src/components/QueueItem/types.ts for an example.
We don't care about that. If there's an opportunity to handle that easily, do it, but don't go out of your way.
packages/ui/src/components/MyComponent/packages/ui/src/components/index.ts