Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ yarn generate:tokens # Generate CSS variables
- Include unit tests and Storybook stories
- Use design tokens for consistent styling
- Ensure accessibility compliance
- **All components MUST follow the namespace interface pattern (see guidelines/interface-pattern.md)**
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ This project was imported from the Reapit Foundations Monorepo at version 4.0.2.
- If you are interested in our future roadmap, you can view [here](https://github.com/orgs/reapit/projects/16/views/2).

Please read our [disclaimer](./DISCLAIMER.md) before proceeding.

## For Contributors & AI Agents

- **[AGENTS.md](./AGENTS.md)** - Essential guide for AI agents working on this project
- **[guidelines/interface-pattern.md](./guidelines/interface-pattern.md)** - TypeScript interface pattern requirements and code review checklist
117 changes: 117 additions & 0 deletions guidelines/interface-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Interface Pattern Code Review Checklist

Quick reference for AI agents when reviewing or writing component code.

## ✅ Required Pattern

```typescript
export function ComponentName({ prop }: ComponentName.Props) {
// implementation
}

export namespace ComponentName {
export interface Props {
/** JSDoc for each prop */
prop: string
}
}
```

## 🔍 Code Review Checklist

### For New Components

- [ ] Uses `ComponentName.Props` pattern (not `ComponentNameProps`)
- [ ] Namespace exported and matches component name exactly
- [ ] Props interface inside namespace
- [ ] All props have JSDoc documentation
- [ ] No standalone interface definitions

### For Component Migrations

- [ ] Original `ComponentNameProps` converted to namespace
- [ ] Deprecated type alias added: `export type ComponentNameProps = ComponentName.Props`
- [ ] Component function signature updated to use `ComponentName.Props`
- [ ] Tests still pass after migration

### For Compound Components

- [ ] Subcomponents use nested namespaces: `Parent.Child.Props`
- [ ] Static properties properly typed
- [ ] Each subcomponent follows same pattern

### For Utility Functions

- [ ] Input/output types use function name as namespace
- [ ] Example: `utilityFunction.Input` and `utilityFunction.Output`

## 🚫 Common Mistakes to Flag

```typescript
// ❌ Standalone interface
interface ButtonProps { }

// ❌ Wrong namespace name
export namespace ButtonComponent {
export interface Props { }
}

// ❌ Missing JSDoc
export namespace Button {
export interface Props {
variant: string // No documentation
}
}

// ❌ Props outside namespace
export interface Props { }
export namespace Button { }
```

## 🎯 Quick Fix Examples

**Before (wrong):**
```typescript
interface DialogProps {
open: boolean
}

export function Dialog({ open }: DialogProps) {
return <div>{open ? 'Open' : 'Closed'}</div>
}
```

**After (correct):**
```typescript
export namespace Dialog {
export interface Props {
/** Whether the dialog is open */
open: boolean
}
}

/**
* @deprecated Use `Dialog.Props` instead.
*/
export type DialogProps = Dialog.Props

export function Dialog({ open }: Dialog.Props) {
return <div>{open ? 'Open' : 'Closed'}</div>
}
```

## 📋 Review Guidelines

When reviewing code, check these locations:

- `src/core/*/` - All core components
- `src/utils/*/` - All utility components
- `src/lab/*/` - Lab components (should follow pattern)

Skip these locations:

- `src/deprecated/*/` - Legacy components (don't modify)
- `src/icons/*/` - Generated components
- `src/tokens/*/` - Generated tokens

All new components must follow the namespace interface pattern before merging.
10 changes: 6 additions & 4 deletions src/core/checkbox-group/checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { FC, ReactNode, HTMLAttributes } from 'react'
import { CheckboxGroupLabel } from './checkbox-group.atoms'
import { ElCheckboxGroup } from './styles'

export interface CheckboxGroupProps extends HTMLAttributes<HTMLDivElement> {
orientation?: 'vertical' | 'horizontal'
children?: ReactNode
export namespace CheckboxGroup {
export interface Props extends HTMLAttributes<HTMLDivElement> {
orientation?: 'vertical' | 'horizontal'
children?: ReactNode
}
}

export interface CheckboxGroupFC extends FC<CheckboxGroupProps> {
export interface CheckboxGroupFC extends FC<CheckboxGroup.Props> {
Label: typeof CheckboxGroupLabel
}

Expand Down
40 changes: 21 additions & 19 deletions src/core/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,34 @@ import { ElCheckboxIcon, ElCheckboxSelectedIcon, ElCheckboxIndeterminateIcon } f
import { ElCheckbox, ElCheckboxInput, ElCheckboxLabelText, ElCheckboxSupplementaryInfo } from './styles'
import mergeRefs from '#src/helpers/mergeRefs'

/**
* Interface for the Checkbox component props.
*
* Optional label text to display next to the checkbox.
* Provides a user-friendly description of the checkbox's purpose.
*
* Optional supplementary information to display below the label.
* Offers additional context or details related to the checkbox.
*
* Optional required to make input element required.
*
* Optional isIndeterminate to make input element as an indeterminate state.
*/
export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
supplementaryInfo?: string
required?: boolean
isIndeterminate?: boolean
export namespace Checkbox {
/**
* Interface for the Checkbox component props.
*
* Optional label text to display next to the checkbox.
* Provides a user-friendly description of the checkbox's purpose.
*
* Optional supplementary information to display below the label.
* Offers additional context or details related to the checkbox.
*
* Optional required to make input element required.
*
* Optional isIndeterminate to make input element as an indeterminate state.
*/
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label?: string
supplementaryInfo?: string
required?: boolean
isIndeterminate?: boolean
}
}

/**
* Checkbox component: A styled and composed checkbox element.
* This component combines the atomic checkbox components (ElCheckbox, ElCheckboxInput, etc.)
* into a single, reusable checkbox component.
*/
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
export const Checkbox = forwardRef<HTMLInputElement, Checkbox.Props>(
({ label, supplementaryInfo, required, isIndeterminate = false, className, ...rest }, ref) => {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
Expand Down
19 changes: 13 additions & 6 deletions src/core/empty-data/action/action-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import { Button } from '#src/core/button'
import type { AttributesToOmit } from './common'
import type { ComponentProps, MouseEventHandler, ReactNode } from 'react'

interface EmptyDataActionButtonProps extends Omit<ComponentProps<typeof Button>, AttributesToOmit> {
/** The action's label. */
children: ReactNode
/** The action to perform. */
onClick?: MouseEventHandler<HTMLButtonElement>
export namespace EmptyDataActionButton {
export interface Props extends Omit<ComponentProps<typeof Button>, AttributesToOmit> {
/** The action's label. */
children: ReactNode
/** The action to perform. */
onClick?: MouseEventHandler<HTMLButtonElement>
}
}

export function EmptyDataActionButton(props: EmptyDataActionButtonProps) {
/**
* @deprecated Use `EmptyDataActionButton.Props` instead.
*/
export type EmptyDataActionButtonProps = EmptyDataActionButton.Props

export function EmptyDataActionButton(props: EmptyDataActionButton.Props) {
return <Button {...props} size="medium" variant="tertiary" useLinkStyle />
}
19 changes: 13 additions & 6 deletions src/core/empty-data/action/action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ import { AnchorButton } from '#src/core/button'
import type { AttributesToOmit } from './common'
import type { ComponentProps, ReactNode } from 'react'

interface EmptyDataActionProps extends Omit<ComponentProps<typeof AnchorButton>, AttributesToOmit> {
/** The action's label. */
children: ReactNode
/** The URL to navigate to; will typically be an entity creation page or drawer. */
href: string
export namespace EmptyDataAction {
export interface Props extends Omit<ComponentProps<typeof AnchorButton>, AttributesToOmit> {
/** The action's label. */
children: ReactNode
/** The URL to navigate to; will typically be an entity creation page or drawer. */
href: string
}
}

/**
* @deprecated Use `EmptyDataAction.Props` instead.
*/
export type EmptyDataActionProps = EmptyDataAction.Props

/**
* A simple action component. Comes in two varieties: `EmptyData.Action`, which renders as an
* anchor element, and `EmptyData.ActionButton`, which renders as an anchor element.
*
* Use `EmptyData.Action` when you need button-like styling but want to navigate to a URL. Use
* `EmptyData.ActionButton` when the action needs to occur on click.
*/
export function EmptyDataAction(props: EmptyDataActionProps) {
export function EmptyDataAction(props: EmptyDataAction.Props) {
return <AnchorButton {...props} size="medium" variant="tertiary" useLinkStyle />
}
19 changes: 13 additions & 6 deletions src/core/empty-data/description/description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import { ElEmptyDataDescription, ElEmptyDataDescriptionSecondaryText, ElEmptyDat

import type { HTMLAttributes, ReactNode } from 'react'

interface EmptyDataDescriptionProps extends HTMLAttributes<HTMLDivElement> {
/** The empty data's title text. */
children: ReactNode
/** The empty data's secondary text. */
secondaryText?: ReactNode
export namespace EmptyDataDescription {
export interface Props extends HTMLAttributes<HTMLDivElement> {
/** The empty data's title text. */
children: ReactNode
/** The empty data's secondary text. */
secondaryText?: ReactNode
}
}

/**
* @deprecated Use `EmptyDataDescription.Props` instead.
*/
export type EmptyDataDescriptionProps = EmptyDataDescription.Props

/**
* A simple component that displays a title and optional secondary text for the `EmptyData`.
*/
export function EmptyDataDescription({ children, secondaryText, ...rest }: EmptyDataDescriptionProps) {
export function EmptyDataDescription({ children, secondaryText, ...rest }: EmptyDataDescription.Props) {
return (
<ElEmptyDataDescription {...rest}>
<ElEmptyDataDescriptionTitle>{children}</ElEmptyDataDescriptionTitle>
Expand Down
10 changes: 6 additions & 4 deletions src/core/mobile-nav-item/mobile-nav-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import type { FC } from 'react'
import { MobileNavItemExpandable, MobileNavItemSimple } from './mobile-nav-item.atoms'
import type { MobileNavItemExpandableProps, MobileNavItemSimpleProps } from './mobile-nav-item.atoms'

const isExpandableVariant = (props: MobileNavItemProps): props is MobileNavItemExpandableProps => {
return 'children' in props
export namespace MobileNavItem {
export type Props = MobileNavItemSimpleProps | MobileNavItemExpandableProps
}

export type MobileNavItemProps = MobileNavItemSimpleProps | MobileNavItemExpandableProps
const isExpandableVariant = (props: MobileNavItem.Props): props is MobileNavItemExpandableProps => {
return 'children' in props
}

export const MobileNavItem: FC<MobileNavItemProps> = (props) => {
export const MobileNavItem: FC<MobileNavItem.Props> = (props) => {
if (isExpandableVariant(props)) {
return <MobileNavItemExpandable {...props} />
} else {
Expand Down
Loading
Loading