Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed

- Interactive widgets (`WSelect`, `WCheckbox`, `WRadio`, `WDatePicker`) now route their selection, checkmark, and accent colors through the theme `primary` token instead of hardcoded `blue-*` classes and `Colors.blue` shades, so a consumer's brand color drives them. The shipped default look is unchanged (`primary` defaults to the Tailwind blue swatch). `WindThemeData.toThemeData()` keeps its indigo Material `ColorScheme` baseline unless a custom `primary` is registered, so the default Material appearance is unchanged too. (`lib/src/widgets/w_select.dart`, `lib/src/widgets/w_checkbox.dart`, `lib/src/widgets/w_radio.dart`, `lib/src/widgets/w_date_picker.dart`, `lib/src/theme/wind_theme_data.dart`) (WIND-3)

### Added

- Seeded default `primary` color token: Wind's default theme now includes a `primary` `MaterialColor` (aliased 1:1 to the Tailwind `blue` swatch), so `bg-primary` / `text-primary` / `border-primary` resolve out of the box and become brand-overridable via `WindThemeData.colors: {'primary': ...}`. Previously `primary` was consulted only by `toThemeData()` and was a silent no-op in className tokens. (`lib/src/theme/defaults/colors.dart`, `lib/src/theme/wind_theme_data.dart`) (WIND-3)
- `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
19 changes: 9 additions & 10 deletions doc/widgets/w-date-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ The calendar grid consists of:
| **Weekday row** | `Mo Tu We Th Fr Sa Su` (Monday start) |
| **Date grid** | 6 rows × 7 columns = 42 cells |
| **Today** | Highlighted with `bg-gray-100 dark:bg-gray-700 rounded-full` |
| **Selected** | `bg-blue-500 text-white rounded-full` |
| **In range** | `bg-blue-100 dark:bg-blue-900/30 text-blue-700` |
| **Selected** | `bg-primary text-white rounded-full` |
| **In range** | `bg-primary-100 dark:bg-primary-900/30 text-primary-700` |
| **Out of month** | `text-gray-300 dark:text-gray-600` |
| **Disabled** | `text-gray-300 dark:text-gray-600`, no click |

Expand Down Expand Up @@ -359,19 +359,18 @@ The calendar popover, header, weekday labels, and day cells use internal Wind cl

## Customizing Theme

The calendar's selection colors (`bg-blue-500`, `bg-blue-100`) and neutral grays use Tailwind color scales from `WindThemeData`. Override them to change the visual appearance:
The calendar's selection colors route through the theme `primary` token (`bg-primary` for the selected day, `bg-primary-100` / `text-primary-700` for the in-range fill). Override `primary` to recolor the calendar to your brand; the neutral grays remain configurable via their own palette keys. The default `primary` is aliased to the Tailwind blue swatch, so leaving it unchanged keeps the original blue look.

```dart
WindTheme(
data: WindThemeData(
colors: {
...WindThemeData.defaultColors,
'blue': {
100: Color(0xFFDBEAFE), // Range fill
500: Color(0xFF3B82F6), // Selected day
700: Color(0xFF1D4ED8), // Range text
900: Color(0xFF1E3A8A), // Dark mode range
},
'primary': MaterialColor(0xFF16A34A, {
100: Color(0xFFDCFCE7), // Range fill
500: Color(0xFF16A34A), // Selected day (shade 500 == bg-primary)
700: Color(0xFF15803D), // Range text
900: Color(0xFF14532D), // Dark mode range
Comment thread
anilcancakir marked this conversation as resolved.
}),
},
),
child: MyApp(),
Expand Down
8 changes: 4 additions & 4 deletions doc/widgets/w-radio.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ WRadio<String>(
| `value` | `T` | **Required** | The value this radio represents. |
| `groupValue` | `T?` | **Required** | The currently selected value for the group. When `value == groupValue`, `selected:` state activates. |
| `onChanged` | `ValueChanged<T>?` | `null` | Called with `value` when this radio is tapped and not already selected. Omit or pass `null` for a non-interactive radio. |
| `className` | `String?` | `null` | Utility classes for the outer ring shell. Defaults to `'w-5 h-5 rounded-full border border-gray-300 flex items-center justify-center selected:border-blue-500'` when `null`. |
| `indicatorClassName` | `String?` | `null` | Utility classes for the inner filled dot (visible only when selected). Defaults to `'w-2.5 h-2.5 rounded-full bg-blue-500 selected:opacity-100'` when `null`. |
| `className` | `String?` | `null` | Utility classes for the outer ring shell. Defaults to `'w-5 h-5 rounded-full border border-gray-300 flex items-center justify-center selected:border-primary-500'` when `null`. |
| `indicatorClassName` | `String?` | `null` | Utility classes for the inner filled dot (visible only when selected). Defaults to `'w-2.5 h-2.5 rounded-full bg-primary selected:opacity-100'` when `null`. |
| `disabled` | `bool` | `false` | Blocks tap and activates the `disabled:` prefix. |
| `states` | `Set<String>?` | `null` | Extra custom states merged with built-in `selected` and `disabled`. |
| `semanticLabel` | `String?` | `null` | Accessible label for the Semantics node. Required on icon-only radios. |
Expand Down Expand Up @@ -118,10 +118,10 @@ WDiv(

### Default Fallback Styling

When `className` or `indicatorClassName` is omitted, `WRadio` falls back to its built-in defaults (blue indicator). Override only when the design calls for a different tone:
When `className` or `indicatorClassName` is omitted, `WRadio` falls back to its built-in defaults, which route through the theme `primary` token (blue by default; override `primary` in `WindThemeData` to rebrand). Override the classNames only when the design calls for a different tone:

```dart
// Uses built-in blue defaults
// Uses built-in primary defaults (blue unless the theme's primary is overridden)
WRadio<String>(
value: 'option',
groupValue: _value,
Expand Down
18 changes: 18 additions & 0 deletions lib/src/theme/defaults/colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ const Map<String, dynamic> colors = {
'transparent': Color(0x00000000),
'black': Color(0xFF000000),
'white': Color(0xFFFFFFFF),
// Semantic brand token. Seeded so `bg-primary`/`text-primary`/`border-primary`
// resolve out of the box and Wind widgets (select, checkbox, radio, date
// picker) can route their selection colors through it. The default mirrors
// the Tailwind blue swatch 1:1 so the shipped look is unchanged; a consumer
// overrides it with `colors: {'primary': <brand>}` to recolor those widgets.
'primary': {
50: Color(0xffeff6ff),
100: Color(0xffdbeafe),
200: Color(0xffbfdbfe),
300: Color(0xff93c5fd),
400: Color(0xff60a5fa),
500: Color(0xff3b82f6),
600: Color(0xff2563eb),
700: Color(0xff1d4ed8),
800: Color(0xff1e40af),
900: Color(0xff1e3a8a),
950: Color(0xff172554),
},
'slate': {
50: Color(0xfff8fafc),
100: Color(0xfff1f5f9),
Expand Down
18 changes: 16 additions & 2 deletions lib/src/theme/wind_theme_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,22 @@ class WindThemeData {
return MaterialColor(raw[500]!.toARGB32(), raw);
}

// Resolve colors or use defaults
final primary = colors['primary'] ?? getDefault('indigo');
// Resolve colors or use defaults.
//
// `primary` is always present in the color map (it is seeded to the Tailwind
// blue swatch so `bg-primary` resolves for Wind widgets). That default seed
// must NOT drive the Material ColorScheme: the Material layer keeps its
// indigo baseline unless the consumer sets an explicit brand color. We treat
// a `primary` equal to the seeded default as "not customized" and fall back
// to indigo, so `toThemeData()` only repaints Material widgets when the
// brand color genuinely differs from Wind's default.
final seededPrimary =
(default_colors.colors['primary'] as Map<int, Color>)[500]!.toARGB32();
final MaterialColor? configuredPrimary = colors['primary'];
final primary = (configuredPrimary != null &&
configuredPrimary.toARGB32() != seededPrimary)
? configuredPrimary
: getDefault('indigo');
final secondary = colors['secondary'] ?? getDefault('teal');
final error = colors['error'] ?? getDefault('red');

Expand Down
10 changes: 5 additions & 5 deletions lib/src/widgets/w_checkbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import '../parser/wind_style.dart';
///
/// ### Supported Features:
/// - **Styling:** `w-6`, `h-6`, `rounded`, `border-2`, `border-gray-300`
/// - **Checked State:** `checked:bg-blue-600`, `checked:border-transparent`
/// - **Interactive States:** `hover:border-blue-400`, `disabled:opacity-50`
/// - **Checked State:** `checked:bg-primary`, `checked:border-transparent`
/// - **Interactive States:** `hover:border-primary-400`, `disabled:opacity-50`
/// - **Icons:** Customize status icon via `iconClassName` and `checkIcon`
///
/// ### Example Usage:
Expand All @@ -24,7 +24,7 @@ import '../parser/wind_style.dart';
/// WCheckbox(
/// value: isChecked,
/// onChanged: (val) => setState(() => isChecked = val),
/// className: 'w-5 h-5 rounded border border-gray-300 checked:bg-blue-500 checked:border-transparent transition-colors',
/// className: 'w-5 h-5 rounded border border-gray-300 checked:bg-primary checked:border-transparent transition-colors',
/// )
/// ```
class WCheckbox extends StatelessWidget {
Expand All @@ -39,7 +39,7 @@ class WCheckbox extends StatelessWidget {
/// Supports:
/// - **Dimensions:** `w-5`, `h-5` (Required)
/// - **Appearance:** `rounded-md`, `border`, `border-gray-300`
/// - **States:** `checked:bg-blue-600`, `hover:border-blue-500`, `disabled:bg-gray-100`
/// - **States:** `checked:bg-primary`, `hover:border-primary-500`, `disabled:bg-gray-100`
Comment thread
anilcancakir marked this conversation as resolved.
Outdated
///
/// Example: `'w-6 h-6 rounded-full border-2 border-red-500 checked:bg-red-500'`
final String? className;
Expand Down Expand Up @@ -130,7 +130,7 @@ class WCheckbox extends StatelessWidget {
states: activeStates,
child: WDiv(
className:
'w-5 h-5 rounded border border-gray-300 items-center justify-center checked:bg-blue-500 error:border-red-500 checked:bg-primary checked:border-transparent ${className != null ? ' $className' : ''}',
'w-5 h-5 rounded border border-gray-300 items-center justify-center error:border-red-500 checked:bg-primary checked:border-transparent ${className != null ? ' $className' : ''}',
states: activeStates, // Pass states to WDiv for checked: prefix
children: [
if (value)
Expand Down
6 changes: 3 additions & 3 deletions lib/src/widgets/w_date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -723,12 +723,12 @@ class _CalendarGrid extends StatelessWidget {
} else if (!isSelectable) {
textClass = 'text-gray-300 dark:text-gray-600';
} else if (isSelected || isRangeStart || isRangeEnd) {
bgClass = 'bg-blue-500';
bgClass = 'bg-primary';
textClass = 'text-white font-medium';
roundedClass = 'rounded-full';
} else if (isInRange) {
bgClass = 'bg-blue-100 dark:bg-blue-900/30';
textClass = 'text-blue-700 dark:text-blue-300';
bgClass = 'bg-primary-100 dark:bg-primary-900/30';
textClass = 'text-primary-700 dark:text-primary-300';
} else if (isToday) {
bgClass = 'bg-gray-100 dark:bg-gray-700';
textClass = 'text-gray-900 dark:text-gray-100 font-medium';
Expand Down
16 changes: 8 additions & 8 deletions lib/src/widgets/w_radio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import '../parser/wind_style.dart';
/// - **Selection:** drives the `selected:` state prefix on className tokens
/// - **Disabled:** drives the `disabled:` state prefix, blocks tap
/// - **Styling:** `w-5`, `h-5`, `rounded-full`, `border`, `border-gray-300`
/// - **Selected State:** `selected:border-blue-500`, `selected:bg-blue-500`
/// - **Selected State:** `selected:border-primary-500`, `selected:bg-primary`
/// - **Hover / Focus:** inherited from the wrapping `WAnchor`
///
/// ### Example Usage:
Expand All @@ -32,8 +32,8 @@ import '../parser/wind_style.dart';
/// onChanged: (val) => setState(() => _theme = val),
/// className: '''
/// w-5 h-5 rounded-full border border-gray-300
/// selected:border-blue-500
/// hover:border-blue-400
/// selected:border-primary-500
/// hover:border-primary-400
/// disabled:opacity-50
/// ''',
/// )
Expand All @@ -58,15 +58,15 @@ class WRadio<T> extends StatelessWidget {
/// - **Dimensions:** `w-5`, `h-5` (required for a visible hit area)
/// - **Shape:** `rounded-full`
/// - **Border:** `border`, `border-gray-300`
/// - **States:** `selected:border-blue-500`, `hover:border-blue-400`,
/// - **States:** `selected:border-primary-500`, `hover:border-primary-400`,
/// `disabled:opacity-50`
///
/// Example: `'w-5 h-5 rounded-full border border-gray-300 selected:border-blue-500'`
/// Example: `'w-5 h-5 rounded-full border border-gray-300 selected:border-primary-500'`
final String? className;

/// Utility classes for the inner indicator dot shown when selected.
///
/// Defaults to a filled circle. Example: `'w-2.5 h-2.5 rounded-full bg-blue-500'`
/// Defaults to a filled circle. Example: `'w-2.5 h-2.5 rounded-full bg-primary'`
final String? indicatorClassName;

/// Whether this radio button is disabled.
Expand Down Expand Up @@ -134,14 +134,14 @@ class WRadio<T> extends StatelessWidget {
// 3. The indicator dot is visible only when selected.
final Widget indicator = WDiv(
className: indicatorClassName ??
'w-2.5 h-2.5 rounded-full bg-blue-500 selected:opacity-100',
'w-2.5 h-2.5 rounded-full bg-primary selected:opacity-100',
states: activeStates,
);

// 4. Compose the radio shell with an optional indicator.
final Widget shell = WDiv(
className: className ??
'w-5 h-5 rounded-full border border-gray-300 flex items-center justify-center selected:border-blue-500',
'w-5 h-5 rounded-full border border-gray-300 flex items-center justify-center selected:border-primary-500',
states: activeStates,
children: [
if (_isSelected) indicator,
Expand Down
40 changes: 30 additions & 10 deletions lib/src/widgets/w_select.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'select_option.dart';
import 'w_div.dart';
import 'w_input.dart';
import 'w_text.dart';
import '../utils/wind_extensions.dart';
import '../utils/wind_logger.dart';

/// Signature for building a custom trigger widget for [WSelect].
Expand Down Expand Up @@ -256,6 +257,12 @@ class _WSelectState<T> extends State<WSelect<T>> {
bool _isHovering = false;
bool _openUpward = false;

/// The theme's `primary` swatch, used for the raw [Icon] colors that cannot
/// take a className. `primary` is always present in the resolved color map
/// (Wind seeds it into the default theme), so the brand color drives these
/// icons the same way `bg-primary`/`text-primary` drive the surrounding text.
MaterialColor get _primaryColor => context.windColors['primary']!;

/// Shared [TapRegion] group for the trigger and the open menu, so re-tapping
/// the trigger toggles it closed (the tap is inside the group) rather than
/// being treated as an outside tap. The opening-tap self-close is handled
Expand Down Expand Up @@ -699,12 +706,16 @@ class _WSelectState<T> extends State<WSelect<T>> {

Widget _buildDefaultChip(SelectOption<T> option) {
return WDiv(
className: 'flex items-center gap-1 bg-blue-100 rounded px-2 py-0.5',
className: 'flex items-center gap-1 bg-primary-100 rounded px-2 py-0.5',
children: [
WText(option.label, className: 'text-blue-800 text-sm'),
WText(option.label, className: 'text-primary-800 text-sm'),
GestureDetector(
onTap: () => _removeValue(option.value),
child: Icon(Icons.close, size: 14, color: Colors.blue.shade700),
child: Icon(
Icons.close,
size: 14,
color: _primaryColor.shade700,
Comment thread
anilcancakir marked this conversation as resolved.
Outdated
),
),
],
);
Expand Down Expand Up @@ -859,10 +870,14 @@ class _WSelectState<T> extends State<WSelect<T>> {
child: CircularProgressIndicator(strokeWidth: 2),
)
else
Icon(Icons.add, size: 16, color: Colors.blue.shade400),
Icon(
Icons.add,
size: 16,
color: _primaryColor.shade400,
Comment thread
anilcancakir marked this conversation as resolved.
Outdated
),
WText(
'Create "$_searchQuery"',
className: 'text-blue-500 dark:text-blue-400 font-medium',
className: 'text-primary-500 dark:text-primary-400 font-medium',
),
],
),
Expand Down Expand Up @@ -911,14 +926,14 @@ class _WSelectState<T> extends State<WSelect<T>> {
}

final String bgClass = isSelected
? 'bg-blue-50 dark:bg-blue-900/30'
? 'bg-primary-50 dark:bg-primary-900/30'
: isHovered
? 'bg-gray-100 dark:bg-slate-700'
: 'bg-transparent';
final String textColorClass = option.disabled
? 'text-gray-400 dark:text-gray-500'
: isSelected
? 'text-blue-700 dark:text-blue-300 font-medium'
? 'text-primary-700 dark:text-primary-300 font-medium'
: 'text-gray-800 dark:text-gray-200';

return WindAnchorStateProvider(
Expand All @@ -942,16 +957,21 @@ class _WSelectState<T> extends State<WSelect<T>> {
Icon(
isSelected ? Icons.check_box : Icons.check_box_outline_blank,
size: 18,
color:
isSelected ? Colors.blue.shade600 : Colors.grey.shade400,
color: isSelected
? _primaryColor.shade600
Comment thread
anilcancakir marked this conversation as resolved.
Outdated
: Colors.grey.shade400,
),
if (option.icon != null) option.icon!,
WDiv(
className: 'flex-1',
child: WText(option.label, className: textColorClass),
),
if (!widget.isMulti && isSelected)
Icon(Icons.check, size: 18, color: Colors.blue.shade600),
Icon(
Icons.check,
size: 18,
color: _primaryColor.shade600,
Comment thread
anilcancakir marked this conversation as resolved.
Outdated
),
],
),
),
Expand Down
Loading