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
177 changes: 144 additions & 33 deletions .agents/skills/create-component/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: >-

# Create Component Workflow for @creativecodeco/ui

Follow this standardized 6-step workflow whenever creating a new component or form control in `@creativecodeco/ui`.
Follow this standardized workflow and best-practices checklist whenever creating or updating a UI component or Form control in `@creativecodeco/ui`.

---

Expand All @@ -17,64 +17,169 @@ Create the props interface co-located in `src/ui/components/<category>/<componen

**Rules:**

- Name the interface `<ComponentName>Type` (e.g. `CardType`, `StepsType`).
- Mark all optional props with `?`.
- Use specific types (`React.ReactNode`, `React.Key`, `ColorType`, `SizeType`, etc.).
- Name the interface `<ComponentName>Type` (e.g. `CardType`, `StepsType`, `SelectType`).
- **Always include `className?: string`** on every component interface so consumers can compose layout utilities cleanly.
- **For Form controls (`src/ui/forms/*`)**: Always extend `ErrorType` (`import type { ErrorType } from '@/types'`) so the control supports both `isError?: boolean` and `error?: string`, plus `id?: string`, `name?: string`, `label?: string`, and `disabled?: boolean`.
- Mark all non-mandatory props as optional with `?` to prevent friction in unit tests, Storybook, and React Hook Form `Controller`.
- Use specific types (`React.ReactNode`, `React.Key`, `ColorType`, `SizeType`, etc.) — **zero `any` or implicit types**.
- Re-export the types from the component's `index.ts` (`export type * from './<component-name>.types';`), which automatically flows through `src/types/index.ts`.

### UI Component Interface Example

```ts
import type { ColorType } from '@/types';
import type { ColorType, SizeType } from '@/types';

export interface ComponentNameType {
children?: React.ReactNode;
variant?: ColorType;
color?: ColorType;
size?: SizeType;
disabled?: boolean;
className?: string;
}
```

### Form Control Interface Example

```ts
import type { ColorType, ErrorType, SizeType } from '@/types';

export interface FormControlNameType extends ErrorType {
id?: string;
name?: string;
label?: string;
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
color?: ColorType;
size?: SizeType;
disabled?: boolean;
className?: string;
}
```

---

## Step 2: Implement Component Logic & Layout
## Step 2: Implement Component Logic, A11y & Responsive Design

Create `src/ui/components/<category>/<component-name>/<component-name>.component.tsx` (or `src/ui/forms/<component-name>/<component-name>.component.tsx`).

**Rules:**
### 2.1 Core Implementation Rules

- Use `classnames` imported as `import cls from 'classnames'`.
- Import types from `@/types`.
- Use default parameters for optional props.
- Ensure strict React key handling when mapping arrays: `key={item.id || index}`.
- **Safe `className` Merging**: Always destructure `className` and merge it via `cls('base-class', className, { ... })` **after** or separately from `{...restProps}` so spreading HTML attributes never overwrites internal DaisyUI classes.
- **Prevent Custom Prop Leakage to DOM**: Always destructure custom library props (`isError`, `error`, `label`, `color`, `size`, `bordered`) before spreading `{...restProps}` onto native DOM elements (`<input>`, `<textarea>`, `<button>`, `<select>`).
- **Use `forwardRef` on Form Controls**: All form controls (`src/ui/forms/*`) must use `React.forwardRef` and assign `displayName` so `react-hook-form` (`Controller` / `register`) and consumers can attach DOM refs for focus management.
- **Semantic Theme Colors Only**: Always use DaisyUI theme tokens (`primary`, `secondary`, `accent`, `neutral`, `base-100`, `base-200`, `base-300`, `base-content`, `info`, `success`, `warning`, `error`). Never hardcode raw Tailwind palette colors (e.g., `text-red-500`, `bg-black`, `bg-blue-500`). When a component defines a default `color` prop, default to `'primary'` (or `'info'` for status alerts).
- **OWASP Security Hardening**:
- **URL Sanitization (Anti-XSS)**: Always pass dynamic `href` and `src` props through `sanitizeUrl` from `@/utils` (`href={sanitizeUrl(href)}` for links, `src={sanitizeUrl(src, '')}` for images) and validate external URLs with `isValidUrl` (which enforces `http:` / `https:`) to block `javascript:`, `vbscript:`, and `data:` XSS payloads.
- **CSS `url(...)` Injection Prevention**: Never interpolate raw strings directly into inline `backgroundImage: url(...)`; sanitize with `sanitizeUrl(image, '').replace(/["'()\\]/g, '')`.
- **Reverse Tabnabbing Protection**: Whenever rendering `<a target='_blank'>`, always enforce `noopener` and `noreferrer` in `rel` even when a custom `rel` prop is passed.
- **Forbidden Sinks**: Never use `dangerouslySetInnerHTML`, `innerHTML`, `outerHTML`, `eval`, or `new Function`.
- **Controlled Input Safety (`readOnly` Fallback)**: For checkable or range inputs (`checked` or `value` prop), add `readOnly={checked !== undefined && !onChange}` (or `value !== undefined && !onChange`) so rendering static states in Storybook or tests never triggers React controlled-input console warnings.
- **Strict React `key` Handling**: When iterating arrays, `key` must strictly evaluate to `React.Key` (`string | number | bigint`). Never assign a `React.ReactNode` directly to `key` without narrowing its type (`typeof item.label === 'string' || typeof item.label === 'number' ? item.label : index`).
- Export as `export default ComponentName`.

### 2.2 Accessibility (WAI-ARIA / A11y) Checklist

Every component MUST satisfy these accessibility standards:

- **Form Controls (`src/ui/forms/*`)**:
- Link `<label className='label' htmlFor={id}>` with the control's `id={id}`.
- Set `aria-invalid={Boolean(isError || error) || undefined}` on the native form control (`<input>`, `<select>`, `<textarea>`).
- Render error messages with `role='alert'` (`{error && <span role='alert' className='text-xs text-error mt-1'>{error}</span>}`) so screen readers announce validation errors immediately.
- Provide fallback `aria-label` when a visual label is optional or when rendering composite controls (e.g., `Otp`, `Rating`, `Swap`, `Collapse`).
- **Interactive & Display Elements**:
- Use [`useSafeButtonProps`](file:///Users/joaltoroc/Code/creativecodeco/ui/src/hooks/use-safe-button-props.hook.ts) or explicit `type='button'` on non-submit `<button>` elements, plus `aria-disabled` when disabled.
- Add descriptive `aria-label` on icon-only buttons, close buttons (`aria-label='Close'`), pagination buttons (`aria-label={'Page X'}`), and hidden/mask inputs (`Rating` radio items, `Swap` checkbox, `Collapse` checkbox).
- Group related radio/action controls with `role='radiogroup'` or `role='group'` and `aria-label`.
- **Feedback & Overlays**:
- `Alert`: `role='alert'`.
- `Toast` & `Status` & `Countdown`: `role='status'` (or `role='timer'`) with `aria-live='polite'`.
- `Modal`: `<dialog open aria-modal='true' aria-label={typeof title === 'string' ? title : 'Modal dialog'}>`.
- `Progress` & `RadialProgress` & `Loading`: Include `aria-label` (`'Progress'`, `'Loading'`) and `role='progressbar'` / `aria-valuenow` where applicable.
- **Navigation**:
- Wrap navigation landmarks in `<nav aria-label='...'>`.
- Mark active items with `aria-current='page'` (`Breadcrumb`, `Pagination`, `Dock`) or `aria-current='step'` (`Steps`).
- Complete WAI-ARIA tab pattern with `role='tablist'`, `role='tab'` + `aria-selected`, and `role='tabpanel'`.
- Include `aria-haspopup='true'` and `aria-expanded={open}` on dropdown/megamenu triggers.

### 2.3 Responsive (Mobile-First) & Motion Polish Checklist

- **Prevent Mobile Viewport Overflow**:
- Overlays and panels with fixed desktop widths must include fluid mobile constraints:
- `Modal`: `modal-box w-11/12` alongside `max-w-xs` / `max-w-sm` / `max-w-lg` / `max-w-3xl`.
- `Drawer`: `w-80 max-w-[85vw]`.
- `Calendar`: `w-72 max-w-full`.
- `MegaMenu`: `w-screen max-w-md sm:max-w-2xl md:min-w-max` and responsive grid (`grid-cols-1 sm:grid-cols-2 md:grid-cols-3`).
- **Scroll & Wrap Protection**:
- Wrap data tables in `<div className='overflow-x-auto'>`.
- Add `overflow-x-auto` to horizontal `Steps` and `flex-wrap` to `Pagination` (`join flex-wrap`) and `Tabs` (`tabs flex-wrap`).
- Default form controls (`TextBox`, `Select`, `TextArea`, `FileInput`, `Progress`) to `w-full` inside their container.
- **Animations & Tactile Feedback**:
- Use custom CSS easing tokens defined in `src/theme/main.css` (`--ease-out`, `--ease-in-out`, `--ease-drawer`) rather than `linear` or default `ease`.
- Animate only compositor-friendly properties (`transform`, `opacity`).
- Always honor `@media (prefers-reduced-motion: reduce)` when adding custom CSS transitions or keyframe animations in `src/theme/*.css`.

### Form Control Implementation Example

```tsx
import cls from 'classnames';
import type { ComponentNameType } from '@/types';

const ComponentName = ({
children,
variant = 'primary',
import type { FormControlNameType } from '@/types';

const FormControlName = ({
id,
name,
label,
value,
onChange,
color,
size = 'md',
disabled = false,
className
}: ComponentNameType) => {
isError,
error,
className,
...restProps
}: FormControlNameType) => {
const hasError = Boolean(isError || error);

return (
<div
className={cls('component-base', className, {
'component-primary': variant === 'primary',
'component-disabled': disabled
})}
>
{children}
<div className='form-control w-full'>
{label && (
<label className='label' htmlFor={id}>
<span className='label-text'>{label}</span>
</label>
)}
<input
id={id}
name={name}
value={value}
onChange={onChange}
readOnly={value !== undefined && !onChange}
disabled={disabled}
aria-invalid={hasError || undefined}
{...restProps}
className={cls('input w-full', className, {
[`input-${color}`]: Boolean(color),
[`input-${size}`]: Boolean(size),
'input-error': hasError
})}
/>
{error && (
<span role='alert' className='text-xs text-error mt-1'>
{error}
</span>
)}
</div>
);
};

export default ComponentName;
export default FormControlName;
```

---

## Step 3: Configure Module Exports
## Step 3: Configure Module Exports & RHF Controller (If Form Control)

1. Create `src/ui/components/<category>/<component-name>/index.ts` (or `src/ui/forms/<component-name>/index.ts`):
```ts
Expand All @@ -85,6 +190,9 @@ export default ComponentName;
export default ComponentName;
```
2. Re-export the directory in `src/ui/components/<category>/index.ts` (or `src/ui/forms/index.ts`).
3. **If creating a Form control (`src/ui/forms/*`)**: Register the new control in:
- `src/types/helpers/controller.types.ts` (`ControllerType` discriminated union).
- `src/helpers/react-hook-form/controller.component.tsx` (`Controller` switch/branch rendering with `isError={Boolean(error)}` and `error={error?.message}`).

---

Expand All @@ -94,19 +202,22 @@ Create `src/ui/components/<category>/<component-name>/<component-name>.test.tsx`

**Rules:**

- Test default render and text content.
- Test active/disabled states and class assignments.
- Test user interactions (clicks, input changes).
- Test default render and text/children content.
- Test `className` prop merging, variant/size/color classes, and `disabled` / `isError` / `error` states.
- Test accessibility queries (`getByRole`, `getByLabelText`, `aria-invalid`, `role='alert'`).
- Test user interactions (clicks, input changes) and ensure **zero `console.error` or `console.warn` outputs** during test execution.

```tsx
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ComponentName from './<component-name>.component';

describe('<ComponentName />', () => {
it('renders correctly with children', () => {
render(<ComponentName>Test Content</ComponentName>);
expect(screen.getByText('Test Content')).toBeInTheDocument();
it('renders correctly with children and custom className', () => {
render(
<ComponentName className='custom-class'>Test Content</ComponentName>
);
expect(screen.getByText('Test Content')).toHaveClass('custom-class');
});
});
```
Expand All @@ -120,8 +231,8 @@ Create `src/ui/components/<category>/<component-name>/<component-name>.stories.t
**Rules:**

- Set `title: '@creativecodeco-ui/<Category>/<ComponentName>'` (where `<Category>` is `Display`, `Feedback`, `Layout`, `Navigation`, or `Form`).
- Include `tags: ['autodocs']`.
- Provide `Default` and variant stories (`Disabled`, `WithIcons`, etc.).
- **Always include `tags: ['autodocs']`** in the default meta export.
- Provide `Default` and key variant stories (`Colors`, `Sizes`, `WithError`, `Disabled`, etc.).

---

Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,33 @@ on:
branches:
- master

permissions:
contents: read

# List of jobs
jobs:
chromatic-deployment:
# Operating System
runs-on: ubuntu-latest
# Job steps
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Install dependencies
# 👇 Install dependencies with the same package manager used in the project (replace it as needed), e.g. yarn, npm, pnpm
run: npm ci --ignore-scripts
# 👇 Adds Chromatic as a step in the workflow

- name: Publish to Chromatic
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # v1.11.0
uses: chromaui/action@5bcadb5104b09e360e210c54f95a7437c2d82bab # v18.13.3
# Chromatic GitHub Action options
with:
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
Expand Down
25 changes: 22 additions & 3 deletions .github/workflows/npm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,44 @@ name: Build

on:
pull_request:
types: [ready_for_review]
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Clean and Install
run: npm ci --ignore-scripts

- name: Security Audit
run: npm audit --audit-level=high

- name: Lint and Format Check
run: npm run lint && npm run format:check

- name: TypeScript Check
run: npm run typecheck

- name: Build
run: npm run build

- name: Run tests
run: npm run test:action

- name: Jest coverage comment
uses: MishaKav/jest-coverage-comment@14d08a64515f6fd4b28ae6a67c5c43364cea1357 # v1.0.95
uses: MishaKav/jest-coverage-comment@7032bf8e71aa2b267b6b143be3c4480b625c2b12 # v1.0.38
with:
coverage-summary-path: ./coverage/coverage-summary.json
title: Pull Request Coverage - By CreativeCode.com.co
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@ on:
release:
types: [created]

permissions:
contents: read
id-token: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Setup .npmrc file to publish to npm
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.2.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '20.x'
node-version-file: '.nvmrc'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'

- name: Clean and Install
run: npm ci --ignore-scripts

- name: Validate and Test
run: npm run lint && npm run format:check && npm run typecheck && npm run test

- name: Configure git
run: |
git config user.email "joaltoroc@creativecode.com.co"
Expand All @@ -31,7 +39,7 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish package
run: npm run release
run: npm publish --provenance --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v24
v24.22.1
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"singleQuote": true,
"jsxSingleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": false
"bracketSameLine": false
}
Loading
Loading