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
12 changes: 9 additions & 3 deletions apps/docs/content/docs/flutterwindcss/fonts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,17 @@ Because the theme's family strings are exactly what `google_fonts` registered, `
rather keep the theme `const`, use Recipe 1.
</Callout>

## Overriding a single family
## Overriding a single family — and using fonts with no theme

`font('Inter')` sets a **literal** family for one chain (Tailwind `font-[Inter]`), independent of the
theme — handy for a one-off. Don't combine it with `fontSans`/`fontSerif`/`fontMono` in the same chain
(the engine asserts, since both would set the family).
theme — handy for a one-off, and it needs **no `FwTheme`** at all. Don't combine it with
`fontSans`/`fontSerif`/`fontMono` in the same chain (the engine asserts, since both would set the
family).

You're not forced into a theme: with no `FwTheme` present, `fontSans`/`fontSerif`/`fontMono` fall back
to the stock `FwTokens.light` families (the generic `sans-serif`/`serif`/`monospace`) rather than
crash. So `font('YourFont')` is the no-theme path for a custom face; the role getters are the
theme-aware path. (See [You're not forced into a theme](/docs/flutterwindcss/theming#youre-not-forced-into-a-theme).)

## Interop path (MaterialApp)

Expand Down
29 changes: 29 additions & 0 deletions apps/docs/content/docs/flutterwindcss/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,35 @@ FwAnimatedTheme(tokens: isDark ? darkTheme : lightTheme, child: const HomeScreen
carries all 32 roles. That's why a pasted theme round-trips with nothing dropped.
</Callout>

## You're not forced into a theme

Semantic tokens (`context.fw.colors.primary`) are *opt-in*. The `.tw` utilities that take **raw
values** need no theme at all — so you can use flutterwindcss with the raw [palette](/docs/flutterwindcss/colors)
and your own [fonts](/docs/flutterwindcss/fonts), no `FwTheme` and no generated `theme.dart`:

```dart
// No FwTheme anywhere — raw palette + a literal font.
Text('Hi').tw
.px(4).py(2)
.bg(FwPalette.blue.shade600)
.text(FwPalette.slate.shade50)
.rounded(8)
.font('Inter');
```

- **Raw palette colours, spacing, sizing, radius, borders, effects, transforms** — all take literal
values; no theme required.
- **`font('Family')`** sets a literal font family; no theme required (see [Fonts](/docs/flutterwindcss/fonts)).
- The **theme-role sugars** (`fontSans`/`fontSerif`/`fontMono`, `roundedMd`/`shadowMd`) *prefer* a
theme, but **fall back to the stock `FwTokens.light` defaults** when none is present — they never
force a theme on you.
- Reading tokens where a theme is *optional*? Use **`context.fwOrNull`** (returns `null` instead of
throwing). Only `context.fw` (and semantic tokens) require a theme.

This is also why the engine works under a `MaterialApp` — provide tokens through an
[`FwThemeExtension`](/docs/flutterwindcss/installation#inside-a-materialapp-interop) and the same
components resolve through it; bring your own fonts via the Material `textTheme`.

## Next steps

<Cards>
Expand Down
19 changes: 14 additions & 5 deletions apps/example/lib/showcase/sections/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ class EffectsSection extends StatelessWidget {
_fitDemo(context, 'contain', BoxFit.contain),
_fitDemo(context, 'cover', BoxFit.cover),
_fitDemo(context, 'fill', BoxFit.fill),
// object-position: the fitted content anchored to the top-start.
_fitDemo(
context,
'cover · top-start',
BoxFit.cover,
alignment: AlignmentDirectional.topStart,
),
],
),
],
Expand All @@ -173,17 +180,19 @@ class EffectsSection extends StatelessWidget {
return DemoTile(label: label, child: filter(box));
}

Widget _fitDemo(BuildContext context, String label, BoxFit fit) {
Widget _fitDemo(BuildContext context, String label, BoxFit fit, {AlignmentGeometry? alignment}) {
final t = context.fw;
return DemoTile(
label: label,
// A wide label scaled to a fixed box via object-fit.
// A wide label scaled to a fixed box via object-fit (+ optional position).
child: SizedBox(
width: 80,
height: 40,
child: Text(
'FIT',
).tw.fit(fit).bg(t.colors.muted).text(t.colors.mutedForeground).weight(FwFontWeight.black),
child: Text('FIT').tw
.fit(fit, alignment: alignment)
.bg(t.colors.muted)
.text(t.colors.mutedForeground)
.weight(FwFontWeight.black),
),
);
}
Expand Down
4 changes: 3 additions & 1 deletion apps/example/lib/showcase/sections/utilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ class UtilitiesSection extends StatelessWidget {
SizedBox(
height: 120,
child: FwScroll(
thumbColor: t.colors.border,
thumbColor: t.colors.mutedForeground,
trackColor: t.colors.muted, // Tailwind scrollbar-color (track)
alwaysShowScrollbar: true,
child: FwColumn(
crossAxisAlignment: CrossAxisAlignment.stretch,
gap: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class FwTokens {
## 5. Theme access (`lib/src/theme/`)

### 5.1 `FwTheme` (`theme/fw_theme.dart`)
An `InheritedWidget` carrying the active `FwTokens`. Light/dark switching is the host app's job (AGENTS.md §5): the host provides whichever instance is active. `updateShouldNotify` compares tokens identity/value.
Carries the active `FwTokens` down the tree **and** applies the theme's `sans` family as the subtree's default text family (a merged, family-only `DefaultTextStyle`, so a pasted theme's fonts apply once registered; corrected — 1.0.3). It is a `StatelessWidget` whose `build` provides a private `_FwThemeScope` `InheritedWidget` (the actual tokens carrier; `FwTheme.maybeOf` looks it up) wrapping the `DefaultTextStyle.merge`. Public API unchanged (`FwTheme(tokens:, child:)` const + `maybeOf`). Light/dark switching is the host app's job (AGENTS.md §5). `_FwThemeScope.updateShouldNotify` compares tokens.

### 5.2 `FwThemeExtension` (`theme/fw_theme_extension.dart`)
A `ThemeExtension<FwThemeExtension>` carrying `FwTokens`, with `copyWith` and `lerp` (so Material's theme animation drives `FwTokens.lerp` correctly). **This file is the single sanctioned `package:flutter/material.dart` import in the entire repo** (AGENTS.md §3.5). No other engine or component file imports Material.
Expand All @@ -151,10 +151,12 @@ extension FwContext on BuildContext {
}
```
Resolution order:
1. `dependOnInheritedWidgetOfExactType<FwTheme>()` → its `tokens`.
1. `dependOnInheritedWidgetOfExactType<_FwThemeScope>()` → its `tokens` (via `FwTheme.maybeOf`).
2. else `Theme.of(this).extension<FwThemeExtension>()?.tokens`.
3. else throw a `FlutterError` with a clear, actionable message ("No FwTheme or FwThemeExtension found in the widget tree. Wrap your app in FwTheme(...) or add FwThemeExtension to your ThemeData.extensions.") and a `DiagnosticsNode` chain.

A non-throwing `fwOrNull` getter (added 1.0.3) returns `null` instead of throwing when no theme is present — used by the sugar pass so `fontSans`/`roundedMd`/`shadowMd` fall back to `FwTokens.light` stock values rather than forcing a theme on the developer.

Components read tokens **only** via `context.fw` (AGENTS.md §3.4) — never `Theme.of` directly.

---
Expand Down Expand Up @@ -242,7 +244,7 @@ Padding(margin, EdgeInsetsDirectional) ← margin is outer
DecoratedBox(gradient|color|image, border, radius)
→ ClipRRect(borderRadius INSET by borderWidth, if clipBehavior != none) ← clips CONTENT
→ Padding(padding, EdgeInsetsDirectional)
→ FittedBox(fit) ← object-fit (module 12)
→ FittedBox(fit, alignment) ← object-fit + object-position (module 12 / 1.0.3)
→ DefaultTextStyle.merge + IconTheme.merge (foreground, font*, align, decoration, textShadows)
→ child
```
Expand All @@ -267,11 +269,11 @@ Directional throughout (AGENTS.md §3.3); spacing args in utility units:
>
> **Typography (module 6):** the as-built utilities are `text(Color) · textSize(double) · weight(int) · leading(double) · tracking(double) · align(TextAlign) · underline · lineThrough`. The §6.5 list above wrote `fontSize`/`fontWeight`/`textAlign`, but those collide with the `FwStyle` **fields** of the same name (the mixin can't redeclare them), so the utilities took collision-free Tailwind-faithful names (`textSize`/`weight`/`align`) — the fields are unchanged. `weight` takes the **CSS int scale** `100..900` (the `FwFontWeight` token values) and maps to a Flutter `FontWeight`. `textSize` is logical px (`FwFontSize.*.px`); `leading` is a line-height **multiple** (`FwLeading.*`); `tracking` is **absolute logical px** (Flutter's model — *not* em; the em-based `FwTracking` scale must be multiplied by the font size at the call site). `textSize`/`leading` assert `> 0`; `weight` asserts a valid step; `tracking` may be negative. `underline`/`lineThrough` **combine** (Tailwind-style) rather than last-wins. Text goldens use Flutter's built-in deterministic test font (no bundled face).
>
> **Text completeness (module 11):** adds the remaining text vocabulary, all riding the same `DefaultTextStyle.merge`: `font(String)` + `fontSans`/`fontSerif`/`fontMono` (family), `maxLines(int)`, `lineClamp(int)` (Tailwind `line-clamp-N` = maxLines + ellipsis), `truncate` (1 line + ellipsis + no wrap), `overflow(TextOverflow)`, and `nowrap`/`wrap`. To avoid the field/setter name clash (same reason as `weight`/`opacity`), the `maxLines` setter writes a `maxLineCount` field and the `overflow` setter writes a `textOverflow` field; `resolve` projects these to the terse `ResolvedStyle.maxLines`/`textOverflow`. `maxLines`/`lineClamp` assert `> 0`. Verified by unit tests + a render-chain test (the resolved props reach `DefaultTextStyle`); rendering the ellipsis itself is Flutter's job, so no new golden is needed.
> **Text completeness (module 11):** adds the remaining text vocabulary, all riding the same `DefaultTextStyle.merge`: `font(String)` (literal family) + `fontSans`/`fontSerif`/`fontMono` (**theme-resolved** family roles as of 1.0.3 — a `FwFontStep` resolved against `FwTokens.typography` by `FwStyled`, like `roundedMd`/`shadowMd`; `FwTheme` applies the `sans` family as the subtree default), `overline` (1.0.3), `maxLines(int)`, `lineClamp(int)` (Tailwind `line-clamp-N` = maxLines + ellipsis), `truncate` (1 line + ellipsis + no wrap), `overflow(TextOverflow)`, and `nowrap`/`wrap`. To avoid the field/setter name clash (same reason as `weight`/`opacity`), the `maxLines` setter writes a `maxLineCount` field and the `overflow` setter writes a `textOverflow` field; `resolve` projects these to the terse `ResolvedStyle.maxLines`/`textOverflow`. `maxLines`/`lineClamp` assert `> 0`. Verified by unit tests + a render-chain test (the resolved props reach `DefaultTextStyle`); rendering the ellipsis itself is Flutter's job, so no new golden is needed.
>
> **Transform extras + interactivity (module 13):** per-axis `scaleX`/`scaleY` (compose multiplicatively with the uniform `scale`), `skewX`/`skewY` (degrees → radians, via `Matrix4.skew`), and `transformOrigin` (→ `Transform.alignment`, default center) extend the paint-only transform — render order T·R·Skew·S. Plus interactivity wrappers `cursor(MouseCursor)` (→ `MouseRegion`), `pointerEventsNone` (→ `IgnorePointer`), `invisible`/`visible` (→ `Visibility` with `maintainSize` — hides but keeps layout space), the `italic`/`notItalic` `fontStyle` toggle, and the `size(n)` width+height sugar. Field/setter clashes avoided per convention (`cursor`→`mouseCursor`, `visible`→`isVisible`, `scaleX`→`scaleXFactor`, `skewX`→`skewXAngle`). New outer wrappers slot between margin and constraints (`margin → cursor → ignore-pointer → visibility → constraints → …`). A resolve-level test pins that every module 11/12/13 field carries through `_overlay` + projection.
>
> **Filters + object-fit (module 12):** the CSS `filter` color functions — `grayscale`/`brightness`/`contrast`/`saturate`/`invert`/`sepia`/`hueRotate` — and `fit(BoxFit)` (Tailwind `object-*`). The color filters resolve to **one** `ColorFilter.matrix`: each setter multiplies its 4×5 matrix into the accumulated `colorMatrix` field, so they **compose** within a chain (CSS `filter: a() b()` ⇒ a then b) rather than last-wins — the second filter-style combinator after `underline`/`lineThrough`. (Content `blur` stays a separate spatial `ImageFilter`.) In the render chain the color filter sits just outside the content blur (a `ColorFiltered`, like CSS `filter` on the rendered element); `fit` is a `FittedBox` inside the padding (the content box). Field/setter clash avoided as usual: setter `fit` ↔ field `boxFit` (→ `ResolvedStyle.fit`); the filter setters write the shared `colorMatrix`. The matrix math (luma weights, multiplicative brightness, contrast bias, composition) is unit-tested; applying the matrix is Flutter's job, so no new golden.
> **Filters + object-fit (module 12):** the CSS `filter` color functions — `grayscale`/`brightness`/`contrast`/`saturate`/`invert`/`sepia`/`hueRotate` — and `fit(BoxFit, {alignment})` (Tailwind `object-*` + `object-{position}`; `alignment` → `FittedBox.alignment`, added 1.0.3). The color filters resolve to **one** `ColorFilter.matrix`: each setter multiplies its 4×5 matrix into the accumulated `colorMatrix` field, so they **compose** within a chain (CSS `filter: a() b()` ⇒ a then b) rather than last-wins — the second filter-style combinator after `underline`/`lineThrough`. (Content `blur` stays a separate spatial `ImageFilter`.) In the render chain the color filter sits just outside the content blur (a `ColorFiltered`, like CSS `filter` on the rendered element); `fit` is a `FittedBox` inside the padding (the content box). Field/setter clash avoided as usual: setter `fit` ↔ field `boxFit` (→ `ResolvedStyle.fit`); the filter setters write the shared `colorMatrix`. The matrix math (luma weights, multiplicative brightness, contrast bias, composition) is unit-tested; applying the matrix is Flutter's job, so no new golden.
>
> **Effects (module 7):** `shadow(List<BoxShadow>) · opacity(double) · blur(double) · backdropBlur(double)`. The §6.5 list above wrote `shadow(FwShadow)`, but no `FwShadow` enum exists (the token is the `FwShadows` *scale* on `FwTokens.shadows`) and the ops layer has no context to resolve a selector — so `shadow` takes the **resolved list** the component reads from the theme: `shadow(context.fw.shadows.md)` (empty list = no shadow), mirroring `bg(Color)`. The `opacity`/`blur`/`backdropBlur` setters write the renamed `FwStyle` fields `groupOpacity`/`contentBlur`/`backdropBlurSigma` (§6.1). Guards: `opacity` in `0..1`; `blur`/`backdropBlur` sigmas `>= 0`. Backdrop blur is covered by `render_chain_test` (needs a textured backdrop), not the effects golden.
>
Expand Down Expand Up @@ -360,7 +362,7 @@ The architecture above is fixed up front. Implementation lands as modules, **eac
| 9 | **Transforms** ✅ landed | `scale` (uniform), `rotate` (degrees → radians), `translate`/`translateX`/`translateY` (utility units) `.tw` setters over the M3 transform render-chain wrapper (paint-only; T·R·S order). The `FwStyle` fields `scale`/`translate` were renamed `scaleFactor`/`translation` to free the setter names (§6.1); `ResolvedStyle` unchanged. Unit (`fw_transform_ops_test`) + golden (`transform_slice`, light/dark). |
| 10 | **Animated theming** ✅ landed | `FwAnimatedTheme` — an **`ImplicitlyAnimatedWidget`** (Material-free) that tweens between the old and new `FwTokens` via `FwTokens.lerp` over a `duration` (default 200ms) / `curve` whenever the `tokens` change, providing the interpolated tokens down through `FwTheme` (drop-in for it). Does **not** ride Material's `ThemeData` animation (the pure path has none). Unit (`fw_animated_theme_test`, incl. linear-midpoint lerp + zero-duration) + golden (`theme_transition`, mid-transition pump). |
| 11 | **Text completeness** ✅ landed | `font`/`fontSans`/`fontSerif`/`fontMono` (family), `maxLines`, `lineClamp`, `truncate`, `overflow` (text-overflow), `nowrap`/`wrap` — all riding the M3 `DefaultTextStyle.merge`. Unit (`fw_text_ops_test`) + render-chain wiring. |
| 12 | **Filters + object-fit** ✅ landed | CSS color filters `grayscale`/`brightness`/`contrast`/`saturate`/`invert`/`sepia`/`hueRotate` composing to one `ColorFilter.matrix`, plus `fit(BoxFit)`. Unit (`fw_filter_ops_test`, matrix math) + render-chain wiring. |
| 12 | **Filters + object-fit** ✅ landed | CSS color filters `grayscale`/`brightness`/`contrast`/`saturate`/`invert`/`sepia`/`hueRotate` composing to one `ColorFilter.matrix`, plus `fit(BoxFit, {alignment})` (object-fit + object-position). Unit (`fw_filter_ops_test`, matrix math) + render-chain wiring. |
| 13 | **Transform extras + interactivity** ✅ landed | `scaleX`/`scaleY`/`skewX`/`skewY`/`transformOrigin`; `cursor`/`pointerEventsNone`/`invisible`/`visible`; `italic`/`notItalic`; `size` sugar. Unit (`fw_misc_ops_test`) + render-chain wiring; **Interactivity** showcase section. |
| 14 | **Group / peer state propagation** ✅ landed | `FwGroup`/`FwPeer` widgets (one scope, two channels — Flutter has no sibling selectors), the `FwGroupCondition`/`FwRelation` resolver member (state tier, per-channel disabled suppression, named groups/peers), and the `groupHover`/`groupFocus`/`groupPressed`/`groupDisabled`/`groupState` + `peer*` setters. Reactor plumbing (`fwReadRelationStates`) is `hide`-n from the barrel (§8). Unit (`fw_layer`/`resolve`/`fw_style_ops`) + live widget tests (`fw_group_test`: group/peer/named/injected/reparent/union/removal/disabled/RTL/assert) + golden (`group_slice`) + **Group & peer** showcase section. See `2026-06-07-flutterwindcss-m14-group-peer-design.md`. |
| 15 | **Ergonomics + completeness** ✅ landed | Tailwind muscle-memory layer: gradient direction sugar (`bgGradientToTop/Bottom/Start/End` + diagonals + `bgLinear`, RTL-aware); `ring(width, {color, offset, offsetColor})` (zero-blur spread `FwRing`, composed with `shadow`); named-scale `shadowXs2/Xs/Sm/Md/Lg/Xl/2xl`/`shadowNone` + `roundedSm/Md/Lg/Xl` (theme-resolved at build via a **gated** `FwStyled` pass — the one opt-in theme read; `resolve()` stays context-free); `FwScroll` (`overflow-auto/scroll`, Material-free `SingleChildScrollView`+`RawScrollbar`); `borderDashed`/`borderDotted` (`FwDashedBorderPainter`, uniform). `space-x/y` needs no API (`gap` is the equivalent). Unit + render + golden (`sugar_slice`) + `fw_scroll_test`/`fw_dashed_border_test`/`fw_ring_test`/`fw_token_sugar_test`/`fw_sugar_ops_test`; **Utilities** showcase section. |
Expand Down
4 changes: 4 additions & 0 deletions packages/flutterwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
remains a literal override. See the new **Fonts** docs page.
- **New utilities:** `overline` (Tailwind `overline` text-decoration), `fit(BoxFit, {alignment})`
(Tailwind `object-{position}`), and `FwScroll(trackColor:)` (the scrollbar track colour).
- **No theme? No problem.** The theme-role sugars (`fontSans`/`roundedMd`/`shadowMd`) now fall back to
the stock `FwTokens.light` defaults when no theme is present instead of throwing — so the raw
palette + your own fonts work with no `FwTheme` at all. New `context.fwOrNull` reads tokens without
throwing when a theme is optional. Added a `SKILLS.md` guide for agentic development.

## 1.0.2

Expand Down
2 changes: 2 additions & 0 deletions packages/flutterwindcss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ dropped.
[pub.dev package page](https://pub.dev/documentation/flutterwindcss/latest/).
- **A runnable example** is in [`example/`](example/) (and a larger showcase in
[`apps/example`](https://github.com/SiphoChris/flutterbits/tree/main/apps/example)).
- **Building with an AI agent?** [`SKILLS.md`](SKILLS.md) is a compact guide (rules, patterns,
gotchas) that helps coding agents use the `.tw` API and tokens correctly.

## License

Expand Down
Loading
Loading