diff --git a/apps/docs/content/docs/flutterwindcss/coverage.mdx b/apps/docs/content/docs/flutterwindcss/coverage.mdx
index 2ca70fc..6e83bec 100644
--- a/apps/docs/content/docs/flutterwindcss/coverage.mdx
+++ b/apps/docs/content/docs/flutterwindcss/coverage.mdx
@@ -32,12 +32,15 @@ needs `w-[37px]`; here you just write `.w(37)`), and `bgGradient`/`shadow` are *
Each has a known Flutter mechanism and is scheduled when a real need appears — none is a daily-driver
and none is a wall: `inset-shadow`/`inset-ring`, `mask-*`, backdrop *color* filters, `columns`,
negative margins, `scroll-margin`/`scroll-padding`/`overscroll-behavior`, `scroll-snap-stop`/`-type`,
-`scrollbar-color`/`-gutter`, `order`, `bg-clip-text` (gradient text), `background-blend-mode`,
-`bg-repeat-space`/`-round`, the `drop-shadow` *filter*, `sticky`, `position: fixed`, `object-position`,
-`overline`, `font-stretch`, `border-double`, `text-indent`, `vertical-align`, `word-break`, and the
-**3D-transform completion set** (`backface-visibility`, `perspective-origin`, `transform-style`/
-`preserve-3d`, `scale-z`). The full item-by-item enumeration (with Flutter mechanisms) lives in the
-engine's [coverage & roadmap spec](https://github.com/SiphoChris/flutterbits/blob/main/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md).
+`scrollbar-gutter`, `order`, `bg-clip-text` (gradient text), `background-blend-mode`,
+`bg-repeat-space`/`-round`, the `drop-shadow` *filter*, `sticky`, `position: fixed`, `font-stretch`,
+`border-double`, `text-indent`, `vertical-align`, `word-break`, and the **3D-transform completion set**
+(`backface-visibility`, `perspective-origin`, `transform-style`/`preserve-3d`, `scale-z`). The full
+item-by-item enumeration (with Flutter mechanisms) lives in the engine's
+[coverage & roadmap spec](https://github.com/SiphoChris/flutterbits/blob/main/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md).
+
+> **Shipped in 1.0.3:** theme-resolved `font-sans`/`-serif`/`-mono` (+ `FwTheme` applies the theme
+> font), `overline`, `object-position` (`fit(..., alignment:)`), and the scrollbar `trackColor`.
### ↪️ Delegated by design
diff --git a/apps/docs/content/docs/flutterwindcss/effects-filters.mdx b/apps/docs/content/docs/flutterwindcss/effects-filters.mdx
index aec6f63..7518801 100644
--- a/apps/docs/content/docs/flutterwindcss/effects-filters.mdx
+++ b/apps/docs/content/docs/flutterwindcss/effects-filters.mdx
@@ -55,6 +55,8 @@ These **compose** within a chain (CSS `filter: a() b()` → matrix multiply), re
## Object-fit
`fit(BoxFit.cover)` maps Tailwind `object-cover` (etc.) for the child content — needs a bounded box.
+Pass `alignment:` for Tailwind `object-{position}` (e.g. `fit(BoxFit.cover, alignment:
+AlignmentDirectional.topStart)` ≈ `object-top object-left`, RTL-aware); defaults to center.
## Next
diff --git a/apps/docs/content/docs/flutterwindcss/fonts.mdx b/apps/docs/content/docs/flutterwindcss/fonts.mdx
new file mode 100644
index 0000000..7049aa5
--- /dev/null
+++ b/apps/docs/content/docs/flutterwindcss/fonts.mdx
@@ -0,0 +1,105 @@
+---
+title: Fonts
+description: How to wire the fonts a generated theme.dart names — bundle them or use google_fonts. flutterwindcss applies the theme's families automatically; you only register the font.
+---
+
+A generated `theme.dart` names the fonts the theme wants — for example:
+
+```dart
+const FwTypographyTheme _type = FwTypographyTheme(
+ sans: 'Outfit',
+ serif: 'Georgia',
+ mono: 'Geist Mono',
+);
+```
+
+These are **family-name strings**. flutterwindcss **bundles no fonts**, so there are two halves to
+getting them on screen:
+
+1. **Apply** the family to your text — *this is automatic.* `FwTheme` applies the theme's `sans`
+ family as the subtree's default, and `.tw.fontSans` / `.fontSerif` / `.fontMono` resolve to the
+ theme's families (just like `roundedMd`/`shadowMd` resolve radius/shadow). You don't wire anything.
+2. **Register** the font so Flutter actually *has* "Outfit" / "Geist Mono" — **this is your job**, and
+ it's the only step. Pick one of the two recipes below.
+
+
+ Georgia (and other system faces) are usually already present on the OS, so they need no
+ registration. You only register custom families like *Outfit* or *Geist Mono*.
+
+
+## Recipe 1 — bundle the font files (recommended)
+
+Predictable and offline. Drop the `.ttf`/`.otf` files in your app and declare them under the **exact**
+family names the theme uses:
+
+```yaml title="pubspec.yaml"
+flutter:
+ fonts:
+ - family: Outfit # must match typography.sans
+ fonts:
+ - asset: assets/fonts/Outfit-Regular.ttf
+ - asset: assets/fonts/Outfit-Bold.ttf
+ weight: 700
+ - family: Geist Mono # must match typography.mono
+ fonts:
+ - asset: assets/fonts/GeistMono-Regular.ttf
+```
+
+That's it — pass the generated theme and the fonts apply:
+
+```dart
+FwTheme(tokens: lightTheme, child: const HomeScreen());
+```
+
+The theme stays `const`, the names line up, and `.fontSerif` / `.fontMono` switch families correctly.
+
+## Recipe 2 — google_fonts (no asset files)
+
+[`google_fonts`](https://pub.dev/packages/google_fonts) fetches and caches fonts at runtime. The
+robust way to combine it with a generated theme is to build the theme's typography **from** the
+families `google_fonts` registers — so the names always match:
+
+```bash
+flutter pub add google_fonts
+```
+
+```dart title="theme.dart (edit the generated _type)"
+import 'package:google_fonts/google_fonts.dart';
+
+// Calling GoogleFonts.* registers the family and returns its name. Drop `const`
+// from _type (and the FwTokens that use it) since these aren't compile-time.
+final FwTypographyTheme _type = FwTypographyTheme(
+ sans: GoogleFonts.outfit().fontFamily!,
+ serif: GoogleFonts.ptSerif().fontFamily!,
+ mono: GoogleFonts.geistMono().fontFamily!,
+);
+```
+
+Because the theme's family strings are exactly what `google_fonts` registered, `FwTheme`'s default and
+`.fontSans` / `.fontSerif` / `.fontMono` all resolve to a font that's really there.
+
+
+ `google_fonts` registers each face under a family name; reading it back with `.fontFamily` (instead
+ of hardcoding `'Outfit'`) guarantees a match regardless of the package's internal naming. If you'd
+ rather keep the theme `const`, use Recipe 1.
+
+
+## Overriding a single family
+
+`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).
+
+## Interop path (MaterialApp)
+
+On the pure path `FwTheme` applies the default family for you. Inside a `MaterialApp`, the host owns
+the default text theme, so set the family there (e.g. `ThemeData(fontFamily: lightTheme.typography.sans)`
+or a `textTheme`); `.fontSans` / `.fontSerif` / `.fontMono` still resolve to the theme via `context.fw`.
+
+## Next steps
+
+
+
+
+
+
diff --git a/apps/docs/content/docs/flutterwindcss/layout.mdx b/apps/docs/content/docs/flutterwindcss/layout.mdx
index e1feac9..1efdfa2 100644
--- a/apps/docs/content/docs/flutterwindcss/layout.mdx
+++ b/apps/docs/content/docs/flutterwindcss/layout.mdx
@@ -58,6 +58,8 @@ FwScroll(
```
- `showScrollbar` (default `true`) and `alwaysShowScrollbar` (`overflow-scroll` vs `overflow-auto`).
+- `thumbColor` and `trackColor` theme the scrollbar (Tailwind `scrollbar-color`); a `trackColor`
+ shows the track and keeps the thumb visible.
- `padding`, `reverse`, and an external `controller` are all supported.
### Scroll-snap
diff --git a/apps/docs/content/docs/flutterwindcss/meta.json b/apps/docs/content/docs/flutterwindcss/meta.json
index 4759217..947273f 100644
--- a/apps/docs/content/docs/flutterwindcss/meta.json
+++ b/apps/docs/content/docs/flutterwindcss/meta.json
@@ -11,6 +11,7 @@
"styling",
"colors",
"theming",
+ "fonts",
"states",
"breakpoints",
"layout",
diff --git a/apps/docs/content/docs/flutterwindcss/typography.mdx b/apps/docs/content/docs/flutterwindcss/typography.mdx
index 1a6f2ac..2b09844 100644
--- a/apps/docs/content/docs/flutterwindcss/typography.mdx
+++ b/apps/docs/content/docs/flutterwindcss/typography.mdx
@@ -23,15 +23,19 @@ not em).
| flutterwindcss | Tailwind |
| --- | --- |
-| `font('Inter')` | `font-[Inter]` |
+| `font('Inter')` | `font-[Inter]` (literal family) |
| `fontSans` `fontSerif` `fontMono` | `font-sans` `font-serif` `font-mono` (getters) |
+`fontSans` / `fontSerif` / `fontMono` resolve to the **active theme's** families (and `FwTheme`
+applies the theme's `sans` as the default), so a pasted theme's fonts apply once you register them —
+see [Fonts](/docs/flutterwindcss/fonts). `font('Inter')` sets a literal family for one chain.
+
## Style & decoration
| flutterwindcss | Tailwind |
| --- | --- |
| `italic` `notItalic` | `italic` `not-italic` (getters) |
-| `underline` `lineThrough` | `underline` `line-through` (getters; combine) |
+| `underline` `lineThrough` `overline` | `underline` `line-through` `overline` (getters; combine) |
| `textShadow([Shadow(...)])` | `text-shadow-*` (v4) |
## Clamping & wrapping
diff --git a/apps/docs/src/lib/generator/emit/dart.test.ts b/apps/docs/src/lib/generator/emit/dart.test.ts
index f924f85..039bd08 100644
--- a/apps/docs/src/lib/generator/emit/dart.test.ts
+++ b/apps/docs/src/lib/generator/emit/dart.test.ts
@@ -26,10 +26,12 @@ describe('emitDart — structure', () => {
expect(dart).toContain("import 'package:flutterwindcss/flutterwindcss.dart';");
expect(dart).not.toContain('material.dart');
});
- it('emits the font names and a google_fonts wiring stub (never a silent bundle)', () => {
+ it('emits the font names and font-registration guidance (never a silent bundle)', () => {
expect(dart).toContain('Outfit');
expect(dart).toContain('Geist Mono');
- expect(dart).toMatch(/TODO:.*google_fonts/);
+ // The dev must REGISTER the fonts (bundle or google_fonts); the engine applies them.
+ expect(dart).toMatch(/REGISTER the fonts/);
+ expect(dart).toMatch(/google_fonts/);
});
it('omits tracking when 0 (FwTypographyTheme default)', () => {
expect(dart).not.toContain('tracking:');
diff --git a/apps/docs/src/lib/generator/emit/dart.ts b/apps/docs/src/lib/generator/emit/dart.ts
index 0bf1c60..0c87552 100644
--- a/apps/docs/src/lib/generator/emit/dart.ts
+++ b/apps/docs/src/lib/generator/emit/dart.ts
@@ -80,10 +80,11 @@ function header(json: ThemeJson): string {
`// Conversion: ${json.meta.conversion}.`,
'//',
`// Fonts named by this theme: ${t.sans} (sans), ${t.serif} (serif), ${t.mono} (mono).`,
- '// flutterwindcss bundles no fonts — the names round-trip, but you must wire',
- '// them yourself (or Flutter falls back to the platform family):',
- '// // TODO: add `google_fonts` to your pubspec and apply these families on',
- '// // FwTheme, or bundle the font files and declare them in pubspec.yaml.',
+ '// flutterwindcss applies these families for you (FwTheme uses `sans` as the',
+ '// default; .fontSans/.fontSerif/.fontMono resolve to the theme). You only need',
+ '// to REGISTER the fonts so Flutter has them — bundle the .ttf files in',
+ '// pubspec.yaml, or wire `google_fonts`. Custom faces only; system fonts (e.g.',
+ '// Georgia) need nothing. Guide: https://flutterbits.vercel.app/docs/flutterwindcss/fonts',
];
if (json.meta.droppedVars.length > 0) {
lines.push('//', `// Dropped unknown CSS vars: ${json.meta.droppedVars.join(', ')}.`);
diff --git a/apps/example/lib/showcase/sections/typography.dart b/apps/example/lib/showcase/sections/typography.dart
index 71eb348..26f3352 100644
--- a/apps/example/lib/showcase/sections/typography.dart
+++ b/apps/example/lib/showcase/sections/typography.dart
@@ -108,7 +108,8 @@ class TypographySection extends StatelessWidget {
children: [
Text('underline').tw.underline.text(t.colors.primary),
Text('lineThrough').tw.lineThrough.text(t.colors.mutedForeground),
- Text('both at once').tw.underline.lineThrough.text(t.colors.destructive),
+ Text('overline').tw.overline.text(t.colors.primary),
+ Text('all three').tw.underline.lineThrough.overline.text(t.colors.destructive),
],
),
),
diff --git a/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md b/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md
index d573adf..fc8cb63 100644
--- a/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md
+++ b/docs/superpowers/specs/2026-06-07-flutterwindcss-coverage-and-roadmap.md
@@ -135,8 +135,8 @@ daily-driver miss, and the first pass's verdicts are unchanged.
| Utility (v4) | Verdict | Flutter mechanism / reason | Size |
|---|---|---|---|
-| `object-position` (`object-top`/`-left`/…) | **By-demand** | `fit()` maps to `FittedBox` but hard-codes center; add an optional directional `alignment` param passed to `FittedBox.alignment`. | S |
-| `overline` (text-decoration-line) | **By-demand (trivial)** | `TextDecoration.overline` exists; the `underline`/`lineThrough` getters silently omit the third line — add an `overline` getter mirroring them via `_addDecoration`. | S |
+| `object-position` (`object-top`/`-left`/…) | **✅ BUILT (1.0.3)** | `fit(BoxFit, {alignment})` → `FittedBox.alignment` (directional, RTL-aware). | S |
+| `overline` (text-decoration-line) | **✅ BUILT (1.0.3)** | `overline` getter via `_addDecoration(TextDecoration.overline)`, combines with underline/line-through. | S |
| `underline-offset-*` | **No-analog** | Flutter `TextStyle` exposes `decorationThickness` but **no** underline-offset; only achievable via a custom text painter. (The first pass listed decoration color/style/thickness as By-demand but omitted offset, which is actually *less* feasible.) | — / M (painter) |
| `decoration-{color}` / `-{style}` / `-{thickness}` | **By-demand** | `TextStyle.decorationColor`/`decorationStyle`/`decorationThickness` (already noted L59-60; re-confirmed). | S |
| `font-stretch-*` | **By-demand** | Variable fonts only: `FontVariation('wdth', pct)` via `TextStyle.fontVariations`. Static fonts have no width axis. | S |
@@ -165,7 +165,7 @@ daily-driver miss, and the first pass's verdicts are unchanged.
| `color-scheme` | **Free/N-A** | The light/dark `FwTokens` selection the host drives **is** color-scheme; no separate utility needed. | — |
| `scroll-snap-stop` (`snap-always`/`-normal`) | **By-demand** | A bool on `_FwSnapPhysics` clamping the fling target to ±1 page. | S |
| `scroll-snap-type` (`snap-mandatory`/`-proximity`/`-none`/axis) | **By-demand** | Axis = `FwScroll.axis`; `snapExtent` already implies mandatory-on-axis; proximity = a threshold in `createBallisticSimulation`; `none` = `snapExtent: null`. | S–M |
-| `scrollbar-color` | **By-demand (half-built)** | `FwScroll.thumbColor` already wired; expose `RawScrollbar.trackColor` too. | S |
+| `scrollbar-color` | **✅ BUILT (1.0.3)** | `FwScroll.thumbColor` + `trackColor` → `RawScrollbar` (track implies a visible thumb). | S |
| `scrollbar-gutter` | **By-demand** | Reserve scrollbar-thickness padding (already named L58). | S |
| `scroll-behavior` (`scroll-smooth`) | **By-demand** | Already listed L111 (`ScrollController.animateTo`). | S |
| SVG `fill` / `stroke` / **`stroke-width`** | **Delegate / By-demand** | `fill`/`stroke` are icon theming (`Icon(color:)` / `lucide_icons_flutter`, a sanctioned dep) → flutterbits; `stroke-width` (`Paint.strokeWidth` in `CustomPaint`/`flutter_svg`) was unnamed — now recorded. | S |
@@ -268,7 +268,7 @@ complete; the next work is Tier 2 (by demand).
| Utility | Tailwind | Flutter mechanism | Home | Size | Status |
|---|---|---|---|---|---|
| **Line-clamp / truncate / text-overflow** | `line-clamp-N`, `truncate`, `text-ellipsis`, `text-clip` | `DefaultTextStyle.merge` carries `maxLines`/`overflow`/`softWrap` — fields + setters `maxLines`/`lineClamp`/`truncate`/`overflow` | `.tw` typography | **S** | ✅ module 11 |
-| **Font family** | `font-sans/serif/mono`, `font-[...]` | `TextStyle.fontFamily` via the same `DefaultTextStyle.merge` — setters `font`/`fontSans`/`fontSerif`/`fontMono` | `.tw` typography | **S** | ✅ module 11 |
+| **Font family** | `font-sans/serif/mono`, `font-[...]` | `TextStyle.fontFamily` via `DefaultTextStyle.merge` — `font(String)` literal; `fontSans`/`fontSerif`/`fontMono` are **theme-resolved** (1.0.3) and `FwTheme` applies the theme `sans` default | `.tw` typography | **S** | ✅ module 11 (theme-resolved 1.0.3) |
| **Whitespace / wrapping** | `whitespace-nowrap`, `whitespace-normal` | `softWrap` via `DefaultTextStyle.merge` — setters `nowrap`/`wrap` | `.tw` typography | **S** | ✅ module 11 |
| **Color filters** | `brightness/contrast/saturate/grayscale/invert/sepia/hue-rotate` | `ColorFiltered` + `ColorFilter.matrix`, composed within a chain (same render-chain slot as the existing blur `ImageFiltered`) | `.tw` effects | **M** | ✅ module 12 |
| **Object-fit** | `object-cover/contain/fill/...` | wrap child in `FittedBox(fit: BoxFit.*)` — setter `fit` | `.tw` (`fit()`) | **S–M** | ✅ module 12 |
diff --git a/packages/flutterwindcss/CHANGELOG.md b/packages/flutterwindcss/CHANGELOG.md
index d8eac92..1aebb26 100644
--- a/packages/flutterwindcss/CHANGELOG.md
+++ b/packages/flutterwindcss/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 1.0.3
+
+- **Theme fonts now apply.** `FwTheme` applies the theme's `sans` family as the subtree's default text
+ family, and `fontSans`/`fontSerif`/`fontMono` resolve to the active theme's families (like
+ `roundedMd`/`shadowMd` do for radius/shadow) instead of generic names. A pasted/generated theme's
+ fonts take effect once you register the font (bundle it or wire `google_fonts`). `font('Family')`
+ 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).
+
## 1.0.2
- **Docs** — the install instructions are now version-agnostic (`flutter pub add flutterwindcss`)
diff --git a/packages/flutterwindcss/lib/src/layout/fw_scroll.dart b/packages/flutterwindcss/lib/src/layout/fw_scroll.dart
index c328c97..76af3ee 100644
--- a/packages/flutterwindcss/lib/src/layout/fw_scroll.dart
+++ b/packages/flutterwindcss/lib/src/layout/fw_scroll.dart
@@ -37,6 +37,7 @@ class FwScroll extends StatefulWidget {
this.physics,
this.reverse = false,
this.thumbColor,
+ this.trackColor,
this.snapExtent,
this.snapAlign = FwSnapAlign.start,
super.key,
@@ -75,6 +76,11 @@ class FwScroll extends StatefulWidget {
/// token such as `context.fw.colors.border` for a themed scrollbar.
final Color? thumbColor;
+ /// Scrollbar **track** colour (the groove behind the thumb; Tailwind
+ /// `scrollbar-color`'s track half). When set, the track is shown; `null` (the
+ /// default) leaves the track hidden, matching `overflow-auto`.
+ final Color? trackColor;
+
/// Item size in logical px to snap to (Tailwind scroll-snap / `snap-*`). When
/// set, the scroll settles on multiples of this extent — the carousel pattern
/// (uniform-size items). `null` = free scrolling. Must be `> 0`.
@@ -128,8 +134,13 @@ class _FwScrollState extends State {
if (widget.showScrollbar) {
content = RawScrollbar(
controller: _controller,
- thumbVisibility: widget.alwaysShowScrollbar,
+ // A visible track implies a visible thumb (a track behind no thumb is
+ // meaningless, and RawScrollbar asserts against it).
+ thumbVisibility: widget.alwaysShowScrollbar || widget.trackColor != null,
thumbColor: widget.thumbColor,
+ trackColor: widget.trackColor,
+ // Show the track only when a colour is given (otherwise stay auto/hidden).
+ trackVisibility: widget.trackColor != null ? true : null,
child: content,
);
}
diff --git a/packages/flutterwindcss/lib/src/style/fw_style.dart b/packages/flutterwindcss/lib/src/style/fw_style.dart
index 4c7ef94..e82cfc1 100644
--- a/packages/flutterwindcss/lib/src/style/fw_style.dart
+++ b/packages/flutterwindcss/lib/src/style/fw_style.dart
@@ -24,9 +24,9 @@ typedef FwLayer = (FwCondition condition, FwStyle style);
/// only *set or change* a field — it cannot reset one back to "unset". Fields with
/// an explicit inverse setter can be re-set in a layer (`notItalic`, `visible`,
/// `wrap`, `borderSolid`, `shadowNone`, `roundedNone`); fields without one
-/// (`maxLines`/`lineClamp`, `aspectRatio`, `fit`, `blendMode`, the color filters,
-/// `mouseCursor`, the transform fields, fractional `align`) can be *overridden* in
-/// a layer but not *cleared*. Set the desired base value on the base style instead.
+/// (`maxLines`/`lineClamp`, `aspectRatio`, `fit`/`fitAlignment`, `blendMode`, the
+/// color filters, `mouseCursor`, the transform fields, fractional `align`) can be
+/// *overridden* in a layer but not *cleared*. Set the desired base value instead.
/// (This is why Tailwind's `line-clamp-none` has no layer-level equivalent — it is
/// one instance of this general rule, not a special case.)
@immutable
@@ -64,6 +64,7 @@ class FwStyle with FwStyleOps {
this.textDecoration,
this.textShadows,
this.fontFamily,
+ this.fontFamilyStep,
this.fontStyle,
this.maxLineCount,
this.textOverflow,
@@ -85,6 +86,7 @@ class FwStyle with FwStyleOps {
this.colorMatrix,
this.mixBlendMode,
this.boxFit,
+ this.fitAlignment,
this.mouseCursor,
this.ignorePointer,
this.isVisible,
@@ -197,9 +199,15 @@ class FwStyle with FwStyleOps {
/// `textShadow` (module 17).
final List? textShadows;
- /// Default font family (set by `font`/`fontSans`/`fontSerif`/`fontMono`).
+ /// Default font family as a literal name (set by `font(String)`).
final String? fontFamily;
+ /// Named font role (set by `fontSans`/`fontSerif`/`fontMono`); resolved against
+ /// the theme's [FwTypographyTheme] into [fontFamily] by `FwStyled` at build, so
+ /// the utilities track the active theme's families. Mutually exclusive with a
+ /// literal [fontFamily] in the same node (asserts).
+ final FwFontStep? fontFamilyStep;
+
/// Default font style (italic/normal; set by `italic`/`notItalic`).
final FontStyle? fontStyle;
@@ -283,6 +291,11 @@ class FwStyle with FwStyleOps {
/// so the Tailwind-natural `fit` setter doesn't collide with the field).
final BoxFit? boxFit;
+ /// Alignment of the fitted child within its box (Tailwind `object-{position}`;
+ /// set by the optional `alignment` of the `fit` setter). Defaults to center at
+ /// resolve time. Directional (RTL-aware) when an `AlignmentDirectional`.
+ final AlignmentGeometry? fitAlignment;
+
/// Mouse cursor over the box (set by `cursor`; Tailwind `cursor-*`). Named
/// `mouseCursor` so the `cursor` setter doesn't collide with the field.
final MouseCursor? mouseCursor;
@@ -351,6 +364,7 @@ class FwStyle with FwStyleOps {
TextDecoration? textDecoration,
List? textShadows,
String? fontFamily,
+ FwFontStep? fontFamilyStep,
FontStyle? fontStyle,
int? maxLineCount,
TextOverflow? textOverflow,
@@ -372,6 +386,7 @@ class FwStyle with FwStyleOps {
List? colorMatrix,
BlendMode? mixBlendMode,
BoxFit? boxFit,
+ AlignmentGeometry? fitAlignment,
MouseCursor? mouseCursor,
bool? ignorePointer,
bool? isVisible,
@@ -410,6 +425,7 @@ class FwStyle with FwStyleOps {
textDecoration: textDecoration ?? this.textDecoration,
textShadows: textShadows ?? this.textShadows,
fontFamily: fontFamily ?? this.fontFamily,
+ fontFamilyStep: fontFamilyStep ?? this.fontFamilyStep,
fontStyle: fontStyle ?? this.fontStyle,
maxLineCount: maxLineCount ?? this.maxLineCount,
textOverflow: textOverflow ?? this.textOverflow,
@@ -431,6 +447,7 @@ class FwStyle with FwStyleOps {
colorMatrix: colorMatrix ?? this.colorMatrix,
mixBlendMode: mixBlendMode ?? this.mixBlendMode,
boxFit: boxFit ?? this.boxFit,
+ fitAlignment: fitAlignment ?? this.fitAlignment,
mouseCursor: mouseCursor ?? this.mouseCursor,
ignorePointer: ignorePointer ?? this.ignorePointer,
isVisible: isVisible ?? this.isVisible,
@@ -477,6 +494,7 @@ class FwStyle with FwStyleOps {
textDecoration == other.textDecoration &&
listEquals(textShadows, other.textShadows) &&
fontFamily == other.fontFamily &&
+ fontFamilyStep == other.fontFamilyStep &&
fontStyle == other.fontStyle &&
maxLineCount == other.maxLineCount &&
textOverflow == other.textOverflow &&
@@ -498,6 +516,7 @@ class FwStyle with FwStyleOps {
listEquals(colorMatrix, other.colorMatrix) &&
mixBlendMode == other.mixBlendMode &&
boxFit == other.boxFit &&
+ fitAlignment == other.fitAlignment &&
mouseCursor == other.mouseCursor &&
ignorePointer == other.ignorePointer &&
isVisible == other.isVisible &&
@@ -537,6 +556,7 @@ class FwStyle with FwStyleOps {
textDecoration,
textShadows == null ? null : Object.hashAll(textShadows!),
fontFamily,
+ fontFamilyStep,
fontStyle,
maxLineCount,
textOverflow,
@@ -558,6 +578,7 @@ class FwStyle with FwStyleOps {
colorMatrix == null ? null : Object.hashAll(colorMatrix!),
mixBlendMode,
boxFit,
+ fitAlignment,
mouseCursor,
ignorePointer,
isVisible,
diff --git a/packages/flutterwindcss/lib/src/style/fw_style_ops.dart b/packages/flutterwindcss/lib/src/style/fw_style_ops.dart
index e7f0b7e..08a067f 100644
--- a/packages/flutterwindcss/lib/src/style/fw_style_ops.dart
+++ b/packages/flutterwindcss/lib/src/style/fw_style_ops.dart
@@ -3,7 +3,6 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import '../tokens/scales.dart';
-import '../tokens/typography.dart';
import 'fw_border_spec.dart';
import 'fw_layer.dart';
import 'fw_ring.dart';
@@ -590,6 +589,10 @@ mixin FwStyleOps {
/// (Tailwind `line-through`).
T get lineThrough => _addDecoration(TextDecoration.lineThrough);
+ /// Draws a line over descendant text; combines with any existing decoration
+ /// (Tailwind `overline`).
+ T get overline => _addDecoration(TextDecoration.overline);
+
/// Text shadow(s) for descendant text (Tailwind `text-shadow-*`, v4). Pass a
/// `List` (an empty list = none). Flows through `DefaultTextStyle`, so
/// it inherits into descendant text like the other typography setters. Last-wins.
@@ -601,18 +604,23 @@ mixin FwStyleOps {
// `maxLines`, `overflow`, `softWrap`), so they inherit into descendant text
// exactly like the other typography setters.
- /// Default font family for descendant text (Tailwind `font-[family]`); pass a
- /// token such as `FwFontFamily.sans`. Last-wins.
+ /// Default font family for descendant text as a **literal** name (Tailwind
+ /// `font-[family]`), e.g. `font('Inter')`. For the theme's roles use
+ /// [fontSans]/[fontSerif]/[fontMono]. Last-wins; mutually exclusive with a font
+ /// role in the same node (asserts at resolve).
T font(String family) => fwRebuild(fwStyle.copyWith(fontFamily: family));
- /// Sans-serif family (Tailwind `font-sans`).
- T get fontSans => font(FwFontFamily.sans);
+ /// The theme's **sans** family (Tailwind `font-sans`). Resolves to
+ /// `context.fw.typography.sans` at build, so it tracks a pasted theme's font.
+ T get fontSans => fwRebuild(fwStyle.copyWith(fontFamilyStep: FwFontStep.sans));
- /// Serif family (Tailwind `font-serif`).
- T get fontSerif => font(FwFontFamily.serif);
+ /// The theme's **serif** family (Tailwind `font-serif`). Resolves to
+ /// `context.fw.typography.serif` at build.
+ T get fontSerif => fwRebuild(fwStyle.copyWith(fontFamilyStep: FwFontStep.serif));
- /// Monospace family (Tailwind `font-mono`).
- T get fontMono => font(FwFontFamily.mono);
+ /// The theme's **mono** family (Tailwind `font-mono`). Resolves to
+ /// `context.fw.typography.mono` at build.
+ T get fontMono => fwRebuild(fwStyle.copyWith(fontFamilyStep: FwFontStep.mono));
/// Italic text (Tailwind `italic`).
T get italic => fwRebuild(fwStyle.copyWith(fontStyle: FontStyle.italic));
@@ -812,7 +820,12 @@ mixin FwStyleOps {
/// fit into (set a size, or place it where the parent constrains it); under an
/// unbounded constraint on the fitted axis it safely degrades to no scaling
/// (the child renders at its natural size) rather than throwing.
- T fit(BoxFit fit) => fwRebuild(fwStyle.copyWith(boxFit: fit));
+ ///
+ /// [alignment] positions the fitted child within the box (Tailwind
+ /// `object-{top,left,…}`); defaults to center. Pass an `AlignmentDirectional`
+ /// (e.g. `AlignmentDirectional.topStart`) to stay RTL-aware.
+ T fit(BoxFit fit, {AlignmentGeometry? alignment}) =>
+ fwRebuild(fwStyle.copyWith(boxFit: fit, fitAlignment: alignment));
// ---- Interactivity + visibility (module 13) ----
diff --git a/packages/flutterwindcss/lib/src/style/fw_styled.dart b/packages/flutterwindcss/lib/src/style/fw_styled.dart
index 4299460..8801df1 100644
--- a/packages/flutterwindcss/lib/src/style/fw_styled.dart
+++ b/packages/flutterwindcss/lib/src/style/fw_styled.dart
@@ -84,12 +84,13 @@ class FwStyled extends StatelessWidget with FwStyleOps {
/// does NOT trigger this box's own live sourcing.
bool get _needsRelationStates => _anyCondition((c) => c.isRelation);
- /// Whether any style node (base or nested layer) carries a radius/shadow *step*
- /// (module 15 named-scale sugar). The only thing that makes `FwStyled` read the
- /// theme (`context.fw`) — gated so a non-sugar box stays theme-agnostic.
+ /// Whether any style node (base or nested layer) carries a radius/shadow/font
+ /// *step* (the theme-resolved sugar — `roundedMd`/`shadowMd`/`fontSans`). The
+ /// only thing that makes `FwStyled` read the theme (`context.fw`) — gated so a
+ /// non-sugar box stays theme-agnostic.
bool get _needsTokenSteps {
bool walk(FwStyle s) {
- if (s.radiusStep != null || s.shadowStep != null) return true;
+ if (s.radiusStep != null || s.shadowStep != null || s.fontFamilyStep != null) return true;
for (final (_, nested) in s.layers) {
if (walk(nested)) return true;
}
diff --git a/packages/flutterwindcss/lib/src/style/fw_token_steps.dart b/packages/flutterwindcss/lib/src/style/fw_token_steps.dart
index 8ac3a4f..70c0cc7 100644
--- a/packages/flutterwindcss/lib/src/style/fw_token_steps.dart
+++ b/packages/flutterwindcss/lib/src/style/fw_token_steps.dart
@@ -69,3 +69,27 @@ enum FwShadowStep {
FwShadowStep.xl2 => tokens.shadows.xl2,
};
}
+
+/// Named font roles for the `fontSans`/`fontSerif`/`fontMono` utilities
+/// (Tailwind `font-sans`/`font-serif`/`font-mono`). A role resolves against the
+/// active theme's [FwTypographyTheme] at build time, so `fontSans` tracks the
+/// theme's `sans` family exactly like `font(context.fw.typography.sans)` — this
+/// is what lets a pasted theme's fonts apply. (`font(String)` stays a literal
+/// family for an arbitrary face, e.g. `font-[Inter]`.)
+enum FwFontStep {
+ /// `context.fw.typography.sans`.
+ sans,
+
+ /// `context.fw.typography.serif`.
+ serif,
+
+ /// `context.fw.typography.mono`.
+ mono;
+
+ /// The family name for this role under [tokens].
+ String resolve(FwTokens tokens) => switch (this) {
+ FwFontStep.sans => tokens.typography.sans,
+ FwFontStep.serif => tokens.typography.serif,
+ FwFontStep.mono => tokens.typography.mono,
+ };
+}
diff --git a/packages/flutterwindcss/lib/src/style/resolve.dart b/packages/flutterwindcss/lib/src/style/resolve.dart
index c2e42e2..31bc656 100644
--- a/packages/flutterwindcss/lib/src/style/resolve.dart
+++ b/packages/flutterwindcss/lib/src/style/resolve.dart
@@ -41,6 +41,15 @@ extension FwStyleTokenResolve on FwStyle {
);
out = out.copyWith(boxShadow: shadowStep!.resolve(tokens));
}
+ if (fontFamilyStep != null) {
+ assert(
+ fontFamily == null,
+ 'flutterwindcss: do not mix a font role (fontSans/Serif/Mono) with a '
+ 'literal font(family) in the same chain — last-wins cannot order two '
+ 'fields. Pick one.',
+ );
+ out = out.copyWith(fontFamily: fontFamilyStep!.resolve(tokens));
+ }
return out;
}
}
@@ -57,6 +66,7 @@ extension FwStyleTokenResolve on FwStyle {
bool _hasUnresolvedTokenSteps(FwStyle style) {
if (style.radiusStep != null && style.borderRadius == null) return true;
if (style.shadowStep != null && style.boxShadow == null) return true;
+ if (style.fontFamilyStep != null && style.fontFamily == null) return true;
for (final (_, nested) in style.layers) {
if (_hasUnresolvedTokenSteps(nested)) return true;
}
@@ -176,6 +186,7 @@ extension FwStyleResolve on FwStyle {
colorMatrix: merged.colorMatrix,
blendMode: merged.mixBlendMode,
fit: merged.boxFit,
+ fitAlignment: merged.fitAlignment,
mouseCursor: merged.mouseCursor,
ignorePointer: merged.ignorePointer,
isVisible: merged.isVisible,
@@ -319,6 +330,7 @@ FwStyle _overlay(FwStyle base, FwStyle top) => base.copyWith(
colorMatrix: top.colorMatrix,
mixBlendMode: top.mixBlendMode,
boxFit: top.boxFit,
+ fitAlignment: top.fitAlignment,
mouseCursor: top.mouseCursor,
ignorePointer: top.ignorePointer,
isVisible: top.isVisible,
diff --git a/packages/flutterwindcss/lib/src/style/resolved_style.dart b/packages/flutterwindcss/lib/src/style/resolved_style.dart
index 2c8290b..8f9c91b 100644
--- a/packages/flutterwindcss/lib/src/style/resolved_style.dart
+++ b/packages/flutterwindcss/lib/src/style/resolved_style.dart
@@ -70,6 +70,7 @@ class ResolvedStyle {
this.colorMatrix,
this.blendMode,
this.fit,
+ this.fitAlignment,
this.mouseCursor,
this.ignorePointer,
this.isVisible,
@@ -226,6 +227,10 @@ class ResolvedStyle {
/// Object-fit for the content (→ `FittedBox`).
final BoxFit? fit;
+ /// Alignment of the fitted content within the box (→ `FittedBox.alignment`;
+ /// Tailwind `object-{position}`). Null → center.
+ final AlignmentGeometry? fitAlignment;
+
/// Mouse cursor over the box (→ `MouseRegion`).
final MouseCursor? mouseCursor;
diff --git a/packages/flutterwindcss/lib/src/style/resolved_style_build.dart b/packages/flutterwindcss/lib/src/style/resolved_style_build.dart
index 74ee9bf..f2b1cb5 100644
--- a/packages/flutterwindcss/lib/src/style/resolved_style_build.dart
+++ b/packages/flutterwindcss/lib/src/style/resolved_style_build.dart
@@ -81,8 +81,9 @@ extension ResolvedStyleBuild on ResolvedStyle {
}
// Object-fit: scale the content to fit its content box (inside padding).
+ // `fitAlignment` (Tailwind object-{position}) positions it; default center.
if (fit != null) {
- current = FittedBox(fit: fit!, child: current);
+ current = FittedBox(fit: fit!, alignment: fitAlignment ?? Alignment.center, child: current);
}
// Inner padding.
diff --git a/packages/flutterwindcss/lib/src/theme/fw_theme.dart b/packages/flutterwindcss/lib/src/theme/fw_theme.dart
index cb71636..49d3fe4 100644
--- a/packages/flutterwindcss/lib/src/theme/fw_theme.dart
+++ b/packages/flutterwindcss/lib/src/theme/fw_theme.dart
@@ -2,9 +2,19 @@ import 'package:flutter/widgets.dart';
import '../tokens/tokens.dart';
-/// The Material-free theme provider: an [InheritedWidget] carrying the active
-/// [FwTokens] down the tree (spec §5.1). This is the **pure path** — it works
-/// in a bare `WidgetsApp` with no Material dependency.
+/// The Material-free theme provider: carries the active [FwTokens] down the tree
+/// (spec §5.1) **and** applies the theme's body (`sans`) font family as the
+/// subtree's default text style. This is the **pure path** — it works in a bare
+/// `WidgetsApp` with no Material dependency.
+///
+/// Applying `typography.sans` as the default means a pasted/generated theme's
+/// fonts take effect automatically (you still *register* the font — bundle it in
+/// `pubspec.yaml` or wire `google_fonts`; flutterwindcss ships none). The default
+/// is *merged*, so any ambient text color/size is preserved — only the family is
+/// set — and `.tw.fontSerif`/`.fontMono` switch to the theme's other families.
+/// (On the interop path the host's `MaterialApp` owns the default text theme, so
+/// there is no `FwTheme` wrapper to apply this — set the family on your
+/// `ThemeData` `textTheme` there.)
///
/// Light/dark switching is the host app's job (AGENTS.md §5): the host rebuilds
/// with whichever [FwTokens] instance is active. For animated transitions the
@@ -13,19 +23,43 @@ import '../tokens/tokens.dart';
/// Components never read this directly; they use `context.fw`, which resolves
/// [FwTheme] first and falls back to the Material `FwThemeExtension`
/// (AGENTS.md §3.4).
-class FwTheme extends InheritedWidget {
- /// Provides [tokens] to [child] and its descendants.
- const FwTheme({required this.tokens, required super.child, super.key});
+class FwTheme extends StatelessWidget {
+ /// Provides [tokens] to [child] and its descendants, and sets the theme's
+ /// `sans` family as the default text family for the subtree.
+ const FwTheme({required this.tokens, required this.child, super.key});
/// The active token bundle for this subtree.
final FwTokens tokens;
+ /// The subtree that reads the tokens via `context.fw`.
+ final Widget child;
+
/// The nearest [FwTheme]'s [tokens], or `null` if there is none above
/// [context]. Registers [context] as a dependent, so it rebuilds when the
/// provided tokens change.
static FwTokens? maybeOf(BuildContext context) =>
- context.dependOnInheritedWidgetOfExactType()?.tokens;
+ context.dependOnInheritedWidgetOfExactType<_FwThemeScope>()?.tokens;
+
+ @override
+ Widget build(BuildContext context) {
+ return _FwThemeScope(
+ tokens: tokens,
+ // Merge so only the family is set — ambient color/size/alignment are kept.
+ child: DefaultTextStyle.merge(
+ style: TextStyle(fontFamily: tokens.typography.sans),
+ child: child,
+ ),
+ );
+ }
+}
+
+/// The `InheritedWidget` that actually carries [FwTokens]; private so the lookup
+/// goes through [FwTheme.maybeOf] / `context.fw` (the supported surface).
+class _FwThemeScope extends InheritedWidget {
+ const _FwThemeScope({required this.tokens, required super.child});
+
+ final FwTokens tokens;
@override
- bool updateShouldNotify(FwTheme oldWidget) => oldWidget.tokens != tokens;
+ bool updateShouldNotify(_FwThemeScope oldWidget) => oldWidget.tokens != tokens;
}
diff --git a/packages/flutterwindcss/lib/src/tokens/typography.dart b/packages/flutterwindcss/lib/src/tokens/typography.dart
index 9b0bf5d..d4a362c 100644
--- a/packages/flutterwindcss/lib/src/tokens/typography.dart
+++ b/packages/flutterwindcss/lib/src/tokens/typography.dart
@@ -124,8 +124,10 @@ abstract final class FwLeading {
/// Font-family *names* only — the engine never bundles fonts (spec §4.5).
abstract final class FwFontFamily {
- /// Default UI sans family name. Flutter resolves this generic name to the
- /// platform UI font; the host overrides it in FwTheme for a custom face.
+ /// The generic fallback family names that [FwTypographyTheme] defaults to.
+ /// Flutter resolves these to the platform faces. A theme supplies custom faces
+ /// via [FwTypographyTheme] (which `FwTheme` applies and `fontSans`/`fontSerif`/
+ /// `fontMono` resolve to); `font('Family')` sets a literal family directly.
static const String sans = 'sans-serif';
/// Serif family name.
diff --git a/packages/flutterwindcss/pubspec.yaml b/packages/flutterwindcss/pubspec.yaml
index bb6805c..c0e5e0f 100644
--- a/packages/flutterwindcss/pubspec.yaml
+++ b/packages/flutterwindcss/pubspec.yaml
@@ -2,7 +2,7 @@ name: flutterwindcss
description: >-
Tailwind CSS v4 design system and styling vocabulary for Flutter — tokens,
theming, and a typed utility API over the widgets layer.
-version: 1.0.2
+version: 1.0.3
repository: https://github.com/SiphoChris/flutterbits
issue_tracker: https://github.com/SiphoChris/flutterbits/issues
topics:
diff --git a/packages/flutterwindcss/test/style/fw_filter_ops_test.dart b/packages/flutterwindcss/test/style/fw_filter_ops_test.dart
index 25712f0..140be99 100644
--- a/packages/flutterwindcss/test/style/fw_filter_ops_test.dart
+++ b/packages/flutterwindcss/test/style/fw_filter_ops_test.dart
@@ -110,5 +110,13 @@ void main() {
expect(const FwStyle().fit(BoxFit.cover).boxFit, BoxFit.cover);
expect(const FwStyle().fit(BoxFit.cover).fit(BoxFit.contain).boxFit, BoxFit.contain);
});
+
+ test('fit alignment (object-position) defaults null, stores directionally', () {
+ expect(const FwStyle().fit(BoxFit.cover).fitAlignment, isNull);
+ expect(
+ const FwStyle().fit(BoxFit.cover, alignment: AlignmentDirectional.topStart).fitAlignment,
+ AlignmentDirectional.topStart,
+ );
+ });
});
}
diff --git a/packages/flutterwindcss/test/style/fw_scroll_test.dart b/packages/flutterwindcss/test/style/fw_scroll_test.dart
index 359db18..a9ae59d 100644
--- a/packages/flutterwindcss/test/style/fw_scroll_test.dart
+++ b/packages/flutterwindcss/test/style/fw_scroll_test.dart
@@ -81,6 +81,41 @@ void main() {
expect(find.byType(SingleChildScrollView), findsOneWidget);
});
+ testWidgets('thumbColor + trackColor flow to RawScrollbar (track shown)', (t) async {
+ await t.pumpWidget(
+ _wrap(
+ FwScroll(
+ thumbColor: const Color(0xFF112233),
+ trackColor: const Color(0xFF445566),
+ child: Column(
+ children: List.generate(40, (i) => SizedBox(height: 20, child: Text('$i'))),
+ ),
+ ),
+ ),
+ );
+ final bar = t.widget(find.byType(RawScrollbar));
+ expect(bar.thumbColor, const Color(0xFF112233));
+ expect(bar.trackColor, const Color(0xFF445566));
+ expect(bar.trackVisibility, isTrue);
+ // A visible track implies a visible thumb.
+ expect(bar.thumbVisibility, isTrue);
+ });
+
+ testWidgets('no trackColor leaves the track hidden (auto)', (t) async {
+ await t.pumpWidget(
+ _wrap(
+ FwScroll(
+ child: Column(
+ children: List.generate(40, (i) => SizedBox(height: 20, child: Text('$i'))),
+ ),
+ ),
+ ),
+ );
+ final bar = t.widget(find.byType(RawScrollbar));
+ expect(bar.trackColor, isNull);
+ expect(bar.trackVisibility, isNull);
+ });
+
testWidgets('snapExtent snaps the scroll offset to item boundaries (start align)', (t) async {
await t.pumpWidget(
Directionality(
diff --git a/packages/flutterwindcss/test/style/fw_text_ops_test.dart b/packages/flutterwindcss/test/style/fw_text_ops_test.dart
index be58d55..24cbfeb 100644
--- a/packages/flutterwindcss/test/style/fw_text_ops_test.dart
+++ b/packages/flutterwindcss/test/style/fw_text_ops_test.dart
@@ -61,9 +61,16 @@ void main() {
});
group('decoration', () {
- test('underline / lineThrough set their decoration', () {
+ test('underline / lineThrough / overline set their decoration', () {
expect(const FwStyle().underline.textDecoration, TextDecoration.underline);
expect(const FwStyle().lineThrough.textDecoration, TextDecoration.lineThrough);
+ expect(const FwStyle().overline.textDecoration, TextDecoration.overline);
+ });
+
+ test('overline combines with the other decoration lines', () {
+ final d = const FwStyle().underline.overline.textDecoration!;
+ expect(d.contains(TextDecoration.underline), isTrue);
+ expect(d.contains(TextDecoration.overline), isTrue);
});
test('underline + lineThrough combine (both present, order-independent)', () {
@@ -81,13 +88,18 @@ void main() {
});
group('text completeness (module 11)', () {
- test('font writes fontFamily; named helpers map to FwFontFamily', () {
+ test('font(String) writes a literal family (last-wins); roles store a step', () {
+ // A literal family is stored directly and last-wins.
expect(const FwStyle().font('Inter').fontFamily, 'Inter');
- expect(const FwStyle().fontSans.fontFamily, FwFontFamily.sans);
- expect(const FwStyle().fontSerif.fontFamily, FwFontFamily.serif);
- expect(const FwStyle().fontMono.fontFamily, FwFontFamily.mono);
- // last-wins
expect(const FwStyle().font('A').font('B').fontFamily, 'B');
+
+ // fontSans/Serif/Mono store a theme role (resolved at build), NOT a literal
+ // family — so the active theme's families apply. fontFamily stays null until
+ // resolveTokenSteps runs (see fw_token_sugar_test for the role + resolution).
+ expect(const FwStyle().fontSans.fontFamily, isNull);
+ expect(const FwStyle().fontSans.fontFamilyStep, isNotNull);
+ expect(const FwStyle().fontSerif.fontFamily, isNull);
+ expect(const FwStyle().fontMono.fontFamily, isNull);
});
test('maxLines writes the cap (last-wins) and asserts > 0', () {
diff --git a/packages/flutterwindcss/test/style/fw_token_sugar_test.dart b/packages/flutterwindcss/test/style/fw_token_sugar_test.dart
index 1bc8b60..55430c4 100644
--- a/packages/flutterwindcss/test/style/fw_token_sugar_test.dart
+++ b/packages/flutterwindcss/test/style/fw_token_sugar_test.dart
@@ -103,4 +103,63 @@ void main() {
);
expect(t.takeException(), isAssertionError);
});
+
+ // ---- Font roles (theme-resolved fontSans/Serif/Mono) ----
+
+ // A theme with distinctive families so a resolved role is unambiguous.
+ const customType = FwTypographyTheme(sans: 'Outfit', serif: 'Lora', mono: 'JetBrains Mono');
+ final customTheme = FwTokens(
+ radiusBase: FwTokens.light.radiusBase,
+ radii: FwTokens.light.radii,
+ shadows: FwTokens.light.shadows,
+ typography: customType,
+ colors: FwTokens.light.colors,
+ );
+
+ // The effective default text family at a probe; [style] (if any) is applied via
+ // a styled box wrapping the probe. Captures inside the builder (no `find`, since
+ // DefaultTextStyle.merge also uses a Builder).
+ Future familyUnder(WidgetTester t, {FwStyle? style}) async {
+ String? captured;
+ Widget probe = Builder(
+ builder: (ctx) {
+ captured = DefaultTextStyle.of(ctx).style.fontFamily;
+ return const SizedBox();
+ },
+ );
+ if (style != null) probe = FwStyled(style: style, child: probe);
+ await t.pumpWidget(
+ FwTheme(
+ tokens: customTheme,
+ child: Directionality(textDirection: TextDirection.ltr, child: probe),
+ ),
+ );
+ return captured;
+ }
+
+ test('fontSans/Serif/Mono store a font role (not a literal family)', () {
+ expect(const FwStyle().fontSans.fontFamilyStep, FwFontStep.sans);
+ expect(const FwStyle().fontSans.fontFamily, isNull);
+ expect(const FwStyle().fontSerif.fontFamilyStep, FwFontStep.serif);
+ expect(const FwStyle().fontMono.fontFamilyStep, FwFontStep.mono);
+ });
+
+ testWidgets('FwTheme applies the theme sans as the default text family', (t) async {
+ // A plain box (no font setter) still inherits the theme's sans family.
+ expect(await familyUnder(t), 'Outfit');
+ });
+
+ testWidgets('fontSerif / fontMono resolve to the theme families at build', (t) async {
+ expect(await familyUnder(t, style: const FwStyle().fontSerif), 'Lora');
+ expect(await familyUnder(t, style: const FwStyle().fontMono), 'JetBrains Mono');
+ });
+
+ testWidgets('fontSans resolves to the theme sans at build', (t) async {
+ expect(await familyUnder(t, style: const FwStyle().fontSans), 'Outfit');
+ });
+
+ testWidgets('mixing a font role with a literal font() in one chain asserts', (t) async {
+ await t.pumpWidget(_themed(const SizedBox(width: 40, height: 40).tw.font('Inter').fontSans));
+ expect(t.takeException(), isAssertionError);
+ });
}
diff --git a/packages/flutterwindcss/test/style/render_chain_test.dart b/packages/flutterwindcss/test/style/render_chain_test.dart
index e2344eb..287de1b 100644
--- a/packages/flutterwindcss/test/style/render_chain_test.dart
+++ b/packages/flutterwindcss/test/style/render_chain_test.dart
@@ -54,6 +54,16 @@ void main() {
await _pump(t, const ResolvedStyle(fit: BoxFit.cover));
final box = t.widget(find.byType(FittedBox));
expect(box.fit, BoxFit.cover);
+ // Default alignment is center when fitAlignment is unset.
+ expect(box.alignment, Alignment.center);
+ });
+
+ testWidgets('fit alignment (object-position) flows to FittedBox.alignment', (t) async {
+ await _pump(
+ t,
+ const ResolvedStyle(fit: BoxFit.cover, fitAlignment: AlignmentDirectional.topEnd),
+ );
+ expect(t.widget(find.byType(FittedBox)).alignment, AlignmentDirectional.topEnd);
});
testWidgets('module 13 wrappers emit only when set', (t) async {