alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
315 lines (230 loc) • 9.32 kB
Markdown
---
name: alouette-data
description: >
Data-display components. Badge: a small pill label for status, counts or
categories. accent defaults to brand, size is sm/md, variant is
solid (tinted) / solid.enabled (filled) / outlined; optional icon takes a
rendered icon element and is auto-sized. Badge has no className prop and is
not pressable. EditableItem: a labelled row (bold label + summary node +
optional details/children) with a pencil IconButton; it owns no editor and
calls onEdit, editAriaLabel is required. Bullet: an icon + text list row, the
icon tinted with the current accent. Load when labelling an item with a
status, count, tag or category chip, when listing points with an icon, or when
showing a value with an edit affordance.
type: core
library: alouette
library_version: "22.4.0"
requires:
- alouette-theming
- alouette-actions
sources:
- "christophehurpeau/alouette:packages/alouette/src/ui/data/Badge.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/data/Badge.stories.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/data/EditableItem.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/data/EditableItem.stories.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/data/Bullet.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/data/Bullet.stories.tsx"
---
This skill builds on alouette-theming. Read it first for the accent model.
# alouette — Data display
`Badge` is a small, self-sizing pill that labels something: a status, a count, a
category. It is display-only — it renders no press handling and no accessible
role.
## Setup
```tsx
import { Badge } from "alouette";
<Badge>New</Badge>;
```
## Core Patterns
### Accent
`accent` defaults to `"brand"` and drives the whole badge — it wraps its content
in `AccentScope`, so the background, border and text all resolve from the accent
theme in the current light/dark mode.
```tsx
<Badge accent="success">Paid</Badge>
<Badge accent="danger">Overdue</Badge>
<Badge accent="info">Beta</Badge>
```
### Variants
`variant` is `"solid" | "solid.enabled" | "outlined"`, default `"solid"`.
```tsx
<Badge variant="solid">Draft</Badge> {/* tinted fill, sharp text */}
<Badge variant="solid.enabled">Active</Badge> {/* full accent fill, on-accent text */}
<Badge variant="outlined">Archived</Badge> {/* accent border + accent text */}
```
`solid` is the quiet default (a tinted highlight background). Use
`solid.enabled` for the one badge that must read as switched-on, and `outlined`
where a fill would compete with the surface behind it.
### Size and icon
`size` is `"sm" | "md"` (default `"md"`) and controls padding, text size **and**
the icon size (12px / 16px). Pass `icon` as a rendered element; do not size it
yourself.
```tsx
import { StarRegularIcon } from "alouette-icons/phosphor-icons/StarRegularIcon";
<Badge size="sm" icon={<StarRegularIcon />}>
Featured
</Badge>;
```
### Placement
The badge is `self-start`, so it never stretches to fill a stack. Position it
with a wrapper — it takes no `className`.
```tsx
<HStack className="gap-xs items-center">
<Text className="text-base">Invoice #128</Text>
<Badge accent="success" size="sm">Paid</Badge>
</HStack>
```
## Bullet
`Bullet` is one row of an icon-led list: a leading `icon` tinted `text-accent`
and the text as `children`. It takes no `accent` of its own — it reads the
nearest scope, so accent a whole list by accenting its container. Stack rows in a
`VStack` and choose the gap yourself.
```tsx
import { Bullet, VStack } from "alouette";
import { CheckCircleRegularIcon } from "alouette-icons/phosphor-icons/CheckCircleRegularIcon";
<VStack className="gap-xs">
<Bullet icon={<CheckCircleRegularIcon />}>Consistent UI</Bullet>
<Bullet icon={<CheckCircleRegularIcon />}>Accessible</Bullet>
</VStack>;
```
The icon stays aligned with the **first** line (`items-start`) and the text
shrinks, so long content wraps under itself rather than pushing the icon down.
## EditableItem
`EditableItem` is the labelled row that shows a saved value and offers to edit
it: a bold `label`, a `summary` node beside it, and a pencil `IconButton` on the
right. It owns **no** editor and no state — it calls `onEdit`.
```tsx
import { EditableItem, Badge } from "alouette";
<EditableItem
label="Display name"
summary={<Badge accent="brand">Ada Lovelace</Badge>}
editAriaLabel="Edit display name"
onEdit={openEditor}
/>;
```
`summary` is any node — a `Badge` is only the most compact option; a `Text`
(sharp, muted or mono) reads better for a plain value, and omitting it leaves
the label alone. `details` adds muted helper text under the label, and
`children` render **below** the row for a value too large for `summary`.
```tsx
<EditableItem
label="Biography"
details="Shown on your public profile."
editAriaLabel="Edit biography"
onEdit={openEditor}
>
<Paragraph>Mathematician and writer…</Paragraph>
</EditableItem>
```
`editAriaLabel` is required — the button has no visible text. `editIcon`
defaults to `PencilSimpleRegularIcon`; `variant` (`contained` / `outlined` /
`ghost`), `accent` and `disabled` are forwarded to the `IconButton`.
For the usual case — the editor is a modal form — use `FormEditableItem` from
alouette-forms/SKILL.md, which adds the open state, the `Modal` and its own
`Form`. Reach for `EditableItem` directly only when the editor is not a form
(navigating to a screen, opening a picker).
## Common Mistakes
### HIGH Passing the icon as a component instead of an element
Wrong:
```tsx
<Badge icon={StarRegularIcon}>Featured</Badge>
```
Correct:
```tsx
<Badge icon={<StarRegularIcon />}>Featured</Badge>
```
`icon` is typed `SVGIconElement` — a rendered element, matching `Button` and
`Message`. A component reference is a type error and renders nothing.
Source: packages/alouette/src/ui/data/Badge.tsx; ui/primitives/Icon.tsx
### MEDIUM Passing className to Badge
Wrong:
```tsx
<Badge className="ml-xs bg-blue-500">New</Badge>
```
Correct:
```tsx
<Box className="ml-xs">
<Badge accent="info">New</Badge>
</Box>
```
`BadgeProps` is `accent | size | variant | icon | children` — there is no
`className` (nor `style`) prop. Appearance comes from `accent` + `variant`;
spacing and placement come from the parent.
Source: packages/alouette/src/ui/data/Badge.tsx
### MEDIUM Inventing variant or size values
Wrong:
```tsx
<Badge variant="filled" size="lg">Active</Badge>
```
Correct:
```tsx
<Badge variant="solid.enabled" size="md">Active</Badge>
```
`variant` is exactly `"solid" | "solid.enabled" | "outlined"` (note the dotted
name) and `size` is only `"sm" | "md"` — unlike `Message`, there is no `lg`.
Source: packages/alouette/src/ui/data/Badge.tsx
### MEDIUM Hand-rolling a pill with Box + Text
Wrong:
```tsx
<Box className="rounded-full bg-green-100 px-xs py-xxs">
<Text className="text-xs">Paid</Text>
</Box>
```
Correct:
```tsx
<Badge accent="success" size="sm">Paid</Badge>
```
A manual pill uses raw Tailwind colors instead of accent tokens, so it does not
follow light/dark mode or the accent theme, and its padding/radius drift from
the scale.
Source: packages/alouette/src/ui/data/Badge.tsx
### LOW Expecting Badge to be pressable
`Badge` renders a plain `Box` — no `onPress`, no `role`, no interactive token
states. For a tappable chip, wrap it (or build the chip) with `PressableBox`
from alouette-actions, which wires the hover/focus/press/disabled states.
Source: packages/alouette/src/ui/data/Badge.tsx; ui/actions/PressableBox.tsx
### HIGH Hand-rolling the label + value + pencil row
Wrong:
```tsx
<HStack className="items-center justify-between">
<HStack className="items-center gap-sm">
<Text className="font-body-bold text-md">Display name</Text>
<Badge>{name}</Badge>
</HStack>
<IconButton size="sm" icon={<PencilSimpleRegularIcon />} onPress={openEditor} />
</HStack>
```
Correct:
```tsx
<EditableItem
label="Display name"
summary={<Badge>{name}</Badge>}
editAriaLabel="Edit display name"
onEdit={openEditor}
/>
```
The hand-rolled row drifts on spacing and usually forgets the edit button's
`aria-label`, leaving it unnamed for assistive tech. When the editor is a form
modal, `FormEditableItem` replaces the surrounding state as well.
Source: packages/alouette/src/ui/data/EditableItem.tsx
### MEDIUM Hand-rolling a bulleted row
Wrong:
```tsx
<HStack className="gap-sm items-start">
<Icon icon={<CheckCircleRegularIcon />} className="text-accent" />
<Text className="shrink">Accessible</Text>
</HStack>
```
Correct:
```tsx
<Bullet icon={<CheckCircleRegularIcon />}>Accessible</Bullet>
```
That is exactly what `Bullet` renders. Hand-rolled rows drift on the gap and
routinely drop `shrink` on the `Text`, which stops long text from wrapping.
Source: packages/alouette/src/ui/data/Bullet.tsx
### MEDIUM Expecting EditableItem to open an editor
`EditableItem` renders no modal and holds no state — `onEdit` is yours to wire.
Nothing happens on press until you open something from it.
Source: packages/alouette/src/ui/data/EditableItem.tsx; ui/forms/FormEditableItem.tsx
See also: alouette-icons/SKILL.md for importing icon elements;
alouette-theming/SKILL.md for what each accent resolves to.