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
71 changes: 71 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Wind UI — Tailwind-Inspired Flutter Styling Framework

Utility-first Flutter UI plugin. Translates `className` strings (Tailwind syntax) into Flutter widget trees via modular parsing architecture.

**Dart:** >=3.4.0 · **Flutter:** >=3.27.0

## Architecture

```
lib/src/
├── widgets/ # 20 W-prefix widgets (WDiv, WText, WButton, WSvg, WDynamic...)
├── parser/
│ ├── wind_parser.dart # Orchestrator — routes tokens to 17 parsers
│ ├── wind_style.dart # Immutable style value object (parse output)
│ ├── wind_context.dart # Theme + breakpoint + brightness + platform + states
│ └── parsers/ # 17 domain parsers (bg, border, flex, text, shadow...)
├── theme/
│ ├── wind_theme.dart # WindTheme widget + WindThemeController
│ ├── wind_theme_data.dart # Config: colors, screens, spacing, fonts
│ └── defaults/ # 16 default token scales
├── dynamic/ # WDynamic — JSON → widget tree (server-driven UI)
├── state/ # WindAnchorStateProvider (hover/focus/press via InheritedWidget)
└── utils/ # Extensions, helpers, color utils, logger
```

**Data flow:** `className` → WindParser.parse() → 17 parsers (first-match-wins) → WindStyle → Widget.build()

**Cache key:** className + breakpoint + brightness + platform + sorted states

## Key Conventions

- All widgets use `W` prefix: `WDiv`, `WButton`, `WText`, `WFormInput`, `WSvg`
- `className` is the primary styling API — takes precedence over explicit style properties
- `child` XOR `children` — never both
- Last class wins — later classes override earlier ones for same property
- Spacing scale: N * 4px (`p-4` = 16px, `gap-2` = 8px)
- Arbitrary values: bracket syntax `w-[200px]`, `text-[#FF0000]`
- TDD — failing test first, red-green-refactor
- Zero tolerance — linter zero warnings, no suppressions

## Key Gotchas

| Mistake | Fix |
|---------|-----|
| `className: 'flex-wrap'` | Use `wrap gap-2` — flex-wrap is a no-op |
| `WDiv(child: x, children: [...])` | `child` XOR `children`, never both |
| `overflow-y-auto` without `scrollPrimary: true` | Add it for iOS tap-to-top |
| `w-full` inside Row/flex-row | Use `flex-1` — w-full causes overflow |
| No `dark:` variant on colors | **Always** pair: `bg-white dark:bg-gray-800` |
| `WIcon(Icons.settings)` | Use `Icons.settings_outlined` — outlined only |
| className typo | Fails silently — parser ignores unknown tokens |
| `h-full` inside scrollable parent | Use `min-h-screen` — h-full = infinite height error |
| Forgetting `WindParser.clearCache()` in tests | Cache persists between tests |

## Post-Change Checklist

After ANY source code change, sync before committing:

1. **`doc/`** — Update relevant documentation files
2. **`skills/wind-ui/`** — Update SKILL.md and references if API/widget changes
3. **`example/lib/pages/`** — Update or create demo pages
4. **`CHANGELOG.md`** — Add entry under `[Unreleased]`
5. **`README.md`** — Update if new widgets, features, or API changes

## Parser Development

1. Find parser in `lib/src/parser/parsers/` (or create new one implementing `WindParserInterface`)
2. Add property to `WindStyle` if needed (immutable — use `copyWith`)
3. Write failing test first in `test/parser/parsers/{name}_parser_test.dart`
4. Implement in parser
5. Run post-change checklist
28 changes: 28 additions & 0 deletions .github/instructions/docs.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: 'Documentation Conventions'
description: 'Formatting rules for Wind framework documentation files in doc/'
applyTo: 'doc/**/*.md'
---

# Documentation Domain

