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
15 changes: 9 additions & 6 deletions apps/docs/content/docs/flutterwindcss/coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/docs/flutterwindcss/effects-filters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
105 changes: 105 additions & 0 deletions apps/docs/content/docs/flutterwindcss/fonts.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout>
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*.
</Callout>

## 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.

<Callout title="Why not just hardcode the names?">
`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.
</Callout>

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

<Cards>
<Card title="Typography" href="/docs/flutterwindcss/typography" />
<Card title="Semantic tokens & theming" href="/docs/flutterwindcss/theming" />
<Card title="Theme generator" href="/theme-generator" />
</Cards>
2 changes: 2 additions & 0 deletions apps/docs/content/docs/flutterwindcss/layout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/flutterwindcss/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"styling",
"colors",
"theming",
"fonts",
"states",
"breakpoints",
"layout",
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/content/docs/flutterwindcss/typography.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions apps/docs/src/lib/generator/emit/dart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:');
Expand Down
9 changes: 5 additions & 4 deletions apps/docs/src/lib/generator/emit/dart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(', ')}.`);
Expand Down
3 changes: 2 additions & 1 deletion apps/example/lib/showcase/sections/typography.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ class TypographySection extends StatelessWidget {
children: <Widget>[
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),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down
10 changes: 10 additions & 0 deletions packages/flutterwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`)
Expand Down
13 changes: 12 additions & 1 deletion packages/flutterwindcss/lib/src/layout/fw_scroll.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -128,8 +134,13 @@ class _FwScrollState extends State<FwScroll> {
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,
);
}
Expand Down
Loading
Loading