AgentStack
SKILL verified MIT Self-run

Wind Ui

skill-fluttersdk-wind-wind-ui · by fluttersdk

fluttersdk_wind 1.1: utility-first Flutter styling with Tailwind-syntax className strings. 27 public widgets (WDiv, WText, WButton, WInput, WSelect, WCheckbox, WDatePicker, WPopover, WAnchor, WIcon, WImage, WSvg, WSpacer, WBreakpoint, WDynamic, WKeyboardActions, WindAnimationWrapper, WBadge, WCard, WSwitch, WRadio, WTabs + 5 WForm* wrappers) consume className through a 20-parser pipeline (20 impl…

No reviews yet
0 installs
12 views
0.0% view→install

Install

$ agentstack add skill-fluttersdk-wind-wind-ui

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Wind Ui? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Wind UI 1.1

Utility-first Flutter styling. Every visual decision lives in a className: String? parsed at build time into an immutable WindStyle and composed into a native Flutter widget tree. Tailwind syntax (flex, p-4, dark:bg-gray-800, hover:shadow-lg), Flutter physics.

This skill assumes the host app already depends on fluttersdk_wind and has WindTheme wrapping MaterialApp. If a fresh project needs setup (rare; the skill normally triggers on an already-installed project), see [Quick install](#13-quick-install) at the bottom.

0. Before writing UI in this project

Three quick checks. Each pays off across the whole session.

  1. Confirm Wind is installed. Look at pubspec.yaml for fluttersdk_wind:. If absent, jump to §13 first.
  2. Read the project's WindThemeData setup. Usually in lib/main.dart or lib/config/wind.dart. Note any custom color families (primary, accent, incident, etc.): those become available as bg-primary-500, text-incident-700, etc. without registration. Skip this and the agent risks writing tokens that silently no-op, or missing the brand palette entirely.
  3. Scan one existing view in lib/ for the project's className idioms. Triple-quoted style? Single-line preferred? Custom states (pressed:, expanded:)? Match the surrounding code, don't invent a new dialect.

After these, the agent has the project's color landscape, breakpoint set, and className voice loaded.

The parser cache is near-100% hit-rate in production. Do not worry about className parse overhead; the same className parses exactly once for its (breakpoint, brightness, platform, states) tuple. Prefer expressive className over inline BoxDecoration / EdgeInsets for "performance" reasons; the cache handles it.

1. Core Laws

These hold for every line of Wind code. Apply each as a hard constraint, not a suggestion.

  1. className is the styling surface. Inline Dart props (backgroundColor on WDiv, foregroundColor on WText) exist only as runtime-dynamic escape hatches for values the cache key cannot represent. Default to className. Never reach for BoxDecoration, EdgeInsets, TextStyle when a token covers it.
  1. Every bg- / text- / border- / ring- / shadow- / fill- carries a dark: peer in the same className. Missing pair is a bug, not a style choice. Pair bg-white dark:bg-gray-800 on the same line, not at the top and bottom of a multi-line className. Wind's dark-mode contract: the agent never opts in; every color opts in by default.
  1. Conditional styling routes through states: Set? plus prefixed classes. Never interpolate Dart expressions into className. 'bg-${isOn ? "blue" : "gray"}-500' breaks the parser cache and is a bug. The right shape:

``dart WDiv( className: ''' rounded-lg p-4 border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 selected:border-blue-500 selected:bg-blue-50 dark:selected:border-blue-400 dark:selected:bg-blue-950 ''', states: isSelected ? const {'selected'} : const {}, child: ..., ); ``

  1. Inside a Row (flex flex-row), prefer flex-1 for a fill-the-row child. A bare w-full on a direct Row child is now treated as flex-1 (the row wraps it in Expanded), so it fills the available width instead of asserting RenderBox was not laid out; flex-1 stays the idiomatic, explicit choice and is what to reach for. (A prefixed md:w-full is NOT auto-expanded; use md:flex-1.) Inside a Column (flex flex-col), scrollable children use flex-1 overflow-y-auto plus the constructor prop scrollPrimary: true for iOS tap-to-top. h-full inside a scrollable parent still triggers "Vertical viewport was given unbounded height".
  1. child XOR children on every W-widget that accepts both. Passing both fails an assertion at construction. Passing neither renders an empty SizedBox.
  1. Unknown tokens fail silently. text-7xl (Wind stops at text-6xl), flex-cow (typo), ps-4 (logical-inline, unsupported), -m-4 (negative margin, unsupported) all parse to nothing. The parser drops them without warning. Spell-check by hand or load references/tokens.md to verify the family.
  1. Last class wins within a parser family. p-4 p-8 resolves to p-8. bg-red-500 bg-blue-500 resolves to bg-blue-500. Conflicts inside the same property are stable but silent; conflicts across properties (text-red-500 color + text-center alignment) coexist because they target different fields.
  1. WindTheme lives BELOW MaterialApp in the runtime tree. The builder pattern inverts apparent order: WindTheme(data: ..., builder: (ctx, controller) => MaterialApp(...)). Consequence: OverlayEntry.builder contexts cannot reach WindTheme via ancestor walk. Capture the State's context before showing an overlay, then pass it to WindParser.parse from inside the overlay builder. WPopover / WSelect already handle this internally.
  1. Wind composes with Flutter, not against it. Scaffold, AppBar, Dialog, BottomSheet, Drawer, SnackBar, Navigator, Hero, FutureBuilder, StreamBuilder, ValueListenableBuilder remain canonical. ListView / GridView.builder / CustomScrollView are the right choice for virtualised lists; WDiv with grid-cols-N produces a static Wrap (or, with items-stretch, equal-height rows), not a virtualised grid. See [Wind ≠ Flutter rules of thumb](#9-wind--flutter-rules-of-thumb).
  1. active: prefix is reserved but not wired. WAnchor tracks hover and focus only; there is no onTapDown/onTapUp tracking. Don't rely on active:bg-blue-700 for press feedback. Use a transient state in the consumer's controller and states: {'pressed'} if you genuinely need press feedback today.

2. The 27 public widgets (+ WindRecipe) at a glance

fluttersdk_wind v1 ships 27 public widgets plus the WindRecipe / WindSlotRecipe variant-composition primitives, all imported from the single barrel package:fluttersdk_wind/fluttersdk_wind.dart. No sub-barrels exist; do not write import 'package:fluttersdk_wind/widgets.dart'.

The headline 25 (table below) are the ones an agent reaches for daily. Two more cover narrow surfaces and live outside the table: WKeyboardActions (iOS keyboard toolbar overlay for Done / Next actions on a focused TextField) and WindAnimationWrapper (the internal stateful wrapper that drives looping animate-* tokens; consumers normally do not instantiate it directly).

| Widget | Category | Required positional | One-line purpose | |---|---|---|---| | WDiv | Layout / container | none | Universal container; auto-wraps in WAnchor when className contains hover: / focus: / active:. child XOR children. Inline color prop: backgroundColor. | | WSpacer | Layout | none | Lightweight SizedBox that reads only w-N / h-N. Skips every other token. | | WBreakpoint | Structural | none | Per-breakpoint WidgetBuilder map (base, sm, md, lg, xl, xxl, plus theme-defined custom keys). Escape hatch when className prefixes are not enough. | | WText | Display | data: String | Typography; supports selectable prop. Inline color prop: foregroundColor. No child / children. | | WIcon | Display | icon: IconData | Material icons; use Icons.*_outlined variants by convention. Reads text-* for size AND color (overloaded). Inherits from DefaultTextStyle when className is absent. Inline color prop: foregroundColor. | | WImage | Display | none (requires src or image) | Network (URL) or asset (prefix asset://path) or ImageProvider. object-cover default. | | WSvg / WSvg.string | Display | src / svg | Vector graphics. fill-* / stroke-* for color. preserve-colors token disables tint for multi-color SVGs (QR codes, logos). | | WAnchor | Interactive | child: Widget | Low-level gesture + focus + hover propagator. Emits Semantics(button: true). | | WButton | Interactive | child: Widget | Wraps WAnchor + WDiv + built-in spinner. isLoading: true injects loading: state. disabled: true injects disabled: state and blocks taps. | | WPopover | Overlay | none (requires builders) | OverlayPortal-based; triggerBuilder(ctx, isOpen, isHovering) + contentBuilder(ctx, close) + optional PopoverController. Auto-flips alignment when bottom space is insufficient. | | WInput | Form (raw) | none | Material-free text input (EditableText core); works under Material, Cupertino, custom, or bare WidgetsApp (no Material ancestor required). value + onChanged for controlled binding, or controller for imperative needs; passing both throws AssertionError in debug. InputType enum: text / password / email / number / multiline (number restricts to a signed decimal on every platform incl. web; pass inputFormatters to override). readOnly: true activates a readonly: state like enabled: false activates disabled:. Native text selection: mouse-drag selects a substring, double-click/double-tap selects a word, tapping the box moves the cursor; selection handles are Cupertino-style on all platforms (keeps WInput cupertino-only, no material.dart import). An Overlay ancestor is required for interactive selection; without one, typing and focus still work but all interactive selection (drag-select, double-tap, long-press, handles, and toolbar) is suppressed. Emits exactly one typeable textbox semantics node carrying semanticLabel ?? placeholder; password reports obscured. | | WCheckbox | Form (raw) | none | Boolean checkbox; auto-injects checked: state when value: true. Default className includes checked:bg-primary (requires a primary color in theme). | | WSelect | Form (raw) | options: List> | Single OR multi-select dropdown with overlay. Supports searchable, async search, async create (tagging), pagination via onLoadMore + hasMore. Auto-flips upward when bottom space ; auto-injects error: when validation fails; renders label / hint / error around WInput. | | WFormSelect | Form (FormField) | options: List> | extends FormField; single-select with validation. | | WFormMultiSelect | Form (FormField) | options: List> | extends FormField>; multi-select with validation; validator inspects the full list. | | WFormCheckbox | Form (FormField) | none | extends FormField; validation hook + label + error display. | | WFormDatePicker | Form (FormField) | none | extends FormField. Range mode stores range.start only in FormFieldState; validators only see the start date. | | WDynamic | Structural / SSR | json: Map | Renders a JSON node tree into Wind widgets. 13 Wind types + 16 Flutter core types allowed by default; builders: adds custom types; denyWidgets: blocks. Max recursion depth default 50. | | WBadge | Display | label: String | Inline status/label pill. Composes a rounded-full WDiv around WText(text-xs). All tone via className (bg-, text-, dark: pairs). No positional child; label is the only positional param. | | WCard | Layout / container | child: Widget | Surface container. header:, child: (required), footer: slots; delegates to WDiv(flex-col). No colors baked in; all tone via className. | | WSwitch | Form (raw) | none | Controlled toggle. value + onChanged. className styles the track; thumbClassName styles the indicator dot. checked: state activates when value: true. The thumb is a flex child of the track, so it slides via justify-start -> checked:justify-end on the track className (Wind has no transform parser; translate-x-* is a no-op). disabled: when disabled: true. | | WRadio | Form (raw) | none | Controlled radio. value, groupValue, onChanged. selected: activates when value == groupValue. Outer ring: className. Inner dot: indicatorClassName (defaults to blue filled circle). Group exclusivity is the caller's responsibility. | | WTabs | Form (raw) / Layout | none | Controlled tabs. tabs: List, selectedIndex, panelBuilder. selected: activates on the active tab. Slot classNames: listClassName, tabClassName, selectedTabClassName, panelClassName. fullWidthList (default true) prepends w-full so a border-b underline spans the container; set false` for content-width / pill tabs. |

Full constructor surface, every named parameter, every default: ${CLAUDE_SKILL_DIR}/references/widgets.md.

WindRecipe / WindSlotRecipe (variant-composition primitives)

WindRecipe and WindSlotRecipe are callable objects (not widgets) that compose className strings from variant axes. Use them to centralise multi-variant component styling instead of scattering conditional className strings across the call sites.

final button = WindRecipe(
  base: 'inline-flex items-center rounded-lg font-medium',
  variants: {
    'intent': {'primary': 'bg-blue-600 dark:bg-blue-500 text-white', 'ghost': 'bg-transparent text-blue-600 dark:text-blue-400'},
    'size':   {'sm': 'px-3 py-1.5 text-sm', 'md': 'px-4 py-2 text-base', 'lg': 'px-6 py-3 text-lg'},
  },
  compoundVariants: [
    WindCompoundVariant(conditions: {'intent': 'primary', 'size': 'lg'}, className: 'shadow-lg'),
  ],
  defaultVariants: {'intent': 'primary', 'size': 'md'},
);

button()                                       // uses defaults
button(variants: {'intent': 'ghost'})          // override one axis
button(variants: {'size': null})               // null clears the default (no size classes)
button(className: 'w-full')                    // caller suffix, appended last

Resolution order (strict, never sorted): base ++ variant-classes(definition order) ++ matched-compound(array order) ++ caller.

Same-granularity contract: a variant must override a base token at the same granularity. Mixing p-4 (base) and px-2 (variant) is silent: the parser's per-family last-wins keeps the shorthand. Override px-* with px-* or p-* with p-*.

WindSlotRecipe returns Map (slot -> className). Use the slot keys directly on widget className: props.

Core Law addition for recipes: pass enum variant values as .name (ButtonIntent.ghost.name); recipe axes are plain strings.

3. The state system (three layers)

| Layer | Set by | Examples | |---|---|---| | Automatic | WAnchor from pointer / keyboard events | hover: (MouseRegion onEnter/onExit), focus: (FocusNode listener) | | Framework-managed | The widget itself, from its own props | loading: (WButton.isLoading), disabled: (any widget's disabled / enabled prop), checked: (WCheckbox.value, WSwitch.value), selected: (WRadio when value==groupValue, WTabs active tab), error: (WForm* when FormFieldState.hasError) | | Consumer-passed | states: Set? on the widget | selected: (card toggles), highlighted:, new:, any custom string. No registration required. |

WDiv and WButton auto-wrap themselves in WAnchor whenever className contains the literal substrings hover:, focus:, or active:. Other widgets do not. If you need hover detection on a WText, wrap it in WDiv or WAnchor explicitly.

All active states merge into a single Set inside WindContext.activeStates and contribute to the parser cache key. Manual states: {'hover'} and a real pointer hover produce the same cache entry by design.

Combining prefixes:

md:hover:bg-blue-500          // breakpoint AND hover
dark:md:hover:bg-blue-400     // dark AND breakpoint AND hover
ios:focus:ring-blue-500       // iOS only AND focused
md:dark:selected:border-2     // breakpoint AND dark AND selected

Prefix order does not matter at the parser level; all stacked prefixes must match for the class to activate.

4. The token landscape (high-frequency subset)

Inline this catalog as your default reach-for set. For the full per-parser regex catalog (every flag, every arbitrary-value pattern): `${CLAUDE_SK

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.