diff --git a/components/landing/mix-winds/MixWindsLanding.tsx b/components/landing/mix-winds/MixWindsLanding.tsx index 550b0f3e..f96ffaba 100644 --- a/components/landing/mix-winds/MixWindsLanding.tsx +++ b/components/landing/mix-winds/MixWindsLanding.tsx @@ -111,11 +111,11 @@ export function MixWindsLanding() { - + Get started - - View on GitHub + + View on pub.dev @@ -245,11 +245,11 @@ export function MixWindsLanding() { think about layout.

- - Install from pub.dev + + Install from GitHub - - Read the source + + View on pub.dev
diff --git a/components/landing/mix-winds/content.ts b/components/landing/mix-winds/content.ts index faee706d..84f28f7c 100644 --- a/components/landing/mix-winds/content.ts +++ b/components/landing/mix-winds/content.ts @@ -82,7 +82,7 @@ export const DART_SNIPPET = `div('flex flex-col gap-4 rounded-3xl bg-slate-950 p ), ])`; -export const INSTALL_SNIPPET = `flutter pub add mix_winds`; +export const INSTALL_SNIPPET = `flutter pub add "mix_winds:{git:{url: https://github.com/btwld/mix.git, path: packages/mix_winds}}"`; export const FEATURES = [ { diff --git a/packages/mix_docs_preview/lib/ecosystem/mix_winds.dart b/packages/mix_docs_preview/lib/ecosystem/mix_winds.dart index cd67e8a4..e1476bad 100644 --- a/packages/mix_docs_preview/lib/ecosystem/mix_winds.dart +++ b/packages/mix_docs_preview/lib/ecosystem/mix_winds.dart @@ -8,78 +8,50 @@ class Example extends StatelessWidget { @override Widget build(BuildContext context) { return TwScope( - child: Div( - classNames: - 'bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6', - child: Div( - classNames: - 'bg-white/10 border border-white/20 rounded-3xl p-6 shadow-2xl', - child: Div( - classNames: 'flex items-start gap-4', - children: [ - Div( - classNames: - 'w-14 h-14 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center border-2 border-purple-400', - child: const Span( - text: 'SM', - classNames: 'text-white font-semibold text-lg', + child: div('bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6', [ + div('bg-white/10 border border-white/20 rounded-3xl p-6 shadow-2xl', [ + div('flex items-start gap-4', [ + div( + 'w-14 h-14 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center border-2 border-purple-400', + [span('text-white font-semibold text-lg', 'SM')], + ), + div('flex-1 min-w-0', [ + div('flex items-center gap-2 mb-1', [ + h3( + 'text-white font-semibold text-lg truncate', + 'Sarah Mitchell', ), + span( + 'px-2 py-0.5 bg-purple-500/30 text-purple-200 text-xs rounded-full font-medium', + 'Admin', + ), + ]), + p( + 'text-slate-300 text-sm mb-4', + 'Your profile changes are ready to publish. Review and confirm to update your public information.', ), - Div( - classNames: 'flex-1 min-w-0', - children: [ - Div( - classNames: 'flex items-center gap-2 mb-1', - children: const [ - H3( - text: 'Sarah Mitchell', - classNames: 'text-white font-semibold text-lg truncate', - ), - Span( - text: 'Admin', - classNames: - 'px-2 py-0.5 bg-purple-500/30 text-purple-200 text-xs rounded-full font-medium', - ), - ], - ), - const P( - text: - 'Your profile changes are ready to publish. Review and confirm to update your public information.', - classNames: 'text-slate-300 text-sm mb-4', - ), - Div( - classNames: - 'bg-white/5 rounded-xl p-3 mb-4 border border-white/10', - child: Div( - classNames: - 'flex items-center gap-2 text-amber-300 text-sm', - children: const [ - Span(text: '\u26A0'), - Span(text: 'This action cannot be undone'), - ], - ), - ), - Div( - classNames: 'flex gap-3', - children: [ - Div( - classNames: - 'flex-1 px-4 py-2.5 bg-white/10 hover:bg-white/20 text-white rounded-xl font-medium border border-white/10 hover:border-white/20 flex items-center justify-center', - child: const Span(text: 'Cancel'), - ), - Div( - classNames: - 'flex-1 px-4 py-2.5 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-400 hover:to-pink-400 text-white rounded-xl font-medium shadow-lg flex items-center justify-center', - child: const Span(text: 'Save Changes'), - ), - ], - ), - ], - ), - ], - ), - ), - ), + div('bg-white/5 rounded-xl p-3 mb-4 border border-white/10', [ + div('flex items-center gap-2 text-amber-300 text-sm', [ + span('', '\u26A0'), + span('', 'This action cannot be undone'), + ]), + ]), + div('flex gap-3', [ + button( + 'flex flex-1 items-center justify-center rounded-xl border border-white/10 bg-white/10 px-4 py-2.5 font-medium text-white hover:border-white/20 hover:bg-white/20', + [span('', 'Cancel')], + onPressed: () {}, + ), + button( + 'flex flex-1 items-center justify-center rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 px-4 py-2.5 font-medium text-white shadow-lg hover:from-purple-400 hover:to-pink-400', + [span('', 'Save Changes')], + onPressed: () {}, + ), + ]), + ]), + ]), + ]), + ]), ); } } diff --git a/packages/mix_docs_preview/pubspec.yaml b/packages/mix_docs_preview/pubspec.yaml index 00dc1837..cd3df368 100644 --- a/packages/mix_docs_preview/pubspec.yaml +++ b/packages/mix_docs_preview/pubspec.yaml @@ -12,10 +12,10 @@ dependencies: flutter: sdk: flutter mix: ^2.1.0 - # mix_winds was renamed from mix_tailwinds and is not on pub.dev under the new - # name yet, so it is pinned to the rename commit on btwld/mix main. Revert to - # a hosted `mix_winds: ^0.1.0-alpha.0` once it is published. This package is - # publish_to: none, so the git pin only affects the docs preview build. + # The mix_winds name on pub.dev still resolves to a placeholder package, so + # the preview is pinned to the rename commit. Switch to a compatible hosted + # release once one is published. This package is publish_to: none, so the git + # pin only affects the docs preview build. mix_winds: git: url: https://github.com/btwld/mix.git diff --git a/site-tests/mix-winds-integration.test.mjs b/site-tests/mix-winds-integration.test.mjs index 350531bd..4575d3f4 100644 --- a/site-tests/mix-winds-integration.test.mjs +++ b/site-tests/mix-winds-integration.test.mjs @@ -34,8 +34,12 @@ test('sells the package instead of reporting a pixel diff', () => { const seam = read('components/landing/mix-winds/ComparisonSeam.tsx') assert.match(landing, /Get started/) + assert.match(landing, /Install from GitHub/) + assert.doesNotMatch(landing, /Install from pub\.dev/) assert.match(landing, /flutter pub add|INSTALL_SNIPPET/) - assert.match(content, /flutter pub add mix_winds/) + assert.match(content, /flutter pub add .*mix_winds:\{git:/) + assert.doesNotMatch(content, /mix_winds@\{git:/) + assert.match(content, /path: packages\/mix_winds/) // No parity percentages, budgets, or pass/fail language on the page. const pageSource = [landing, content, seam].join('\n') @@ -101,6 +105,38 @@ test('shows the functional API with real HTML and Dart highlighting', () => { assert.match(highlighter, /"dart"/) }) +test('uses the functional API in the ecosystem guide and live preview', () => { + const guide = read('src/content/documentation/mix/ecosystem/mix-winds.mdx') + const preview = read('packages/mix_docs_preview/lib/ecosystem/mix_winds.dart') + + for (const source of [guide, preview]) { + assert.match(source, /child: div\(/) + assert.match(source, /\bh3\(/) + assert.match(source, /\bp\(/) + assert.match(source, /\bbutton\(/) + assert.match(source, /onPressed: \(\) \{\}/) + assert.doesNotMatch(source, /child: Div\(/) + assert.doesNotMatch(source, /child: const Span\(/) + } + + assert.match(guide, /## Functional API/) + assert.match(guide, /Use the uppercase constructors when you need/) + assert.match(guide, /onDiagnostic:/) + assert.doesNotMatch(guide, /onUnsupported:/) + + // Basic usage needs no provider; the parity preview keeps TwScope for + // explicit Tailwind preflight typography. + assert.match(guide, /`TwScope` is optional/) + assert.match(guide, /otherwise uses `TwConfig\.standard\(\)`/) + assert.match(guide, /void main\(\) \{\s+runApp\(\s+MaterialApp\(/s) + assert.match(preview, /return TwScope\(/) + + assert.match(guide, /mix_winds:\{git:/) + assert.doesNotMatch(guide, /mix_winds@\{git:/) + assert.match(guide, /placeholder package/) + assert.doesNotMatch(guide, /mix_winds:\s+\^?\d/) +}) + test('serves a legible capture on narrow screens', () => { const content = read('components/landing/mix-winds/content.ts') const seam = read('components/landing/mix-winds/ComparisonSeam.tsx') diff --git a/src/content/documentation/mix/ecosystem/mix-winds.mdx b/src/content/documentation/mix/ecosystem/mix-winds.mdx index a85c87ac..62354528 100644 --- a/src/content/documentation/mix/ecosystem/mix-winds.mdx +++ b/src/content/documentation/mix/ecosystem/mix-winds.mdx @@ -23,7 +23,13 @@ import { Callout } from "nextra/components"; -The card alert above demonstrates gradients, glassmorphism, badges, hover states, and nested flex layouts — all expressed through class name strings: + + `TwScope` is optional. The preview uses it to match Tailwind Preflight's base typography across + the whole card. The utility helpers already fall back to `TwConfig.standard()` when no scope is + present, so ordinary `div`, `p`, and `button` calls do not require a wrapper. + + +The card alert above demonstrates gradients, glassmorphism, badges, hover states, and nested flex layouts. The lowercase helpers keep each class string beside its content, while `button` also provides native interaction and semantics: ```dart import 'package:flutter/material.dart'; @@ -35,77 +41,55 @@ class CardAlert extends StatelessWidget { @override Widget build(BuildContext context) { return TwScope( - child: Div( - classNames: - 'bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6', - child: Div( - classNames: - 'bg-white/10 border border-white/20 rounded-3xl p-6 shadow-2xl', - child: Div( - classNames: 'flex items-start gap-4', - children: [ - Div( - classNames: - 'w-14 h-14 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center border-2 border-purple-400', - child: const Span( - text: 'SM', - classNames: 'text-white font-semibold text-lg', + child: div( + 'bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6', + [ + div( + 'bg-white/10 border border-white/20 rounded-3xl p-6 shadow-2xl', + [ + div('flex items-start gap-4', [ + div( + 'w-14 h-14 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center border-2 border-purple-400', + [span('text-white font-semibold text-lg', 'SM')], ), - ), - Div( - classNames: 'flex-1 min-w-0', - children: [ - Div( - classNames: 'flex items-center gap-2 mb-1', - children: const [ - H3( - text: 'Sarah Mitchell', - classNames: 'text-white font-semibold text-lg truncate', - ), - Span( - text: 'Admin', - classNames: - 'px-2 py-0.5 bg-purple-500/30 text-purple-200 text-xs rounded-full font-medium', - ), - ], - ), - const P( - text: - 'Your profile changes are ready to publish. Review and confirm to update your public information.', - classNames: 'text-slate-300 text-sm mb-4', - ), - Div( - classNames: - 'bg-white/5 rounded-xl p-3 mb-4 border border-white/10', - child: Div( - classNames: - 'flex items-center gap-2 text-amber-300 text-sm', - children: const [ - Span(text: '\u26A0'), - Span(text: 'This action cannot be undone'), - ], + div('flex-1 min-w-0', [ + div('flex items-center gap-2 mb-1', [ + h3( + 'text-white font-semibold text-lg truncate', + 'Sarah Mitchell', ), + span( + 'px-2 py-0.5 bg-purple-500/30 text-purple-200 text-xs rounded-full font-medium', + 'Admin', + ), + ]), + p( + 'text-slate-300 text-sm mb-4', + 'Your profile changes are ready to publish. Review and confirm to update your public information.', ), - Div( - classNames: 'flex gap-3', - children: [ - Div( - classNames: - 'flex-1 px-4 py-2.5 bg-white/10 hover:bg-white/20 text-white rounded-xl font-medium border border-white/10 hover:border-white/20 flex items-center justify-center', - child: const Span(text: 'Cancel'), - ), - Div( - classNames: - 'flex-1 px-4 py-2.5 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-400 hover:to-pink-400 text-white rounded-xl font-medium shadow-lg flex items-center justify-center', - child: const Span(text: 'Save Changes'), - ), - ], - ), - ], - ), + div('bg-white/5 rounded-xl p-3 mb-4 border border-white/10', [ + div('flex items-center gap-2 text-amber-300 text-sm', [ + span('', '\u26A0'), + span('', 'This action cannot be undone'), + ]), + ]), + div('flex gap-3', [ + button( + 'flex flex-1 items-center justify-center rounded-xl border border-white/10 bg-white/10 px-4 py-2.5 font-medium text-white hover:border-white/20 hover:bg-white/20', + [span('', 'Cancel')], + onPressed: () {}, + ), + button( + 'flex flex-1 items-center justify-center rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 px-4 py-2.5 font-medium text-white shadow-lg hover:from-purple-400 hover:to-pink-400', + [span('', 'Save Changes')], + onPressed: () {}, + ), + ]), + ]), + ]), ], ), - ), + ], ), ); } @@ -121,14 +105,18 @@ The following resources point to the package registry and source code: ## Install -From pub.dev: +Install the current development version directly from the Mix repository. This follows the repository's default branch without hardcoding a release number: -```yaml -dependencies: - mix_winds: 0.1.0-alpha.0 +```bash +flutter pub add "mix_winds:{git:{url: https://github.com/btwld/mix.git, path: packages/mix_winds}}" ``` -From the Git repository (latest unreleased changes): + + The `mix_winds` name on pub.dev currently resolves to a placeholder package. Until a functional + release is published there, do not use the shorter `flutter pub add mix_winds` command. + + +The equivalent `pubspec.yaml` entry is: ```yaml dependencies: @@ -138,9 +126,11 @@ dependencies: path: packages/mix_winds ``` +For a reproducible application build, add a Git `ref` for a release tag or commit. Keep the unpinned form when the goal is always to follow the latest repository version. + ## Quick start -Wrap your app with `TwScope`, then use `Div` and `Span` to build layouts with Tailwind-like class names: +Import the package and use the lowercase helpers directly. The standard utility configuration works without any root wrapper: ```dart import 'package:flutter/material.dart'; @@ -148,24 +138,18 @@ import 'package:mix_winds/mix_winds.dart'; void main() { runApp( - TwScope( - child: MaterialApp( - home: Scaffold( - body: Center( - child: Div( - classNames: - 'flex flex-col gap-4 p-6 bg-white rounded-xl shadow-md', - children: [ - Span( - text: 'Hello mix_winds', - classNames: 'text-2xl font-bold text-gray-900', - ), - Span( - text: 'Utility-first styling powered by Mix.', - classNames: 'text-sm text-gray-600', - ), - ], - ), + MaterialApp( + home: Scaffold( + body: Center( + child: div( + 'flex flex-col gap-4 rounded-xl bg-white p-6 shadow-md', + [ + h2('text-2xl font-bold text-gray-900', 'Hello mix_winds'), + p( + 'text-sm text-gray-600', + 'Utility-first styling powered by Mix.', + ), + ], ), ), ), @@ -176,25 +160,30 @@ void main() { This renders a white rounded card with a title and subtitle, using only class name strings. -## Widget API +## Functional API + +The class-first helpers are the cleanest API for most layouts. Every helper requires a class string; pass `''` when an element has no utilities. -The following table lists all widgets provided by `mix_winds`: +Each helper resolves the nearest `TwConfig` and otherwise uses `TwConfig.standard()`. Add `TwScope` only when the subtree needs Tailwind-compatible base typography, a custom configuration, extra Mix tokens, or custom modifier ordering. -| Widget | Purpose | +| Helper | Purpose | |--------|---------| -| `TwScope` | Root wrapper. Provides `TwConfig` (scales, colors, breakpoints) and default typography to the widget tree. | -| `Div` | Container and layout element. Auto-detects whether to render as a flex container or a box based on class names. | -| `Span` | Inline text element. | -| `P` | Block-level paragraph element. | -| `H1` – `H6` | Heading elements. No default styles applied (matches Tailwind Preflight behavior). | -| `TruncatedP` | Convenience wrapper for text truncation in flex containers. Auto-applies `flex-1 min-w-0`. | +| `div(classes, [children])` | Container and layout element. Returns a `Div`. | +| `p(classes, text)` | Block-level paragraph. Returns a `P`. | +| `span(classes, text)` | Inline text. Returns a `Span`. | +| `h1` – `h6(classes, text)` | Semantic headings with no default visual styles, matching Tailwind Preflight. | +| `button(classes, children, onPressed: ...)` | Native button interaction, states, and semantics. Returns a `Button`. | +| `twIcon(classes, icon)` | Icon styled with utility classes. Returns a `TwIcon`. | +| `truncatedP(classes, text)` | Truncated paragraph that applies `flex-1 min-w-0`. Returns a `TruncatedP`. | -All layout widgets accept a `classNames` string and an optional `onUnsupported` callback for unrecognized tokens: +Use the uppercase constructors when you need a const widget, key, custom configuration, diagnostics, a single `child`, or advanced button and focus options: ```dart Div( classNames: 'flex gap-4 unknown-class', - onUnsupported: (token) => debugPrint('Unsupported: $token'), + onDiagnostic: (diagnostic) { + debugPrint('${diagnostic.code}: ${diagnostic.token}'); + }, children: [...], ) ``` @@ -300,7 +289,7 @@ Prefix any utility with a variant to apply it conditionally. | `2xl:` | 1536px | ```dart -Div(classNames: 'flex flex-col md:flex-row gap-4', children: [...]) +div('flex flex-col gap-4 md:flex-row', [...]) ``` ### Interaction states @@ -315,10 +304,10 @@ Div(classNames: 'flex flex-col md:flex-row gap-4', children: [...]) | `group-hover:` | Parent group is hovered | ```dart -Div( - classNames: - 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white px-4 py-2 rounded-lg', - child: Span(text: 'Click me'), +button( + 'rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 active:bg-blue-800', + [span('', 'Click me')], + onPressed: () {}, ) ``` @@ -334,7 +323,7 @@ Div( Combine multiple variant prefixes on a single token: ```dart -Div(classNames: 'bg-white md:hover:bg-blue-500 dark:bg-gray-900') +div('bg-white md:hover:bg-blue-500 dark:bg-gray-900') ``` ## Special syntax @@ -344,7 +333,7 @@ Div(classNames: 'bg-white md:hover:bg-blue-500 dark:bg-gray-900') Use square brackets for values outside the default scale: ```dart -Div(classNames: 'bg-[#FF0000] p-[24px] w-[100px]') +div('bg-[#FF0000] p-[24px] w-[100px]') ``` ### Opacity modifiers @@ -352,7 +341,7 @@ Div(classNames: 'bg-[#FF0000] p-[24px] w-[100px]') Append `/` (0–100) to any color utility: ```dart -Div(classNames: 'bg-white/50 text-black/75') +div('bg-white/50 text-black/75') ``` ### Important flag @@ -360,7 +349,7 @@ Div(classNames: 'bg-white/50 text-black/75') Prefix with `!` to mark a utility as important (overrides other values during merge): ```dart -Div(classNames: '!font-bold') +div('!font-bold') ``` ### Negative values @@ -368,17 +357,26 @@ Div(classNames: '!font-bold') Prefix with `-` for margin and transform utilities: ```dart -Div(classNames: '-mt-4 -translate-x-8') +div('-mt-4 -translate-x-8') ``` ## Defaults and typography configuration - `mix_winds` applies default typography through `TwScope` using Mix's token and text scope system. - Prefer this over overriding `ThemeData.textTheme` when configuring default text styles. + `TwScope` is configuration, not a prerequisite for utility parsing. Use it when you want + `mix_winds` to own the subtree's base typography instead of inheriting Flutter's surrounding + `DefaultTextStyle`. -`TwScope` injects a base text style into the widget tree so all text inherits consistent defaults. Configure typography through `TwTextDefaults`: +`TwScope` performs three jobs: + +1. It provides one `TwConfig` to every descendant. Without it, each helper uses `TwConfig.standard()`. +2. It installs a `MixScope` for the preflight text token, optional Mix tokens, and modifier ordering. +3. It applies `TwTextDefaults` through `TextScope`, giving unqualified text a consistent font family, base size, weight, tracking, and line height. + +In the card preview, the third job matters: labels such as `Cancel` intentionally omit a `text-*` size and inherit Tailwind's 16px base typography. Without `TwScope`, the utility classes still work, but those unspecified text properties come from the surrounding Flutter app and the visual result can differ. + +Configure typography through `TwTextDefaults`: ```dart TwScope(