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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.
### Changed

- Documented and test-proved the `WindRecipe`/`WindSlotRecipe` caller-`className` merge contract: the recipe only appends the caller's className last (already the behavior since introduction); the conflict with a base token is resolved one layer down, at parse time, by `WindParser`'s per-family last-wins. No twMerge/`cn` port was added or is planned. (`test/recipe/wind_recipe_test.dart`, `test/parser/parsers/sizing_parser_test.dart`, `doc/styling/wind-recipe.md`, `skills/wind-ui/SKILL.md`)
- `skills/wind-ui/references/tailwind-divergence.md`, `skills/wind-ui/SKILL.md`, and `doc/layout/display.md` reconciled to reflect the `flex-wrap` alias (no longer a listed unsupported token) and the unknown-token debug hint (no longer purely silent). Example pages under `example/lib/pages/` switched from `flex-wrap` to the canonical `wrap` token. (WIND-5)

### Added

- `flex-wrap` -> `wrap` alias: `FlexboxGridParser` now maps the Tailwind spelling `flex-wrap` directly to `WindDisplayType.wrap` (`canParse` already accepted `flex-*`; only the `parse()` handler was missing). `wrap` remains the canonical, unaliased token; `flex-wrap` prints a one-time `kDebugMode` hint suggesting it. (`lib/src/parser/parsers/flexbox_grid_parser.dart`) (WIND-5)
- Unknown-className debug warning: `WindParser.findAndGroupClasses` now emits a one-time `kDebugMode` `debugPrint` naming any className that no parser recognizes, deduped per unique token per session (mirrors the existing `_warnedAliases` shadow/cycle dedup). The token is still dropped from output; release builds print nothing. Valid Wind tokens handled outside the parser map are exempt so the warning aims only at genuine typos: the widget-consumed `object-*` fit family (read by `WImage`), the `inline-flex`/`inline-block`/`inline` display keywords (emitted by `WBadge`, inert in Flutter), and deliberately inert compat tokens that Wind's own widget docstrings / the consumer contract emit (`transition`/`transition-colors`/etc., `antialiased`, `sr-only`, the `*-nums` font-variant family incl. `tabular-nums`). (`lib/src/parser/wind_parser.dart`) (WIND-5)
- `cursor-*` utilities: a new `CursorParser` maps Tailwind cursor names (`cursor-pointer`, `cursor-not-allowed`, `cursor-text`, `cursor-grab`/`cursor-grabbing`, `cursor-zoom-in`/`cursor-zoom-out`, the full resize set, etc.) to the matching `SystemMouseCursors`. `WDiv` applies the resolved cursor through a `MouseRegion`, so a plain container can feel clickable on web/desktop without `WAnchor` or a manual `MouseRegion`; on a `hover:`/`focus:`/`active:` `WDiv` the cursor `MouseRegion` sits below the auto-wrapped `WAnchor` and wins. Inert on touch platforms; last token wins; unknown names no-op. (`lib/src/parser/parsers/cursor_parser.dart`, `lib/src/parser/wind_style.dart`, `lib/src/widgets/w_div.dart`) (#125)
- `size-*` sizing utility (Tailwind v3.4+ shorthand): `size-2`, `size-full`, `size-1/2`, `size-[20px]`, `size-[50%]`, `size-screen` set BOTH width and height in one token. Fixes childless `WDiv` sizing: a `WDiv(className: 'size-2 rounded-full bg-...')` status dot now renders its box instead of collapsing (the previous gap was that `size-*` was unrecognized, not that `w-*`/`h-*` were ignored, which already worked childless). A later `w-*`/`h-*` overrides the matching axis. (`lib/src/parser/parsers/sizing_parser.dart`) (#123)
- `grid` equal-height rows via `items-stretch`. By default a Wind `grid` renders as a `Wrap` and sizes each cell to its own content, leaving a row ragged when one card is taller. Adding `items-stretch` (CSS Grid's default `align-items: stretch`) now builds the grid as a column of `IntrinsicHeight` rows so every cell in a row matches the tallest, and cells divide the row width evenly. Cells should size from their own content; `h-full` on a stretched cell is unsupported (it inserts a `LayoutBuilder` that asserts under `IntrinsicHeight`, see the intrinsic-sizing limitation docs). (`lib/src/widgets/w_div.dart`) (#126)
Expand Down
4 changes: 2 additions & 2 deletions doc/layout/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ WDiv(

Use `wrap` to create a wrapping container. This renders a Flutter `Wrap` widget.

> [!WARNING]
> Do **not** use `flex flex-wrap`. In Flutter, `Row`/`Column` cannot wrap. Always use the `wrap` display utility instead.
> [!NOTE]
> In Flutter, `Row`/`Column` cannot wrap, so `wrap` renders a dedicated `Wrap` widget rather than composing with `flex`. Prefer `wrap` directly; `flex-wrap` is aliased to it for callers coming from CSS (emits a one-time debug hint suggesting `wrap`).

```dart
WDiv(
Expand Down
10 changes: 5 additions & 5 deletions example/lib/pages/backgrounds/colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class BackgroundColorsPage extends StatelessWidget {
description: 'Common background colors',
children: [
WDiv(
className: 'flex flex-wrap gap-3 overflow-x-auto',
className: 'flex wrap gap-3 overflow-x-auto',
children: [
_buildColorBox('bg-red-500', 'Red'),
_buildColorBox('bg-blue-500', 'Blue'),
Expand All @@ -56,7 +56,7 @@ class BackgroundColorsPage extends StatelessWidget {
description: 'From 50 (light) to 900 (dark)',
children: [
WDiv(
className: 'flex flex-wrap gap-2',
className: 'flex wrap gap-2',
children: [
_buildShadeBox('bg-blue-50', '50', dark: true),
_buildShadeBox('bg-blue-100', '100', dark: true),
Expand All @@ -81,7 +81,7 @@ class BackgroundColorsPage extends StatelessWidget {
WDiv(
className: 'p-4 bg-gray-200 dark:bg-slate-700 rounded-lg',
child: WDiv(
className: 'flex flex-wrap gap-3 overflow-x-auto',
className: 'flex wrap gap-3 overflow-x-auto',
children: [
_buildOpacityBox('bg-red-500', '100%'),
_buildOpacityBox('bg-red-500/75', '75%'),
Expand All @@ -100,7 +100,7 @@ class BackgroundColorsPage extends StatelessWidget {
description: 'Custom hex colors with bracket notation',
children: [
WDiv(
className: 'flex flex-wrap gap-3 overflow-x-auto',
className: 'flex wrap gap-3 overflow-x-auto',
children: [
_buildColorBox('bg-[#1da1f2]', 'Twitter'),
_buildColorBox('bg-[#FF5733]', 'Custom'),
Expand All @@ -116,7 +116,7 @@ class BackgroundColorsPage extends StatelessWidget {
description: 'Common background values',
children: [
WDiv(
className: 'flex flex-wrap gap-3 overflow-x-auto',
className: 'flex wrap gap-3 overflow-x-auto',
children: [
WDiv(
className: 'bg-white p-4 rounded-lg border border-gray-300',
Expand Down
5 changes: 3 additions & 2 deletions example/lib/pages/layout/flex_intro.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ class FlexIntroExamplePage extends StatelessWidget {
),
),
ExampleSection(
title: 'Wrap (not flex-wrap)',
title: 'Wrap',
description:
'Flutter Row/Column do not wrap. Use the wrap utility to render a Wrap widget instead.',
'Flutter Row/Column do not wrap. Use the wrap utility to render a Wrap widget instead '
'(the Tailwind spelling is aliased to this token for callers coming from CSS).',
child: WDiv(
className: '''
wrap gap-2 p-4 rounded-lg
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/layout/z_index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ZIndexExamplePage extends StatelessWidget {
description: 'Standard z-index values from theme',
children: [
WDiv(
className: 'flex flex-wrap gap-2 overflow-x-auto',
className: 'flex wrap gap-2 overflow-x-auto',
children: [
_buildZIndexChip('z-0', '0'),
_buildZIndexChip('z-10', '10'),
Expand All @@ -121,7 +121,7 @@ class ZIndexExamplePage extends StatelessWidget {
description: 'Custom z-index with bracket notation',
children: [
WDiv(
className: 'flex flex-wrap gap-2 overflow-x-auto',
className: 'flex wrap gap-2 overflow-x-auto',
children: [
_buildZIndexChip('z-[100]', '100'),
_buildZIndexChip('z-[1000]', '1000'),
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/popover/popover_alignment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class PopoverAlignmentExamplePage extends StatelessWidget {
description: 'Opens below the trigger',
children: [
WDiv(
className: 'flex gap-4 flex-wrap',
className: 'flex gap-4 wrap',
children: [
_alignmentDemo('bottomLeft', PopoverAlignment.bottomLeft),
_alignmentDemo('bottomCenter', PopoverAlignment.bottomCenter),
Expand All @@ -55,7 +55,7 @@ class PopoverAlignmentExamplePage extends StatelessWidget {
description: 'Opens above the trigger',
children: [
WDiv(
className: 'flex gap-4 flex-wrap',
className: 'flex gap-4 wrap',
children: [
_alignmentDemo('topLeft', PopoverAlignment.topLeft),
_alignmentDemo('topCenter', PopoverAlignment.topCenter),
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/responsive/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ResponsiveCardExamplePage extends StatelessWidget {
className: 'text-sm text-pink-100 mb-3',
),
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down Expand Up @@ -135,7 +135,7 @@ class ResponsiveCardExamplePage extends StatelessWidget {
),
WDiv(className: 'h-4'),
WDiv(
className: 'flex flex-wrap gap-2',
className: 'flex wrap gap-2',
children: [
_buildTag('flex-col'),
_buildTag('md:flex-row'),
Expand Down
2 changes: 1 addition & 1 deletion example/lib/pages/responsive/grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ResponsiveGridExamplePage extends StatelessWidget {
'w-full p-4 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-500',
children: [
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/responsive/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ResponsiveLayoutExamplePage extends StatelessWidget {
className: 'text-sm text-cyan-100 mb-3',
),
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down Expand Up @@ -151,7 +151,7 @@ class ResponsiveLayoutExamplePage extends StatelessWidget {
),
WDiv(className: 'h-4'),
WDiv(
className: 'flex flex-wrap gap-2',
className: 'flex wrap gap-2',
children: [
_buildTag('flex-col'),
_buildTag('lg:flex-row'),
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/responsive/spacing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ResponsiveSpacingExamplePage extends StatelessWidget {
className: 'text-sm text-violet-100 mb-3',
),
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down Expand Up @@ -118,7 +118,7 @@ class ResponsiveSpacingExamplePage extends StatelessWidget {
),
WDiv(className: 'h-4'),
WDiv(
className: 'flex flex-wrap gap-2 md:gap-4 lg:gap-6',
className: 'flex wrap gap-2 md:gap-4 lg:gap-6',
children: List.generate(4, (i) {
return WDiv(
className:
Expand Down
2 changes: 1 addition & 1 deletion example/lib/pages/responsive/typography.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ResponsiveTypographyExamplePage extends StatelessWidget {
className: 'text-sm text-emerald-100 mb-3',
),
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down
2 changes: 1 addition & 1 deletion example/lib/pages/responsive/visibility.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ResponsiveVisibilityExamplePage extends StatelessWidget {
className: 'text-sm text-amber-100 mb-3',
),
WDiv(
className: 'flex flex-wrap items-center gap-2',
className: 'flex wrap items-center gap-2',
children: [
WDiv(
className: 'px-3 py-1 rounded-full bg-white/20',
Expand Down
2 changes: 1 addition & 1 deletion example/lib/pages/sizing/aspectratio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class AspectRatioExamplePage extends StatelessWidget {
description: 'Custom aspect ratios with bracket notation',
children: [
WDiv(
className: 'flex flex-wrap gap-4 overflow-x-auto',
className: 'flex wrap gap-4 overflow-x-auto',
children: [
_buildAspectBox('aspect-[4/3]', '4:3', 'purple', 120),
_buildAspectBox('aspect-[21/9]', '21:9', 'orange', 160),
Expand Down
4 changes: 2 additions & 2 deletions example/lib/pages/typography/colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class TypographyColorsPage extends StatelessWidget {
children: [
WDiv(
className:
'flex flex-wrap gap-4 p-4 bg-gray-100 dark:bg-slate-800 rounded-lg overflow-x-auto',
'flex wrap gap-4 p-4 bg-gray-100 dark:bg-slate-800 rounded-lg overflow-x-auto',
children: [
_buildColorChip('text-red-500', 'Red'),
_buildColorChip('text-blue-500', 'Blue'),
Expand All @@ -55,7 +55,7 @@ class TypographyColorsPage extends StatelessWidget {
children: [
WDiv(
className:
'flex flex-wrap gap-2 p-4 bg-gray-100 dark:bg-slate-800 rounded-lg overflow-x-auto',
'flex wrap gap-2 p-4 bg-gray-100 dark:bg-slate-800 rounded-lg overflow-x-auto',
children: [
WText(
'50',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class _WindAnimationWrapperBasicExamplePageState
className: 'flex flex-col gap-4',
children: [
WDiv(
className: 'flex gap-2 flex-wrap',
className: 'flex gap-2 wrap',
children: types.map((type) {
final isActive = type == _selectedType;
return WButton(
Expand Down
42 changes: 41 additions & 1 deletion lib/src/parser/parsers/flexbox_grid_parser.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../wind_context.dart';
Expand All @@ -11,7 +12,7 @@ import 'wind_parser_interface.dart';
/// [WDiv] and other layout-aware widgets arrange their children.
///
/// ### Supported Utility Classes:
/// - **Display:** `flex`, `grid`, `wrap`, `block`, `hidden`
/// - **Display:** `flex`, `grid`, `wrap` (also aliased from `flex-wrap`), `block`, `hidden`
/// - **Direction:** `flex-row`, `flex-col`
/// - **Justify Content:** `justify-start`, `justify-end`, `justify-center`, `justify-between`, `justify-around`, `justify-evenly`
/// - **Align Items:** `items-start`, `items-end`, `items-center`, `items-baseline`, `items-stretch`
Expand All @@ -33,6 +34,31 @@ import 'wind_parser_interface.dart';
class FlexboxGridParser implements WindParserInterface {
const FlexboxGridParser();

/// Whether the `flex-wrap` -> `wrap` alias hint has already been printed
/// this session (debug-only, dedup to one line regardless of how many
/// `flex-wrap` occurrences are parsed).
static bool _warnedFlexWrapAlias = false;

/// Emits a one-time debug hint pointing callers at the canonical `wrap`
/// token, mirroring the `_warnedAliases` dedup pattern in `WindParser`.
static void _warnFlexWrapAlias() {
if (_warnedFlexWrapAlias) return;
_warnedFlexWrapAlias = true;
debugPrint(
"Wind: 'flex-wrap' is aliased to 'wrap'; prefer 'wrap' directly.",
);
}

/// Resets the one-time `flex-wrap` alias hint (test-only).
///
/// Mirrors `WindParser.clearCache()` resetting `_warnedAliases`, so a test
/// asserting the hint fires is not order-dependent on earlier tests that
/// already parsed `flex-wrap`.
@visibleForTesting
static void resetWarnings() {
_warnedFlexWrapAlias = false;
}

/// Matches theme-based gap classes (e.g., `gap-4`, `gap-x-2`, `gap-y-1/2`)
/// Also matches space-x and space-y as aliases (Tailwind's margin-based spacing)
static final RegExp _themeGapRegex = RegExp(
Expand Down Expand Up @@ -216,6 +242,20 @@ class FlexboxGridParser implements WindParserInterface {
isHidden = false;
visibilitySet = true;
}
} else if (displayType == null && className == 'flex-wrap') {
// Tailwind's `flex-wrap` sets `flex-wrap: wrap` on an existing flex
// container; wind has no separate wrap-property slot from `display`,
// so it is aliased directly to `WindDisplayType.wrap` (wind's
// dedicated wrap-display token). This is an ADDED alias, not a
// replacement: `wrap` keeps its own meaning and behavior.
displayType = WindDisplayType.wrap;
if (!visibilitySet) {
isHidden = false;
visibilitySet = true;
}
if (kDebugMode) {
_warnFlexWrapAlias();
}
} else if (flexDirection == null &&
_directionMap.containsKey(className)) {
flexDirection = _directionMap[className];
Expand Down
Loading