- One `#` title per file — widget name or concept name. One-line description immediately after
- Table of Contents with `[Section Name](#section-name)` links after description
- `<x-preview path="category/file" size="md" source="example/lib/pages/category/file.dart"></x-preview>` for live demos
- Section anchors: `<a name="section-name"></a>` before each `##` heading
- Code blocks always use `dart` language specifier
- Props table format — left-aligned columns, backtick all code:
```
| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `className` | `String?` | `null` | Wind utility classes. |
```
- Required props show `**Required**` in Default column
- Constructor section shows full signature with defaults
- Heading hierarchy: `#` page title → `##` main sections → `###` subsections. Never skip levels
- x-preview `path` matches `example/lib/pages/{path}.dart` without extension
- x-preview `size`: `sm` (compact), `md` (standard), `lg` (full-width)
- Keep code examples short, realistic, and copy-pasteable
- Related docs at bottom: `- [Widget Name](../widgets/widget-name.md)`
- Do NOT restructure sections that haven't changed — preserve existing format exactly
- When adding new sections, match the style of adjacent sections in the same file
22 changes: 22 additions & 0 deletions .github/instructions/example-pages.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: 'Example Page Conventions'
description: 'Structure and patterns for Wind framework demo pages in example/lib/pages/'
applyTo: 'example/lib/pages/**/*.dart'
---

# Example Pages Domain

- File name: `{feature_name}.dart` (snake_case). Class: `{FeatureName}ExamplePage`
- Extend `StatefulWidget` for interactive demos (state toggles, counters, loading simulation)
- Root widget: `WDiv(className: 'w-full h-full overflow-y-auto p-4', child: ...)` — always scrollable
- Content wrapper: `WDiv(className: 'flex flex-col gap-6', children: [_buildHeader(), ...sections])`
- Header: gradient WDiv with title (text-lg font-bold text-white) + description (text-sm)
- Use `_buildSection({title, description, children})` helper for consistent section layout
- Section title: `text-lg font-semibold text-gray-900 dark:text-white`
- Section description: `text-sm text-gray-600 dark:text-gray-400 mb-4`
- Always include dark mode variants in all className strings
- Show multiple variants of the component: basic, styled, states (hover, disabled, loading), responsive
- Include interactive state demos: `setState(() => _isLoading = !_isLoading)`
- Use realistic content — names, emails, descriptions — not "Lorem ipsum"
- Each page demonstrates ONE widget or concept thoroughly, not multiple
- These pages are referenced by `doc/` via `<x-preview>` — keep file paths stable
22 changes: 22 additions & 0 deletions .github/instructions/parsers.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: 'Parser Conventions'
description: 'Implementation patterns for Wind className parsers'
applyTo: 'lib/src/parser/**/*.dart'
---

# Parser Domain

- Every parser implements `WindParserInterface` with exactly two methods: `canParse()` and `parse()`
- `canParse()` must be O(1) — use `startsWith()` or pre-compiled `static final RegExp`. No heavy logic
- `parse()` iterates classes in **reverse** (last class wins semantics). Forward iteration is a bug
- Return `styles.copyWith(...)` from parse() — never return null, never mutate input
- If `classes == null`, return `styles` unchanged immediately
- Use named RegExp capture groups: `(?<root>p|pt|pr|pb|pl|px|py)` — not positional groups
- Prefix stripping (`hover:`, `dark:`, `md:`) happens in WindParser before delegation — parsers never see prefixes
- First-match-wins routing: WindParser checks `canParse()` across all parsers — first `true` wins. Order matters
- One parser per file in `parsers/`. File name matches domain: `padding_parser.dart`, `border_parser.dart`
- Register new parsers in `WindParser._parserMap` — key is descriptive string, value is const instance
- `WindStyle` is immutable — properties are nullable. Merge with existing: `pTop ?? styles.padding?.top ?? 0`
- Theme value resolution via `context.theme.getSpacing()`, `context.theme.getColor()` — never hardcode values
- Arbitrary values use `[...]` bracket syntax: `p-[10px]`, `bg-[#FF5733]`. Parse brackets before theme lookup
- Cache key = className + breakpoint + brightness + platform + sorted states. Call `WindParser.clearCache()` in tests
21 changes: 21 additions & 0 deletions .github/instructions/tests.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: 'Testing Conventions'
description: 'Test structure, helpers, and patterns for Wind framework tests'
applyTo: 'test/**/*.dart'
---

# Testing Domain

