diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e5f874b --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 diff --git a/.github/instructions/docs.instructions.md b/.github/instructions/docs.instructions.md new file mode 100644 index 0000000..5ae8705 --- /dev/null +++ b/.github/instructions/docs.instructions.md @@ -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 +- `` for live demos +- Section anchors: `` 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 diff --git a/.github/instructions/example-pages.instructions.md b/.github/instructions/example-pages.instructions.md new file mode 100644 index 0000000..37caff1 --- /dev/null +++ b/.github/instructions/example-pages.instructions.md @@ -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 `` — keep file paths stable diff --git a/.github/instructions/parsers.instructions.md b/.github/instructions/parsers.instructions.md new file mode 100644 index 0000000..e47ce26 --- /dev/null +++ b/.github/instructions/parsers.instructions.md @@ -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: `(?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 diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 0000000..b65d689 --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -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()` +- 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) diff --git a/.github/instructions/widgets.instructions.md b/.github/instructions/widgets.instructions.md new file mode 100644 index 0000000..c7fae72 --- /dev/null +++ b/.github/instructions/widgets.instructions.md @@ -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? 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 diff --git a/.github/scripts/sync-cc-to-copilot.sh b/.github/scripts/sync-cc-to-copilot.sh new file mode 100755 index 0000000..0fcf005 --- /dev/null +++ b/.github/scripts/sync-cc-to-copilot.sh @@ -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" <