- name
- wind-ui
- description
- Build Flutter UI with Wind's utility-first className system. W-prefix widgets, design culture, enforcement gates, theming, component-first workflow.
- when_to_use
- TRIGGER: WDiv, WText, WButton, WInput, WSelect, WCheckbox, WIcon, WImage, WSvg, WPopover, WAnchor, WFormInput, WFormSelect, WFormMultiSelect, WFormCheckbox, WDatePicker, WFormDatePicker, WSpacer, WDynamic, className, WindTheme, WindThemeData, wind-ui, Flutter styling, Flutter layout, mobile UI, app design, component design.
DO NOT TRIGGER: general Flutter questions without className usage, pure Dart logic without UI, backend/API code.
- version
- 1.0.0-alpha.6
<!-- Wind UI v1.0.0-alpha.6 | Skill updated: 2026-04-16 | Last lib/ change: 2026-04-04 -->
# Wind UI v1
Utility-first styling for Flutter UI. Translate Tailwind-like classes to Flutter render tree semantics via `className` string parsing.
## 1. Core Laws
1. **W-prefix mandate**: Use `WDiv` (not Container), `WText` (not Text), `WButton` (not ElevatedButton/GestureDetector).
2. **className-first**: ALL styling via `className` string. Never inline BoxDecoration, TextStyle, or EdgeInsets when a Wind equivalent exists.
3. **dark: is mandatory**: Every `bg-`, `text-`, and `border-` class MUST have a `dark:` counterpart.
4. **Trailing commas**: Always on the last constructor parameter and list item.
5. **Multi-line**: Constructor parameters must always be multi-line when 3 or more.
6. **No theory output**: When applying styling, provide the code directly. Do not explain the classes.
## 2. Flutter Layout Reality
Flutter constraint resolution differs fundamentally from CSS. Wind UI maps `className` syntax to native Flutter semantics. Follow these rules to avoid overflows and unbounded height errors.
**Space-filling**
| Goal | Use | Why |
|------|-----|-----|
| Fill remaining space in Row/Column | `flex-1` | Maps to `Expanded`, negotiates available space |
| Full width in Column context | `w-full` | `SizedBox(width: double.infinity)`, works here |
| Full width in Row context | `flex-1` (NOT `w-full`!) | `w-full` causes `RenderFlex` overflow in Rows |
| `flex-none` / `shrink-0` | No flex shrink. Unlike CSS, Flutter Row/Column children do not auto-shrink; use `flex-1` to allow growth | Row child stays fixed width |
**Scrollable layouts**
| Goal | className | Required context |
|------|-----------|-----------------|
| Vertical scroll | `overflow-y-auto` + `scrollPrimary: true` | Parent MUST have bounded height |
| Bounded height for scroll | `flex-1` on the scrollable `WDiv` | Inside a Column/Flex parent |
| Horizontal scroll | `overflow-x-auto` | Same rules |
| Flex children inside scrollable main axis | `flex-1` on child works (Wind skips `Expanded` automatically when parent scrolls that axis) | `flex flex-row + overflow-x-auto` or `flex flex-col + overflow-y-auto` — no unbounded-constraints assertion |
**WDiv Composition Rules**
| className contains | Flutter widget | Notes |
|-------------------|----------------|-------|
| `flex flex-col` | Column | `flex` alone defaults to Row |
| `flex flex-row` or `flex` | Row | Direction default |
| `flex-row-reverse` / `flex-col-reverse` | Row/Column with flipped main axis | Uses `textDirection` / `verticalDirection` so `justify-start` mirrors (matches CSS) |
| `order-{0..12}` / `order-first` / `order-last` / `order-[n]` | Child reorder inside flex parent | Parent stable-sorts; children without `order-*` default to 0 |
| `wrap` or `grid` | Wrap | NOT `flex-wrap` (no-op!) |
| `overflow-y-auto` | SingleChildScrollView | Needs bounded height |
| `relative` | Stack(clipBehavior: Clip.none) | Children split: normal layout + Positioned |
| `absolute top-4 right-4` | Positioned(top: 16, right: 16) | Must be inside a `relative` parent |
| `absolute inset-0` | Positioned(top: 0, right: 0, bottom: 0, left: 0) | Full overlay pattern |
| `hidden` | SizedBox.shrink() | |
**Critical Layout Gotchas:**
```dart
// ❌ WRONG: overflow in Row
WDiv(
className: 'flex flex-row',
children: [
WDiv(className: 'w-full', child: WText('Long text')), // OVERFLOW
],
)
// ✅ CORRECT
WDiv(
className: 'flex flex-row',
children: [
WDiv(className: 'flex-1', child: WText('Long text')), // Fills remaining space
],
)
```
```dart
// ❌ WRONG: unbounded height scroll
Column(
children: [
WDiv(className: 'overflow-y-auto', children: [...]) // ERROR: infinite height
],
)
// ✅ CORRECT
WDiv(
className: 'flex flex-col h-full', // Bounded
children: [
WDiv(
className: 'flex-1 overflow-y-auto',
scrollPrimary: true, // iOS tap-to-top
children: [...],
),
],
)
```
```dart
// ❌ WRONG: truncate without bounded width
WDiv(
className: 'flex flex-row',
children: [
WText('Very long title', className: 'truncate'), // OVERFLOW
],
)
// ✅ CORRECT
WDiv(
className: 'flex flex-row',
children: [
WDiv(
className: 'flex-1', // Bounds the text
child: WText('Very long title', className: 'truncate'),
),
],
)
```
```dart
// ❌ WRONG: absolute without relative parent
WDiv(
className: 'flex flex-row',
children: [
WDiv(className: 'absolute top-0 right-0', child: badge), // No Stack!
],
)
// ✅ CORRECT: relative parent creates Stack
WDiv(
className: 'relative flex flex-row',
children: [
WDiv(className: 'flex-1', child: content),
WDiv(className: 'absolute top-0 right-0', child: badge), // Positioned overlay
],
)
```
**Note:** `h-full` inside a scrollable parent results in an infinite height error. Use `min-h-screen` instead. Native Flutter widgets (e.g., `ListView.builder`, charts) inside a `Row` or `Column` MUST be wrapped in `Expanded()` (allowed exception to Gate 1 for non-Wind widget bounding). `absolute` children only work inside a `relative` parent. Only `WDiv` and `WText` are detected as absolute; wrap other widgets in a `WDiv`.
## 3. Widget Quick Reference
| Widget | Required Props | Key Optional Props | Use For |
|--------|---------------|-------------------|---------|
| `WDiv` | - | `className`, `child`/`children`, `states`, `scrollPrimary` | Any container, layout |
| `WText` | `data` | `className`, `selectable`, `states` | All text rendering |
| `WButton` | `child` | `onTap`, `isLoading`, `disabled`, `className` | All interactive buttons |
| `WInput` | `value`, `onChanged`| `type`, `placeholder`, `className`, `placeholderClassName`| Standalone inputs |
| `WSelect` | `options`, `value`/`values`, `onChange` | `searchable`, `isMulti`, `onCreateOption`, `className`| Dropdowns |
| `WCheckbox`| `value`, `onChanged`| `className` | Toggle checkboxes |
| `WIcon` | `icon` | `className` | Icons with styling |
| `WImage` | `src` | `className` | Images with object-fit |
| `WSvg` | `src` | `className`, `preserve-colors` | SVG with fill/stroke/preserve |
| `WPopover` | `triggerBuilder`, `contentBuilder`| `alignment`, `className` | Dropdowns, menus, tooltips|
| `WAnchor` | `child` | `onTap`, `className` | Raw interactive wrapper |
| `WFormInput`| (FormField) | `controller`, `type`, `placeholder`, `className`, `validator` | MagicForm-integrated input|
| `WFormSelect`| (FormField) | `options`, `searchable`, `className` | MagicForm-integrated select|
| `WFormMultiSelect`| (FormField) | `options`, `onCreateOption`, `className` | MagicForm multi-select |
| `WFormCheckbox`| (FormField) | `label`, `className` | MagicForm-integrated checkbox|
| `WDatePicker`| `mode`, `onDateSelected`/`onRangeSelected` | `className`, `initialDate`, `firstDate`, `lastDate`| Single date or date range picker|
| `WFormDatePicker`| (FormField) | `mode`, `className`, `firstDate`, `lastDate` | MagicForm-integrated date picker|
| `WSpacer` | - | `className` | Semantic spacing between siblings |
| `WDynamic` | `json` | `actions`, `controller`, `customIcons`, `builders`, `denyWidgets` | Server-driven UI from JSON |
| `WBreakpoint` | `base` (builder) | `sm`, `md`, `lg`, `xl`, `xxl`, `custom` | Render a fully different widget tree per breakpoint — escape hatch when className prefixes + `hidden` aren't enough |
**Rules:**
- `child` and `children` are mutually exclusive on `WDiv`.
- `WDiv` auto-wraps in `WAnchor` if `hover:`, `focus:`, or `active:` is present in the `className`.
- `WButton` with `isLoading: true` activates `loading:` prefix classes and disables interaction.
- `WSelect` with `isMulti: true` must use `values` + `onMultiChange` instead of `value` + `onChange`.
- `WSpacer` is a `const`-friendly semantic spacer. Prefer over `SizedBox` or empty `WDiv` for gaps between siblings.
- `WDatePicker` `mode`: `DatePickerMode.single` or `DatePickerMode.range`. Range mode uses `onRangeSelected` callback.
## 4. Token System
**Spacing Formula:** `n × 4px` (e.g., `p-4` = 16px, `gap-6` = 24px, `m-2` = 8px)
**Color Syntax:** `{role}-{color}-{shade}` (e.g., `bg-blue-500`, `text-gray-900`, `border-red-300`)
- **Opacity:** `bg-primary/50`, `text-gray-500/75`
- **Arbitrary:** `bg-[#FF5733]`, `w-[200px]`, `text-[18px]`
**Responsive:** Prefix with breakpoint (e.g., `md:flex-row`, `lg:p-6`, `xl:hidden`)
- Breakpoints: `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `2xl` (1536px)
**Typography Sizes:** `xs` (12), `sm` (14), `base` (16), `lg` (18), `xl` (20), `2xl` (24), `3xl` (30), `4xl` (36)
**Border Radius:** `rounded` (4), `rounded-md` (6), `rounded-lg` (8), `rounded-xl` (12), `rounded-2xl` (16), `rounded-full` (9999)
**Position Types:** `relative` (renders Stack), `absolute` (renders Positioned inside a Stack)
**Offsets (spacing scale):** `top-{n}` `right-{n}` `bottom-{n}` `left-{n}` (e.g., `top-4` = 16px)
**Insets:** `inset-{n}` (all sides), `inset-x-{n}` (left+right), `inset-y-{n}` (top+bottom)
**Negative offsets:** `-top-{n}`, `-inset-{n}` (prefix with `-`)
**Arbitrary offsets:** `top-[24px]`, `left-[24px]` (px only; `%` is unsupported)
**Note:** `fixed` and `sticky` are not implemented. Absolute children must be inside a `relative` parent.
## 5. State & Modifier Prefixes
| Prefix | Trigger | Example |
|--------|---------|---------|
| `dark:` | Dark mode (MANDATORY on all colors) | `bg-white dark:bg-gray-800` |
GitHub에서 보기