- Test structure mirrors `lib/src/` exactly: `test/parser/parsers/`, `test/widgets/`, `test/theme/`, `test/dynamic/`
- Every Wind widget test needs `wrapWithTheme()` helper — wraps in `MaterialApp > WindTheme > Scaffold`
- Parser tests use `createTestContext()` helper with named params: `brightness`, `activeBreakpoint`, `isHovering`, etc.
- Always `WindParser.clearCache()` in `setUp()` — cache persists between tests and causes false positives
- Use `group()` for logical grouping by feature, `testWidgets()` for widget tests, `test()` for pure logic
- Always `await tester.pumpWidget()`, `await tester.tap()`, `await tester.pump()` — missing await = flaky test
- Use `pumpAndSettle()` only for animations, `pump()` for single-frame rebuilds
- Parser tests: initialize parser + context in `setUp()` with `late` keyword
- Widget tests: test behavior (taps, state changes), not implementation details
- Expect patterns: `findsOneWidget`, `findsNothing`, `findsWidgets`, `isTrue`, `isA<Type>()`
- Test both theme-scale values (`p-4`) and arbitrary values (`p-[10px]`) for parser coverage
- Test dark mode: pass `brightness: Brightness.dark` to `createTestContext()`
- Test edge cases: null classes, empty string, conflicting classes (last wins), unknown tokens (ignored)
22 changes: 22 additions & 0 deletions .github/instructions/widgets.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: 'Widget Conventions'
description: 'W-prefix widget hierarchy, constructor patterns, and state handling'
applyTo: 'lib/src/widgets/**/*.dart'
---

# Widget Domain

- All widgets use `W` prefix: `WDiv`, `WButton`, `WText`, `WFormInput`, `WSvg`
- Form-integrated variants: `WForm{Feature}` (WFormInput, WFormSelect, WFormCheckbox, WFormDatePicker)
- Always `const` constructor with `super.key` first, required params next, optional last, trailing commas
- One class per file named after the widget: `w_button.dart` → `WButton` + `_WButtonState`
- `className` is the primary styling API — it takes precedence over any explicit style properties
- Widget build flow: parse className → detect displayType (flex/grid/block) → build minimal widget tree
- Use `WindParser.parse(className, WindContext.of(context), states: states)` — always with context
- WAnchor is required for `hover:`, `focus:`, `active:` states — WDiv auto-wraps if these prefixes detected
- `child` XOR `children` — never both. `child` for single content, `children` for flex/grid layouts
- Loading state (`isLoading: true`) disables all callbacks and activates `loading:` prefixed classes
- Disabled state (`disabled: true`) activates `disabled:` prefixed classes
- Custom states via `Set<String>? states` parameter — used with matching prefixes like `selected:`, `active:`
- Never hardcode colors or sizes — resolve through className or theme
- DartDoc: `/// **The Utility-First [Name]**` header, then `### Supported Features:` and `### Example Usage:` sections
41 changes: 41 additions & 0 deletions .github/scripts/sync-cc-to-copilot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Sync Claude Code rules (.claude/rules/) → GitHub Copilot instructions (.github/instructions/)
# Run from project root: bash .github/scripts/sync-cc-to-copilot.sh

set -euo pipefail

RULES_DIR=".claude/rules"
INSTRUCTIONS_DIR=".github/instructions"

mkdir -p "$INSTRUCTIONS_DIR"

for rule in "$RULES_DIR"/*.md; do
[ -f "$rule" ] || continue

name=$(basename "$rule" .md)
target="$INSTRUCTIONS_DIR/${name}.instructions.md"

# Extract path: from frontmatter
path_glob=$(sed -n '/^---$/,/^---$/{ /^path:/{ s/^path: *"*\(.*\)"*/\1/; p; } }' "$rule")

# Extract body (everything after second ---)
body=$(awk 'BEGIN{c=0} /^---$/{c++; next} c>=2{print}' "$rule")

# Generate human-readable name from filename
display_name=$(echo "$name" | sed 's/-/ /g; s/\b\(.\)/\u\1/g')

# Write Copilot instruction file
cat > "$target" <<COPILOT_EOF
---
name: '${display_name} Conventions'
description: 'Conventions for ${name} domain'
applyTo: '${path_glob}'
---

${body}
COPILOT_EOF

echo " synced: $rule → $target"
done

echo "Done. ${INSTRUCTIONS_DIR}/ is up to date."
Loading