Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed

- `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