Skip to content
Open
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ The Storybook documentation covers:

---

## AI Agent Skills

This repository ships an **integration skill** for AI coding agents that consume `@tedi-design-system/react` in a downstream application. The skill lives at [`skills/tedi-react/`](./skills/tedi-react) and conforms to the [skill.sh](https://skill.sh) standard, so it works with any modern AI tool that supports skills.

It teaches an agent:

- The canonical import paths (`/tedi` vs `/community`), required providers, and setup snippet
- Component APIs, props, polymorphic and breakpoint patterns
- Form control conventions (controlled/uncontrolled, helpers, validation)
- Theming with design tokens from `@tedi-design-system/core`
- Common pitfalls to avoid (deprecated Community components, hardcoded colors, `var()` fallbacks, etc.)
- Pointers back to this repo and the [live Storybook](https://storybook.tedi.ee/react/main/?path=/docs/documentation-get-started--get-started) as authoritative sources

### Install

Use the [skills.sh](https://skills.sh) CLI from your project root:

```bash
npx skills add TEDI-Design-System/react
```

The CLI auto-discovers the `tedi-react` skill under [`skills/`](./skills) and registers it for any compatible agent. Once installed, the agent will trigger the skill whenever you work with TEDI React components.

> Skills for **contributing to** the TEDI Design System (contributor skills, standards validation, etc.) live in a separate repo: [TEDI-Design-System/ai-skills](https://github.com/TEDI-Design-System/ai-skills).

---

## Repository Development Guide (Contributors)

The following instructions apply only if you are working on this repository itself
Expand Down
65 changes: 60 additions & 5 deletions skills/tedi-react/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
---
name: tedi-react
description: >
Build UIs with @tedi-design-system/react — 50+ accessible React components with design token
theming. Use when creating interfaces, integrating form controls, customizing themes, or working
with TEDI components in a React application.
Build UIs with @tedi-design-system/react — the official Estonian government React component
library (`@tedi-design-system/react`). Use whenever the user is integrating, importing, or
composing TEDI components in a downstream React app: `Button`, `Alert`, `TextField`, `Select`,
`Card`, `Tooltip`, `Dropdown`, `Tabs`, `Toggle`, `Pagination`, `EmptyState`, `Table`, `Modal`,
etc. Triggers on theming with TEDI design tokens, switching dark/light theme via
`ThemeProvider`, wiring `LabelProvider` / `StyleProvider` / `AccessibilityProvider`, form
validation with the `helper` prop, responsive `xs` / `md` / `lg` breakpoint props, and
polymorphic `as`-prop usage. Do NOT use when contributing to the TEDI library repo itself —
use `tedi-react-contributing` for that.
---

# TEDI Design System — React

React component library with 50+ accessible components. Built on React 18/19 with TypeScript, CSS Modules, and design tokens from `@tedi-design-system/core`.

## Authoritative Sources

This skill bundles a snapshot of the API and patterns, but the library is public and ships fast. When a prop, default, or component listed below feels stale or absent, treat these as the source of truth and fetch from them.

### Pin to the consumer's installed version

Before fetching source, **determine which version of `@tedi-design-system/react` the project actually has installed** and browse the matching git tag — not `main`. The repo's release tags follow the pattern `react-<version>` (e.g. `react-17.0.0-rc.8`, `react-17.1.0-rc.4`).

1. Read the resolved version from the project — `package.json`'s `dependencies."@tedi-design-system/react"`, or `npm ls @tedi-design-system/react`, or the lockfile entry. Strip any range prefix (`^`, `~`).
2. Construct the tag URL: `https://github.com/TEDI-Design-System/react/tree/react-<version>/...`
3. If the resolved version is a pre-release or the tag doesn't exist (rare), fall back to `main` and note the version mismatch when answering.

**Example** for a project on `17.0.0-rc.8`:
- TEDI-Ready components: `https://github.com/TEDI-Design-System/react/tree/react-17.0.0-rc.8/src/tedi/components`
- Barrel export: `https://github.com/TEDI-Design-System/react/blob/react-17.0.0-rc.8/src/tedi/index.ts`
- Specific component: `https://github.com/TEDI-Design-System/react/blob/react-17.0.0-rc.8/src/tedi/components/buttons/button/button.tsx`

### Canonical references

- **Source code & releases**: [github.com/TEDI-Design-System/react](https://github.com/TEDI-Design-System/react) — TEDI-Ready components live under `src/tedi/components/`, community under `src/community/components/`. The barrel export `src/tedi/index.ts` is the canonical list of TEDI-Ready exports. Always prefer the version-pinned tag URLs (see above) over `main` when consulting source.
- **Live Storybook (interactive docs + prop tables)**: [storybook.tedi.ee/react/main](https://storybook.tedi.ee/react/main/?path=/docs/documentation-get-started--get-started) — has every component's args table, default values, and runnable examples. Note that the public Storybook tracks `main`; if it disagrees with the consumer's installed tag, the tag wins.
- **Design system wiki** (cross-framework guidelines): [github.com/TEDI-Design-System/general/wiki](https://github.com/TEDI-Design-System/general/wiki)
- **Releases & changelog**: [github.com/TEDI-Design-System/react/releases](https://github.com/TEDI-Design-System/react/releases), [CHANGELOG.md](https://github.com/TEDI-Design-System/react/blob/main/CHANGELOG.md), [Issues](https://github.com/TEDI-Design-System/react/issues)
- **npm**: [@tedi-design-system/react](https://www.npmjs.com/package/@tedi-design-system/react)
- **Sibling packages**: [@tedi-design-system/core](https://www.npmjs.com/package/@tedi-design-system/core) (tokens, SCSS, icons), [@tedi-design-system/angular](https://www.npmjs.com/package/@tedi-design-system/angular) (Angular counterpart — useful for behavioral parity questions)

**Verification tip**: if the user asks about a recently added component or a prop you're unsure of, fetch the relevant `.tsx` file from the version-pinned tag (e.g. `src/tedi/components/<category>/<name>/<name>.tsx`) — the JSDoc on `interface ...Props` is the canonical spec.

## Installation

```bash
Expand All @@ -29,21 +63,30 @@ dayjs: ^1.11.10
### 1. Wrap your app with providers

```tsx
import { ThemeProvider, LabelProvider, StyleProvider } from '@tedi-design-system/react/tedi';
import {
ThemeProvider,
LabelProvider,
StyleProvider,
AccessibilityProvider,
} from '@tedi-design-system/react/tedi';

function App() {
return (
<ThemeProvider>
<LabelProvider>
<StyleProvider>
<YourApp />
<AccessibilityProvider>
<YourApp />
</AccessibilityProvider>
</StyleProvider>
</LabelProvider>
</ThemeProvider>
);
}
```

`AccessibilityProvider` exposes `useDeclareLoader` and other a11y hooks; omit it only if you have no loaders/announcements. `PrintingProvider` is also available — wrap it inside `AccessibilityProvider` when you need the `usePrint` context.

### 2. Import core styles

```tsx
Expand Down Expand Up @@ -186,6 +229,18 @@ import { Alert, sendNotification, ToastContainer } from '@tedi-design-system/rea
sendNotification({ type: 'success', title: 'Done', children: 'Task completed' });
```

## Common Pitfalls

A handful of mistakes account for most TEDI integration issues. Avoid them up front:

- **Import from `/tedi` or `/community`, never the package root.** `@tedi-design-system/react` is not a valid import path — the package has explicit entry points (`@tedi-design-system/react/tedi`, `@tedi-design-system/react/community`, `@tedi-design-system/react/index.css`). Importing from the root will fail or silently miss types.
- **Prefer TEDI-Ready over Community whenever possible.** Several Community components (`Button`, `Anchor`, `Check`, `Radio`, `Tabs`, `Toggle`, `Tooltip`, `Dropdown`, `Tag`) are deprecated in favor of TEDI-Ready equivalents. Reach into Community only when no TEDI-Ready alternative exists (e.g. `Modal`, `Stepper`, `Table`, `DateTimePicker`).
- **Always pass `id` to form controls.** `TextField`, `Select`, `Checkbox`, `Radio`, etc. require it — it's how the label/helper/aria wiring works. There is no auto-generated fallback.
- **Use design tokens, not hardcoded colors.** Reach for `var(--tedi-color-*)`, `var(--tedi-spacing-*)`, etc. from `@tedi-design-system/core` instead of hex codes. This is what makes theme switching and brand overrides work.
- **Do not add CSS `var()` fallbacks.** Write `var(--tedi-spacing-4)`, not `var(--tedi-spacing-4, 16px)` — fallbacks defeat token-driven theming.
- **Support both controlled and uncontrolled.** When wrapping a TEDI form control with your own, accept `value`/`defaultValue` and forward both — don't force consumers into one mode.
- **Mock `useBreakpointProps` in tests** for any component you wrote that uses breakpoint support; jsdom won't respond to media queries.

## Additional References

Load based on your task — **do not load all at once**:
Expand Down
148 changes: 137 additions & 11 deletions skills/tedi-react/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,40 @@ Same as Checkbox (without indeterminate)
- `label: string` (required)
- `accept?: string`, `multiple?: boolean`, `maxSize?: number`

### Toggle
**Props:** `ToggleProps` | fRef, form
- `id: string` (required)
- `label: ReactNode` (required), `hideLabel?: boolean`, `labelPosition?: 'left' | 'right' = 'right'`
- `checked?: boolean`, `defaultChecked?: boolean`
- `onChange?: (value: boolean) => void`
- `size?: 'default' | 'large'`
- `color?: 'primary' | 'colored'`
- `helper?: FeedbackTextProps`
- `disabled?: boolean`, `isLoading?: boolean`

```tsx
<Toggle id="notifications" label="Email me" checked={on} onChange={setOn} />
```

### InputGroup
Compose a labeled input with prefix/suffix slots (e.g. currency symbol, unit, button addon).

**Props:** `InputGroupProps` extends `FormLabelProps`
- `children: ReactNode` (required) — use `InputGroup.Input` plus optional `InputGroup.Prefix` / `InputGroup.Suffix`
- `addons?: boolean = true` — merge borders/radius into a single visual control
- `helper?: FeedbackTextProps | FeedbackTextProps[]`
- `disabled?: boolean`, plus all `FormLabel` props (`label`, `id`, `required`, etc.)

Sub-components: `InputGroup.Input`, `InputGroup.Prefix`, `InputGroup.Suffix`

```tsx
<InputGroup id="price" label="Price">
<InputGroup.Prefix>€</InputGroup.Prefix>
<InputGroup.Input type="number" />
<InputGroup.Suffix>/ month</InputGroup.Suffix>
</InputGroup>
```

## Layout

### Row / Col (Grid)
Expand Down Expand Up @@ -327,6 +361,61 @@ Sub-component: `Skeleton.Block`
<Link as={NavLink} to="/profile">Profile</Link>
```

### Tabs
**Props:** `TabsProps`
- `children: ReactNode` (required) — use `Tabs.List` + `Tabs.Trigger` and `Tabs.Content`
- `value?: string` (controlled), `defaultValue?: string`
- `onChange?: (tabId: string) => void`

Sub-components: `Tabs.List`, `Tabs.Trigger` (props: `id` required, `icon?`, `disabled?`), `Tabs.Content` (props: `id` to scope content to a tab)

```tsx
<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Trigger id="overview">Overview</Tabs.Trigger>
<Tabs.Trigger id="settings" icon="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content id="overview">Overview panel</Tabs.Content>
<Tabs.Content id="settings">Settings panel</Tabs.Content>
</Tabs>
```

### Pagination
**Props:** `PaginationProps`
- `pageCount: number` (required)
- `page?: number` (controlled, 1-based), `defaultPage?: number = 1`
- `onPageChange?: (page: number) => void`
- `totalItems?: number` — renders a "{count} results" label when set
- `pageSize?: number`, `pageSizeOptions?: number[]`, `onPageSizeChange?: (size: number) => void`
- `labels?: Partial<PaginationLabels>` — override default English labels (`ariaLabel`, `previous`, `next`, `pageAriaLabel`, `currentPageAriaLabel`, `results`, `pageSize`)

```tsx
<Pagination
pageCount={20}
page={page}
onPageChange={setPage}
totalItems={195}
pageSize={10}
pageSizeOptions={[10, 20, 50]}
onPageSizeChange={setPageSize}
/>
```

### HashTrigger
Wraps an element and fires a callback (and optionally scrolls to it) when the URL hash matches. The `id` is injected onto the first child element so the browser can resolve it. Handy for opening modals or scrolling to sections from external deep links.

**Props:** `HashTriggerProps`
- `children: ReactNode` (required) — receives `id` injected onto the first child element; if `children` isn't a valid element, `HashTrigger` wraps them in a `<div id={id}>`
- `id: string` (required) — hash value to match (without the leading `#`)
- `onMatch?: (id: string) => void` — fired when the hash matches; receives the matched id
- `scrollOnMatch?: boolean = true` — scrolls the element into view if it's off-screen (instant on initial load, smooth otherwise)

```tsx
<HashTrigger id="section-2" onMatch={(id) => console.log('matched', id)}>
<section>Section 2 content</section>
</HashTrigger>
```

## Notifications

### Alert
Expand Down Expand Up @@ -413,6 +502,20 @@ Sub-components: `Popover.Trigger`, `Popover.Content`
<StatusBadge color="success" status="success" icon="check">Active</StatusBadge>
```

### StatusIndicator
Small colored dot for status — pair with a label or position over another element.

**Props:** `StatusIndicatorProps`
- `type?: 'success' | 'danger' | 'warning' | 'inactive' = 'success'`
- `size?: 'sm' | 'lg' = 'sm'`
- `hasBorder?: boolean` — white ring (use over avatars/icons)
- `position?: 'default' | 'top-right'` — absolute-positioned at parent's corner

```tsx
<StatusIndicator type="success" />
<StatusIndicator type="danger" size="lg" hasBorder position="top-right" />
```

## Misc

### Separator
Expand All @@ -423,6 +526,36 @@ Sub-components: `Popover.Trigger`, `Popover.Content`
- `thickness?: 1 | 2`
- `spacing?: SeparatorSpacing`

### EmptyState
"Nothing here yet" placeholder with icon, copy, and a CTA slot.

**Props:** `EmptyStateProps`
- `type?: 'separate' | 'attached' | 'inside' = 'separate'` — `attached` removes top border (sits under a card/table); `inside` removes border + radius (lives inside another container)
- `size?: 'default' | 'small' = 'default'`
- `icon?: string | IconWithoutBackgroundProps | null = 'spa'`
- `heading?: ReactNode`
- `children?: ReactNode` — body text
- `actions?: ReactNode` — CTA slot, usually a `<Button>` or `<Link>`

```tsx
<EmptyState heading="No results" actions={<Button>Clear filters</Button>}>
Try a different search term.
</EmptyState>
```

### Utility Components

Niche helpers exported from `@tedi-design-system/react/tedi` — load on demand:

- **Affix** — sticky-position wrapper (top/bottom offset)
- **Ellipsis** — single-line truncation with tooltip on overflow
- **Print** — show/hide subtree based on `usePrint()` context (paired with `PrintingProvider`)
- **ScrollFade** — fade edges of a scrollable container as content runs off
- **ScrollVisibility** — show/hide an element based on scroll position
- **StretchContent** — fill available space inside flex/grid parents
- **HashTrigger** — react to URL hash changes (see Navigation)
- **FeedbackText**, **FormLabel**, **Field** — primitives for composing custom form controls

---

# Community Components
Expand All @@ -448,19 +581,13 @@ Import from `@tedi-design-system/react/community`. These are community-contribut

### Check (Checkbox) — **DEPRECATED** (use TEDI-Ready Checkbox)
### Radio — **DEPRECATED** (use TEDI-Ready Radio via ChoiceGroup)
### Toggle — **DEPRECATED** (use TEDI-Ready Toggle)
### ChoiceGroup — **DEPRECATED** (use TEDI-Ready ChoiceGroup)

### Select
- `id: string`, `options`, `value?`, `defaultValue?`, `onChange?`
- `multiple?: boolean`, `async?: boolean`, `isSearchable?: boolean`, `isClearable?: boolean`

### Toggle
- `ariaLabel: string`, `label?`, `checked?`, `defaultChecked?`, `onChange?`
- `size?: 'medium' | 'large'`, `color?: 'default' | 'alternative'`, `icon?`, `disabled?`

### ChoiceGroup
- `id: string`, `items: ChoiceGroupItemProps[]`, `inputType?: 'radio' | 'checkbox'`
- `type?: 'light' | 'selector' | 'filter' | 'default'`, `value?`, `onChange?`

### FileUpload
- `id: string`, `name: string`, `accept?`, `multiple?`, `maxSize?`
- `files?`, `defaultFiles?`, `onChange?`, `onDelete?`
Expand All @@ -483,9 +610,8 @@ Import from `@tedi-design-system/react/community`. These are community-contribut
- `activeStep?`, `defaultActiveStep?: number`, `onActiveStepChange?`
- `allowStepLabelClick?: boolean`, `ariaLabel: string`, `card?: CardProps | boolean`

### Tabs
- `currentTab?: string`, `defaultCurrentTab?`, `onTabChange?`
- Sub-components: Tabs.Nav, Tabs.NavItem, Tabs.Item
### Tabs — **DEPRECATED** (use TEDI-Ready Tabs)
Legacy variant with `Tabs.Nav`, `Tabs.NavItem`, `Tabs.Item`. New code should use the TEDI-Ready `Tabs` component (different API: `Tabs.List` / `Tabs.Trigger` / `Tabs.Content`).

### TableOfContents
- `items: TableOfContentsItemProps[]`, `heading?`, `open?`, `defaultOpen?`
Expand Down
15 changes: 13 additions & 2 deletions skills/tedi-react/references/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,28 @@ import { Checkbox, Radio, ChoiceGroup } from '@tedi-design-system/react/tedi';
onChange={setSize}
/>

// Segmented choice group
// Segmented choice group (single visually-merged control)
<ChoiceGroup
id="view"
name="view"
label="View"
variant="segmented"
layout="segmented"
items={viewOptions}
value={view}
onChange={setView}
/>

// Card-style choices (each item rendered as a selectable card)
<ChoiceGroup
id="plan"
name="plan"
label="Plan"
variant="card"
layout="separated"
items={planOptions}
value={plan}
onChange={setPlan}
/>
Comment on lines +154 to +164
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ChoiceGroup supports variant="card" and layout="separated", and check if both props should be used together

# Search for ChoiceGroup prop type definitions
ast-grep --pattern $'interface $PROPS {
  $$$
  variant$_: $_
  $$$
  layout$_: $_
  $$$
}'

# Search for variant and layout prop usage patterns in ChoiceGroup
rg -nP --type=tsx --type=ts 'variant.*card|layout.*separated' -C5 -g '*ChoiceGroup*'

# Check if there are examples or tests showing variant + layout together
rg -nP --type=tsx --type=ts 'variant=.*card.*layout=|layout=.*separated.*variant=' -C3

Repository: TEDI-Design-System/react

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Find ChoiceGroup component files
find . -type f -name "*hoice*roup*" | head -20

Repository: TEDI-Design-System/react

Length of output: 1333


🏁 Script executed:

#!/bin/bash
# Search for ChoiceGroup component definition in .ts, .tsx files using proper syntax
rg "export.*ChoiceGroup|interface.*ChoiceGroup.*Props|type.*ChoiceGroup.*Props" -t ts -A 10 | head -50

Repository: TEDI-Design-System/react

Length of output: 5173


🏁 Script executed:

#!/bin/bash
# Look for ChoiceGroup implementation and prop definitions
rg "variant.*card|layout.*separated" -t ts -B 2 -A 2 | head -80

Repository: TEDI-Design-System/react

Length of output: 8028


🏁 Script executed:

#!/bin/bash
# Check the ChoiceGroup props interface
cat src/tedi/components/form/choice-group/choice-group.types.ts

Repository: TEDI-Design-System/react

Length of output: 430


🏁 Script executed:

#!/bin/bash
# Check the ChoiceGroup component implementation - check for value/onChange and defaultValue
head -100 src/tedi/components/form/choice-group/choice-group.tsx

Repository: TEDI-Design-System/react

Length of output: 3759


🏁 Script executed:

#!/bin/bash
# Check the coding guidelines and best practices referenced in CLAUDE.md
cat .claude/skills/contributing/references/best-practices.md | grep -A 20 "form control"

Repository: TEDI-Design-System/react

Length of output: 647


🏁 Script executed:

#!/bin/bash
# Check the forms.md file to see the segmented example and understand the pattern
head -200 skills/tedi-react/references/forms.md | tail -100

Repository: TEDI-Design-System/react

Length of output: 1917


🏁 Script executed:

#!/bin/bash
# Look at the full context around lines 154-164 in forms.md
sed -n '130,180p' skills/tedi-react/references/forms.md

Repository: TEDI-Design-System/react

Length of output: 1053


🏁 Script executed:

#!/bin/bash
# Check the beginning of forms.md for any uncontrolled examples mentioned
head -60 skills/tedi-react/references/forms.md

Repository: TEDI-Design-System/react

Length of output: 1901


🏁 Script executed:

#!/bin/bash
# Look for defaultValue in the entire forms.md file
rg "defaultValue" skills/tedi-react/references/forms.md -B 3 -A 3

Repository: TEDI-Design-System/react

Length of output: 376


Add an uncontrolled example for the card-style ChoiceGroup.

This example only demonstrates controlled mode (value + onChange). Per the documented rule (line 31: "Every form component must accept both value (controlled) and defaultValue (uncontrolled)"), include an uncontrolled variant:

// Uncontrolled — component manages state internally
<ChoiceGroup
  id="plan"
  name="plan"
  label="Plan"
  variant="card"
  layout="separated"
  items={planOptions}
  defaultValue="basic"
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/tedi-react/references/forms.md` around lines 154 - 164, Add an
uncontrolled example of the card-style ChoiceGroup: duplicate the existing card
example that currently uses ChoiceGroup with props
id/name/label/variant/layout/items/value={plan}/onChange={setPlan} and provide
an uncontrolled variant that uses defaultValue (e.g., defaultValue="basic")
instead of value and onChange so the component manages its own state; reference
ChoiceGroup, planOptions, and remove value={plan}/onChange={setPlan} in the
uncontrolled snippet.

```

## Validation & Helper Text
Expand Down
Loading