diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..586c4c7 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,60 @@ +name: "Changelog + Version" + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +# On every push to main: bump cli/package.json (create-page badge) and +# prepend an OpenAI-written Keep-a-Changelog section. Commits directly +# with [changelog][skip ci] so tests and this workflow do not loop. + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: changelog-main + cancel-in-progress: false + +jobs: + changelog: + name: Update changelog & version + runs-on: ubuntu-latest + timeout-minutes: 10 + if: ${{ github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message, '[changelog]') }} + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Needed so the bot commit triggers deploy hooks that listen to main. + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Generate changelog + bump version + id: release + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_MODEL: gpt-4o-mini + BEFORE_SHA: ${{ github.event.before }} + AFTER_SHA: ${{ github.sha }} + run: node scripts/update-changelog.mjs + + - name: Commit CHANGELOG.md and cli/package.json + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md cli/package.json + if git diff --staged --quiet; then + echo "No changelog/version changes to commit." + exit 0 + fi + git commit -m "docs: release v${{ steps.release.outputs.version }} [changelog][skip ci]" + git push diff --git a/.github/workflows/test-tier3.yml b/.github/workflows/test-tier3.yml index 8068264..267f98c 100644 --- a/.github/workflows/test-tier3.yml +++ b/.github/workflows/test-tier3.yml @@ -22,6 +22,7 @@ jobs: layer1: name: Layer 1 — Template Unit Tests + if: ${{ github.event_name == 'workflow_dispatch' || (!contains(github.event.head_commit.message, '[changelog]') && !contains(github.event.head_commit.message, '[skip ci]')) }} runs-on: ubuntu-latest timeout-minutes: 15 @@ -141,6 +142,11 @@ jobs: steps: - name: Check all jobs passed run: | + # Bot changelog commits skip the matrix; treat that as a no-op pass. + if [ "${{ needs.layer1.result }}" = "skipped" ]; then + echo "⏭️ Tier 3 skipped (changelog / skip-ci commit)." + exit 0 + fi if [ "${{ needs.layer1.result }}" != "success" ] || [ "${{ needs.layer2.result }}" != "success" ]; then echo "❌ Release gate FAILED. Not all jobs passed." exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3ac8dc5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +User-facing changes to FlutterInit (wizard, CLI, scaffolds, and docs that affect builders). +The version on [/create](https://flutterinit.com/create) tracks `cli/package.json` and advances on every push to `main`. + +## [1.1.0] - 2026-09-21 + +Extended UI is now a first-class product story: public update + guide, clearer naming across the wizard and scaffolds, and the create-page version set to **1.1.0**. + +### Added +- Blog update and guide for Extended UI (`App*` widgets + optional shadcn_ui) +- Skills lock entry for related agent skills + +### Changed +- Product version on `/create` bumped to `1.1.0` via `cli/package.json` +- Renamed scaffold UI surface from FlutterInit-prefixed widgets to the `App*` kit naming +- Wizard copy and schema labels aligned with Extended UI / App kit wording diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f22927..5e90730 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,12 @@ Please follow these naming standards for your branches: - `docs/doc-updates` (Improvements to markdown) - `perf/optimization` (Performance enhancements) +## Changelog & versioning + +Product version lives in `cli/package.json` and is shown on `/create`. On every push to `main`, GitHub Actions bumps that version and prepends an entry to `CHANGELOG.md` (OpenAI-written, committed with `[changelog][skip ci]`). + +Do **not** hand-edit version bumps for routine merges — the workflow owns them. Large product stories still belong in `content/blog/updates/` as MDX announcements. + ## Development Workflow We provide a specialized dev loop for template creation that provides "Hot Reload" for your scaffolds. diff --git a/README.md b/README.md index 9b5bc4f..e862f7c 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,11 @@ FlutterInit ships a built-in blog at [flutterinit.com/blogs](https://flutterinit | Kind | Purpose | |------|---------| -| **Updates** | Release notes, announcements, and changelogs | +| **Updates** | Release notes, announcements, and major product stories | | **Guides** | Deep-dive technical guides for every supported stack combination | +The living product changelog lives at [flutterinit.com/changelog](https://flutterinit.com/changelog) (source: [`CHANGELOG.md`](CHANGELOG.md)). The version badge on [/create](https://flutterinit.com/create) tracks [`cli/package.json`](cli/package.json) and is bumped automatically on every push to `main`. + Every guide shows the exact `stackConfig` it covers (Architecture · State · Backend · Navigation), a "when to choose this stack" recommendation, an interactive file tree of the generated output, and a Table of Contents sidebar. Blogs content lives under `content/blog/` as `.mdx` files. The file path is the URL slug — no CMS or database required. diff --git a/app/changelog/page.tsx b/app/changelog/page.tsx new file mode 100644 index 0000000..958ade8 --- /dev/null +++ b/app/changelog/page.tsx @@ -0,0 +1,140 @@ +import type { Metadata } from "next" +import Link from "next/link" +import Image from "next/image" +import { readFile } from "node:fs/promises" +import path from "node:path" +import { MDXRemote } from "next-mdx-remote/rsc" +import remarkGfm from "remark-gfm" +import rehypeSlug from "rehype-slug" +import { mdxComponents } from "@/app/blogs/components/MDXComponents" +import cliPackage from "@/cli/package.json" +import { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" + +export const metadata: Metadata = { + title: "Changelog | FlutterInit", + description: + "Release notes for FlutterInit — wizard, CLI, and scaffold changes. The version on /create advances with every push to main.", + alternates: { + canonical: "https://flutterinit.com/changelog", + }, + openGraph: { + title: "Changelog | FlutterInit", + description: + "Release notes for FlutterInit — wizard, CLI, and scaffold changes.", + type: "website", + url: "https://flutterinit.com/changelog", + }, +} + +const mdxOptions = { + remarkPlugins: [remarkGfm], + rehypePlugins: [rehypeSlug], +} + +/** Drop the top H1 so the page owns the title. */ +function bodyWithoutTitle(markdown: string): string { + return markdown.replace(/^#\s+Changelog\s*\r?\n+/, "").trimStart() +} + +// Module-scope read keeps this route prerenderable under Cache Components. +const changelogSource = bodyWithoutTitle( + await readFile(path.join(process.cwd(), "CHANGELOG.md"), "utf8"), +) + +export default function ChangelogPage() { + const version = cliPackage.version + + return ( +
+
+
+ + FlutterInit + + FlutterInit + + + +
+
+ +
+
+ + + + + Home + + + + + + Changelog + + + + +
+ +
+
+

+ Changelog +

+ + v{version} + +
+

+ What changed in FlutterInit for builders. The same version appears on{" "} + + /create + + {" "}and advances automatically on every push to{" "} + + main + + . +

+
+ +
+ +
+ +
+
+
+ ) +} diff --git a/app/components/landing/Footer.tsx b/app/components/landing/Footer.tsx index ad449cf..095f943 100644 --- a/app/components/landing/Footer.tsx +++ b/app/components/landing/Footer.tsx @@ -120,6 +120,12 @@ export function Footer() {
+ + Changelog + = 3.41.", + description: "Adds the shadcn_ui package plus ShadApp wrappers and a ShadThemeData bridge built from your design tokens. Requires Flutter SDK >= 3.41.", url: "https://mariuti.com/flutter-shadcn-ui/", }, ui_kit_app: { title: "App kit (native)", - description: "Generated feature screens (login, signup, home, onboarding) use the App* adaptive widget kit.", + description: "Generated feature screens (login, signup, home, onboarding) use the App adaptive widget kit.", }, ui_kit_shadcn: { title: "shadcn/ui kit", - description: "Generated feature screens use ShadApp* wrappers that mirror the App* API so you can swap kits without rewriting screens.", + description: "Generated feature screens use ShadApp wrappers that mirror the App API so you can swap kits without rewriting screens.", }, localization_en: { title: "English Localization", diff --git a/app/components/wizard/steps/ThemeStep.tsx b/app/components/wizard/steps/ThemeStep.tsx index 21e8d6d..06ea1c2 100644 --- a/app/components/wizard/steps/ThemeStep.tsx +++ b/app/components/wizard/steps/ThemeStep.tsx @@ -81,7 +81,7 @@ const themeChoiceOptions: Array<{ { value: "shadcn", label: "shadcn_ui", - description: "shadcn/ui package plus ShadApp* wrappers.", + description: "shadcn/ui package plus ShadApp wrappers.", }, ] @@ -96,19 +96,19 @@ const defaultAppOptions: Array<{ value: "material", radioValue: "default_app_material", label: "MaterialApp", - description: "Root app is MaterialApp; screens use App* widgets.", + description: "Root app is MaterialApp; screens use App widgets.", }, { value: "cupertino", radioValue: "default_app_cupertino", label: "CupertinoApp", - description: "Root app is CupertinoApp; screens use App* widgets.", + description: "Root app is CupertinoApp; screens use App widgets.", }, { value: "shad", radioValue: "default_app_shad", label: "ShadApp", - description: "Root wraps with ShadApp; screens use ShadApp* widgets.", + description: "Root wraps with ShadApp; screens use ShadApp widgets.", }, ] diff --git a/app/lib/config/schema.ts b/app/lib/config/schema.ts index 07cfe0c..e4c0796 100644 --- a/app/lib/config/schema.ts +++ b/app/lib/config/schema.ts @@ -166,8 +166,8 @@ export const platformStyleOptions = [ ] as const satisfies Array<{ value: PlatformStyle; label: string; description: string }> export const uiKitOptions = [ - { value: "app", label: "App kit (native)", description: "Generated screens use App* adaptive widgets." }, - { value: "shadcn", label: "shadcn/ui", description: "Generated screens use ShadApp* wrappers." }, + { value: "app", label: "App kit (native)", description: "Generated screens use App adaptive widgets." }, + { value: "shadcn", label: "shadcn/ui", description: "Generated screens use ShadApp wrappers." }, ] as const satisfies Array<{ value: UiKit; label: string; description: string }> const firebaseSchema = z.object({ diff --git a/app/lib/generator/index.ts b/app/lib/generator/index.ts index 8736cd9..ca47da6 100644 --- a/app/lib/generator/index.ts +++ b/app/lib/generator/index.ts @@ -95,9 +95,9 @@ type TemplateContext = ScaffoldConfig & { }> /** Opt-in shadcn_ui component layer */ usesShadcn: boolean - /** Feature screens use ShadApp* when true */ + /** Feature screens use ShadApp when true */ shadcnDefault: boolean - /** Adaptive / material / cupertino platform style for App* widgets */ + /** Adaptive / material / cupertino platform style for App widgets */ platformStyle: "adaptive" | "material" | "cupertino" } } diff --git a/cli/src/prompts.ts b/cli/src/prompts.ts index e662fdc..18a1e68 100644 --- a/cli/src/prompts.ts +++ b/cli/src/prompts.ts @@ -333,7 +333,7 @@ export async function runPrompts(): Promise { checkCancel(primaryColor) const platformStyle = await select<'adaptive' | 'material' | 'cupertino'>({ - message: 'Platform style for App* components', + message: 'Platform style for App components', options: [ { value: 'adaptive', @@ -365,8 +365,8 @@ export async function runPrompts(): Promise { const kit = await select<'app' | 'shadcn'>({ message: 'Default kit for generated screens', options: [ - { value: 'app', label: 'App kit (native)', hint: 'Screens use App* adaptive widgets.' }, - { value: 'shadcn', label: 'shadcn/ui', hint: 'Screens use ShadApp* wrappers.' }, + { value: 'app', label: 'App kit (native)', hint: 'Screens use App adaptive widgets.' }, + { value: 'shadcn', label: 'shadcn/ui', hint: 'Screens use ShadApp wrappers.' }, ], }) checkCancel(kit) diff --git a/cli/templates/base/lib/src/shared/enums/app_ui_enums.dart.hbs b/cli/templates/base/lib/src/shared/enums/app_ui_enums.dart.hbs index fa919e9..16c9759 100644 --- a/cli/templates/base/lib/src/shared/enums/app_ui_enums.dart.hbs +++ b/cli/templates/base/lib/src/shared/enums/app_ui_enums.dart.hbs @@ -1,4 +1,4 @@ -/// Shared size scale for App* components beyond buttons. +/// Shared size scale for App components beyond buttons. enum AppSize { small, medium, diff --git a/cli/templates/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs b/cli/templates/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs index 2ca4bb3..9e81168 100644 --- a/cli/templates/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs +++ b/cli/templates/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs @@ -52,7 +52,7 @@ class AppPlatformScope extends InheritedWidget { } extension AppPlatformX on BuildContext { - /// True when App* widgets should render Cupertino for this context. + /// True when App widgets should render Cupertino for this context. bool get isCupertinoUi { final style = AppPlatformScope.of(this); return switch (style) { diff --git a/cli/templates/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs b/cli/templates/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs index 4668d4c..65a940e 100644 --- a/cli/templates/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs +++ b/cli/templates/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs @@ -1,6 +1,6 @@ import '../../../imports/imports.dart'; -/// Live gallery of App* (and optional ShadApp*) components. +/// Live gallery of App (and optional ShadApp) components. /// /// Open via [AppRoutes.uiShowcase] in debug builds. class UiShowcaseScreen extends StatelessWidget { @@ -72,7 +72,7 @@ class UiShowcaseScreen extends StatelessWidget { SizedBox(height: AppSpacing.sm), AppEmptyState( title: 'Nothing here', - subtitle: 'Empty states use App* actions.', + subtitle: 'Empty states use App actions.', actionLabel: 'Retry', onAction: () {}, ), diff --git a/cli/templates/partials/llm/design-quick-ref.hbs b/cli/templates/partials/llm/design-quick-ref.hbs index c75833f..9fcd938 100644 --- a/cli/templates/partials/llm/design-quick-ref.hbs +++ b/cli/templates/partials/llm/design-quick-ref.hbs @@ -11,5 +11,5 @@ Full reference: **[DESIGN.md](DESIGN.md)** - **Tokens:** `AppSpacing`, `AppBorders`, `AppShadows`, `AppDurations`, `AppCurves` from `lib/src/theme/theme_constants.dart` - **Extensions:** `context_extension.dart` — `width`, `height`, `isDarkMode`, `showAppDialog`, `showTypedSnackBar` {{#if flags.usesScreenutil}}- **ScreenUtil:** baseline 390×844; `.w` `.h` `.r` `.sp`; wrapper already in app — no second `ScreenUtilInit`{{else}}- **Layout:** `MediaQuery` / `LayoutBuilder` / `Flexible` — no ScreenUtil{{/if}} -- **Widgets:** reusable UI in `lib/src/shared/widgets/ui/app/` (App* adaptive kit){{#if flags.usesShadcn}}; shadcn wrappers in `ui/shadcn/`{{/if}} — use tokens not magic numbers +- **Widgets:** reusable UI in `lib/src/shared/widgets/ui/app/` (App adaptive kit){{#if flags.usesShadcn}}; shadcn wrappers in `ui/shadcn/`{{/if}} — use tokens not magic numbers - **Platform style:** `{{ui.platformStyle}}` via `AppPlatformScope` / `context.isCupertinoUi` \ No newline at end of file diff --git a/cli/templates/partials/llm/packages-list.hbs b/cli/templates/partials/llm/packages-list.hbs index 2072910..88f6eb4 100644 --- a/cli/templates/partials/llm/packages-list.hbs +++ b/cli/templates/partials/llm/packages-list.hbs @@ -24,4 +24,4 @@ {{#if flags.usesFlutterHooks}}- `flutter_hooks`{{/if}} {{#if flags.usesDotenv}}- `flutter_dotenv`{{/if}} {{#if flags.usesSkeletonizer}}- `skeletonizer`{{/if}} -{{#if flags.usesShadcn}}- `shadcn_ui` — custom component kit + `ShadApp*` wrappers{{/if}} +{{#if flags.usesShadcn}}- `shadcn_ui` — custom component kit + `ShadApp` wrappers{{/if}} diff --git a/cli/templates/partials/llm/ui-components.hbs b/cli/templates/partials/llm/ui-components.hbs index abb46c5..207327f 100644 --- a/cli/templates/partials/llm/ui-components.hbs +++ b/cli/templates/partials/llm/ui-components.hbs @@ -49,5 +49,5 @@ context.designTokens.paddingMedium {{else}} ### shadcn kit -Not enabled for this project. Re-generate with shadcn opted in from the UI & Theme step to add `ShadApp*` wrappers. +Not enabled for this project. Re-generate with shadcn opted in from the UI & Theme step to add `ShadApp` wrappers. {{/if}} diff --git a/content/blog/guides/ui/extended-ui-app-widgets-shadcn.mdx b/content/blog/guides/ui/extended-ui-app-widgets-shadcn.mdx new file mode 100644 index 0000000..42c396c --- /dev/null +++ b/content/blog/guides/ui/extended-ui-app-widgets-shadcn.mdx @@ -0,0 +1,170 @@ +--- +title: "How to use Extended UI: App widgets, platform style, and shadcn_ui" +description: "A practical guide to FlutterInit Extended UI: App widgets, AppPlatformScope, optional ShadApp via shadcn_ui, and how the wizard maps to config." +kind: guide +category: ui +tags: [ui, app-widgets, shadcn, material, cupertino, design-system, flutterinit] +publishedAt: "2026-09-21" +author: arjun +whenToChoose: "Use the App kit for native Material/Cupertino UI. Turn on shadcn_ui when you want web-style controls with the same screen APIs." +--- + +FlutterInit Extended UI is the generated widget kit under `lib/src/shared/widgets/ui/`. Every project gets adaptive `App` widgets. Turn on `shadcn_ui` and you also get `ShadApp` wrappers with matching constructors, so auth and home can change kits without a rewrite. + +Short launch note: [Extended UI update](/blogs/updates/extended-ui). + +## The problem this solves + +Architecture and state get you a clean tree. UI still does not. Teams invent buttons, fields, and empty states per screen, then argue about iOS vs Android chrome later. + +Extended UI puts one kit in the repo from generate time: one import path, design tokens, a debug showcase, and notes in `DESIGN.md` so agents stop inventing `EdgeInsets.all(13)`. + +## Folder layout + +``` +lib/src/shared/widgets/ui/ + app/ # always generated + app_platform.dart # AppPlatformScope + helpers + actions/ # AppButton, AppFab, AppChip, … + inputs/ # AppTextField, AppOtpField, … + display/ # AppCard, AppAvatar, … + feedback/ # loading, dialogs, sheets, snackbars + navigation/ # AppScaffold, AppTopBar, … + layout/ # AppGap, AppSection, … + shadcn/ # only when ui.shadcn is true + shad_theme.dart + shad_app_button.dart + … + ui_showcase_screen.dart + ui.dart # barrel export +``` + +Import through your shared barrel, usually `imports.dart`. Prefer `AppButton` over raw `ElevatedButton` or `CupertinoButton`. + +## Platform style + +`AppPlatformScope` sets how `App` widgets render: + +```dart +AppPlatformScope( + style: AppPlatformStyle.adaptive, // or .material / .cupertino + child: child, +) +``` + +Resolution order, highest wins: + +1. Per-widget `platform:` +2. Nearest `AppPlatformScope` +3. If adaptive, Material on Android and Cupertino on iOS/macOS from `ThemeData.platform` + +```dart +AppButton( + label: 'Continue', + onPressed: onContinue, + platform: AppPlatformStyle.cupertino, +) + +context.isCupertinoUi; +context.resolveCupertinoUi(platform); +``` + +Colors still come from your seed `ColorScheme`. Forcing Cupertino does not invent a second palette. + +I default projects to adaptive unless the product is clearly Material-only, like many web clients, or iOS-first. + +## Building a screen + +Generated login, signup, home, and onboarding already use the kit. A form looks like this: + +```dart +AppTextField( + controller: emailController, + label: 'Email', + prefixIcon: const Icon(Icons.email_outlined), + validator: (v) { /* … */ }, +), +SizedBox(height: AppSpacing.md), +AppButton( + label: 'Sign in', + onPressed: isLoading ? null : onSubmit, + isLoading: isLoading, + isFullWidth: true, + variant: ButtonVariant.primary, + height: ButtonSize.large, +) +``` + +Keep spacing and type on tokens: `AppSpacing`, `context.textTheme`, `context.colors`. [DESIGN.md explained](/blogs/guides/ai-context/design-md-explained) covers the full contract. + +In debug builds, open `/debug/ui-showcase`, routed as `AppRoutes.uiShowcase`, to browse the kit without a scratch screen. + +## Adding shadcn_ui + +On UI & theme, select shadcn_ui. Generation adds: + +- `shadcn_ui` in `pubspec.yaml`. Needs Flutter SDK 3.41 or newer. +- `buildShadLightTheme` / `buildShadDarkTheme` from the same seed as Material +- `ShadApp` wrappers under `ui/shadcn/` with the same params as `App` + +```dart +ShadAppButton( + label: 'Save', + onPressed: _save, + variant: ButtonVariant.primary, + height: ButtonSize.large, + isLoading: state.isLoading, +) +``` + +Feature templates swap names through Handlebars partials. Same screen tree, different widget: + +| Partial | App kit | shadcn as default kit | +|---|---|---| +| `ui/button` | `AppButton` | `ShadAppButton` | +| `ui/text_field` | `AppTextField` | `ShadAppTextField` | +| `ui/card` | `AppCard` | `ShadAppCard` | + +### Default app when shadcn is on + +| Choice | Root | Screens default to | +|---|---|---| +| MaterialApp | `MaterialApp` with shadcn wrap | `App` | +| CupertinoApp | `CupertinoApp` with shadcn wrap | `App` | +| ShadApp | `ShadApp` as theme host | `ShadApp` | + +Both kits stay exported when shadcn is enabled. You can call `AppButton` from a ShadApp project if you want a native control in one place. + +## Wizard to config + +| UI & theme choice | Config | +|---|---| +| Material | `theme.preset: material3`, `ui.shadcn: false`, `ui.defaultKit: app` | +| Cupertino | `theme.preset: cupertino`, often `ui.platformStyle: cupertino` | +| shadcn_ui | `ui.shadcn: true`; default app sets `defaultKit` and root | +| Appearance | `theme.darkMode.enabled` / `.system` | +| Primary color | `theme.primaryColor` (seed for Material and Shad themes) | + +`npx create-flutterinit` asks the same UI questions. + +## What I pick when + +| Goal | Pick | +|---|---| +| Native phone/tablet | App kit, platform style adaptive | +| Material everywhere, including web demos and admin | App kit + material | +| iOS-first chrome | Cupertino theme or cupertino platform style | +| Web-style menus, tables, dense controls | shadcn_ui, often with ShadApp as default | +| Cheap kit swaps later | Stick to `App` / `ShadApp` in features | + +Mixing raw Material widgets into feature screens while the rest of the app uses the kit is how you get uneven spacing and two button styles. Don't. + +## Checklist after generate + +- Open `lib/src/shared/widgets/ui/` and skim `app/` +- Hit `/debug/ui-showcase` once +- Read the UI section in `DESIGN.md` +- Build new screens with `App` (or `ShadApp` if that is the default kit) +- Skip raw `ElevatedButton` / `TextField` in features unless you have a reason + +Generate on [/create](/create), set UI & theme, then open the tree or Preview code before you download. diff --git a/content/blog/updates/extended-ui.mdx b/content/blog/updates/extended-ui.mdx new file mode 100644 index 0000000..fb4780c --- /dev/null +++ b/content/blog/updates/extended-ui.mdx @@ -0,0 +1,75 @@ +--- +title: "Extended UI: a real widget kit in every FlutterInit scaffold" +description: "FlutterInit now generates 50+ adaptive App widgets by default, plus optional shadcn_ui ShadApp wrappers you pick on the UI & theme step." +kind: update +category: announcements +tags: [ui, design-system, shadcn, material, cupertino, flutter, scaffolding] +publishedAt: "2026-09-21" +author: arjun +featured: true +--- + +Most Flutter generators stop at folders and `pubspec.yaml`. You unzip, open login, and still face raw `ElevatedButton` and a `TextField` with magic padding. That is where the week disappears. + +I got tired of that. So FlutterInit now ships **Extended UI**: a product widget kit under `lib/src/shared/widgets/ui/`, generated with the rest of the project. + +## What you get by default + +Every scaffold includes the App kit. Fifty-plus `App` widgets that pick Material or Cupertino at runtime through `AppPlatformScope`. + +| Category | Examples | +|---|---| +| Actions | `AppButton`, `AppIconButton`, `AppFab`, `AppChip` | +| Inputs | `AppTextField`, `AppPasswordField`, `AppOtpField`, `AppDatePicker` | +| Display | `AppCard`, `AppAvatar`, `AppBadge`, `AppDataTable` | +| Feedback | Loading, empty states, dialogs, sheets, snackbars | +| Navigation | `AppScaffold`, `AppTopBar`, `AppBottomNavBar`, `AppTabBar` | +| Layout | `AppGap`, `AppSection`, `AppResponsiveBuilder` | + +Platform style is one of three modes: + +- Adaptive: Material on Android, Cupertino on iOS and macOS +- Material: Material everywhere +- Cupertino: Cupertino everywhere, still seeded from your `ColorScheme` + +Override one widget with `platform:`, or wrap a subtree when a screen needs a different look. Debug builds include `/debug/ui-showcase` so you can poke every control without writing a throwaway page. + +## Optional shadcn_ui + +On the UI & theme step, pick `shadcn_ui` and generation adds the [shadcn_ui](https://mariuti.com/flutter-shadcn-ui/) package, a `ShadThemeData` bridge from the same seed color, and `ShadApp` wrappers that mirror the `App` API. + +When shadcn is on, you also choose the root: + +- MaterialApp: Material root, screens still default to `App` +- CupertinoApp: Cupertino root, screens still default to `App` +- ShadApp: ShadApp hosts theme, feature screens default to `ShadApp` + +Login, signup, home, and onboarding keep the same structure either way. You are not rewriting screens to try a different look. + +## Tokens stay required + +The kit sits on the same tokens as the rest of the scaffold: + +```dart +AppSpacing.md +AppBorders.card +AppShadows.elevated +context.colors.primary +context.appColors.success +context.textTheme.titleMedium +``` + +`DESIGN.md` and the agent docs tell humans and coding agents to reach for `AppButton`, not a one-off Material control with hex colors. + +## Try it + +1. Open [flutterinit.com/create](https://flutterinit.com/create) +2. Reach UI & theme +3. Set appearance and primary color +4. Pick Material, Cupertino, or shadcn_ui +5. If shadcn, pick MaterialApp, CupertinoApp, or ShadApp +6. Generate, or open Preview code and inspect `lib/src/shared/widgets/ui/` + +Same options in the CLI via `npx create-flutterinit`. + +File layout, `AppPlatformScope`, screen patterns, and wizard config live in the [Extended UI guide](/blogs/guides/ui/extended-ui-app-widgets-shadcn). diff --git a/scripts/update-changelog.mjs b/scripts/update-changelog.mjs new file mode 100644 index 0000000..2f64714 --- /dev/null +++ b/scripts/update-changelog.mjs @@ -0,0 +1,264 @@ +#!/usr/bin/env node +/** + * Bumps cli/package.json version and prepends a Keep-a-Changelog section + * using OpenAI, based on commits/diffs since the previous push tip. + * + * Env: + * OPENAI_API_KEY (required) + * OPENAI_MODEL (optional, default gpt-4o-mini) + * BEFORE_SHA (optional) previous tip; empty/zeros → last commit only + * AFTER_SHA (optional) current tip; default HEAD + */ +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const CLI_PKG_PATH = path.join(ROOT, "cli", "package.json") +const CHANGELOG_PATH = path.join(ROOT, "CHANGELOG.md") +const MODEL = process.env.OPENAI_MODEL || "gpt-4o-mini" +const API_KEY = process.env.OPENAI_API_KEY + +if (!API_KEY) { + console.error("OPENAI_API_KEY is required") + process.exit(1) +} + +function git(args) { + return execFileSync("git", args, { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }).trim() +} + +function isZeroSha(sha) { + return !sha || /^0+$/.test(sha) +} + +function bumpSemver(version, bump) { + const parts = version.split(".").map((n) => Number.parseInt(n, 10)) + if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) { + throw new Error(`Invalid semver in cli/package.json: ${version}`) + } + let [major, minor, patch] = parts + if (bump === "major") { + major += 1 + minor = 0 + patch = 0 + } else if (bump === "minor") { + minor += 1 + patch = 0 + } else { + patch += 1 + } + return `${major}.${minor}.${patch}` +} + +function todayUTC() { + return new Date().toISOString().slice(0, 10) +} + +function collectRange() { + const after = process.env.AFTER_SHA || git(["rev-parse", "HEAD"]) + const before = process.env.BEFORE_SHA || "" + + if (isZeroSha(before)) { + // First push / empty before — summarize only the tip commit. + return { range: `${after}^!`, after } + } + + // Inclusive of commits reachable from after but not before. + return { range: `${before}..${after}`, after } +} + +function collectGitContext(range) { + let log = "" + let diff = "" + try { + log = git(["log", "--pretty=format:%h %s%n%b%n---", range]) + } catch { + log = git(["log", "-1", "--pretty=format:%h %s%n%b"]) + } + + try { + if (range.endsWith("^!")) { + diff = git(["show", "--stat", "-U2", "--format=", range.slice(0, -2)]) + } else { + diff = git(["diff", "--stat", "-U2", range]) + } + } catch { + try { + diff = git(["show", "--stat", "-U2", "--format=", "HEAD"]) + } catch { + diff = "(diff unavailable)" + } + } + + // Cap payload size for the API. + const MAX = 60_000 + if (diff.length > MAX) { + diff = `${diff.slice(0, MAX)}\n…(truncated)` + } + if (log.length > 20_000) { + log = `${log.slice(0, 20_000)}\n…(truncated)` + } + + return { log, diff } +} + +const SYSTEM_PROMPT = `You maintain the FlutterInit product changelog and version bumps. + +FlutterInit is a Flutter project scaffolder (web wizard at /create + CLI create-flutterinit). +The public product version lives in cli/package.json and is shown on /create. + +Return ONLY valid JSON with this shape: +{ + "bump": "patch" | "minor" | "major", + "lede": "1-2 sentence user-facing summary", + "sections": { + "Added": ["…"], + "Changed": ["…"], + "Fixed": ["…"], + "Removed": ["…"], + "Internal": ["…"] + } +} + +Rules: +- Default bump is "patch". Use "minor" for new user-facing capability. Use "major" only for clearly breaking product changes. +- Always bump on every push; if changes are internal-only, set bump "patch", put one short bullet under Internal, and leave other arrays empty. +- Write for builders using FlutterInit — outcomes, not file lists. Name wizard steps, CLI flags, or paths only when useful. +- No filler ("various improvements", "updated files", "misc changes"). No emoji. +- Bullets ≤ ~100 characters, one idea each. +- Omit empty section keys you do not need; Internal only when there is nothing user-facing. +- Never invent features that are not evidenced by the commits/diff.` + +async function askOpenAI({ version, log, diff }) { + const user = `Current version: ${version} + +Commits: +${log || "(none)"} + +Diff summary: +${diff || "(none)"}` + + const res = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: MODEL, + temperature: 0.2, + response_format: { type: "json_object" }, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: user }, + ], + }), + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`OpenAI API error ${res.status}: ${text}`) + } + + const data = await res.json() + const content = data.choices?.[0]?.message?.content + if (!content) throw new Error("OpenAI returned empty content") + + const parsed = JSON.parse(content) + const bump = ["major", "minor", "patch"].includes(parsed.bump) + ? parsed.bump + : "patch" + const lede = + typeof parsed.lede === "string" && parsed.lede.trim() + ? parsed.lede.trim() + : "Maintenance update." + const sections = parsed.sections && typeof parsed.sections === "object" + ? parsed.sections + : { Internal: ["Maintenance update."] } + + return { bump, lede, sections } +} + +function formatSection(version, date, lede, sections) { + const order = ["Added", "Changed", "Fixed", "Removed", "Internal"] + const lines = [`## [${version}] - ${date}`, "", lede, ""] + + for (const key of order) { + const items = sections[key] + if (!Array.isArray(items) || items.length === 0) continue + lines.push(`### ${key}`, "") + for (const item of items) { + const text = String(item).replace(/^\s*[-*]\s*/, "").trim() + if (text) lines.push(`- ${text}`) + } + lines.push("") + } + + return lines.join("\n").trimEnd() + "\n" +} + +function upsertChangelog(sectionMarkdown) { + const header = `# Changelog + +User-facing changes to FlutterInit (wizard, CLI, scaffolds, and docs that affect builders). +The version on [/create](https://flutterinit.com/create) tracks \`cli/package.json\` and advances on every push to \`main\`. + +` + + let existing = "" + if (fs.existsSync(CHANGELOG_PATH)) { + existing = fs.readFileSync(CHANGELOG_PATH, "utf8") + // Drop the top title/intro so we re-seed a stable header. + existing = existing.replace(/^# Changelog\r?\n[\s\S]*?(?=\n## )/, "") + } + + const next = header + sectionMarkdown + (existing.startsWith("\n") ? existing : `\n${existing}`) + fs.writeFileSync(CHANGELOG_PATH, next.replace(/\n{3,}/g, "\n\n"), "utf8") +} + +function writeVersion(pkg, nextVersion) { + pkg.version = nextVersion + fs.writeFileSync(CLI_PKG_PATH, `${JSON.stringify(pkg, null, 2)}\n`, "utf8") +} + +async function main() { + const pkg = JSON.parse(fs.readFileSync(CLI_PKG_PATH, "utf8")) + const current = pkg.version + const { range } = collectRange() + const { log, diff } = collectGitContext(range) + + console.log(`Analyzing range: ${range}`) + console.log(`Current version: ${current}`) + + const { bump, lede, sections } = await askOpenAI({ version: current, log, diff }) + const nextVersion = bumpSemver(current, bump) + const date = todayUTC() + const section = formatSection(nextVersion, date, lede, sections) + + writeVersion(pkg, nextVersion) + upsertChangelog(section) + + // Machine-readable outputs for the workflow. + const outPath = process.env.GITHUB_OUTPUT + if (outPath) { + fs.appendFileSync( + outPath, + `version=${nextVersion}\nbump=${bump}\n`, + "utf8", + ) + } + + console.log(`Bumped ${current} → ${nextVersion} (${bump})`) + console.log(section) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/skills-lock.json b/skills-lock.json index f59dfb2..84a18f2 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -236,6 +236,12 @@ "skillPath": ".claude/skills/ui-ux-pro-max/SKILL.md", "computedHash": "6cb585d36a0d0418986bc8449bc0c8ff216344d692e08245d7f9e7414fee11a6" }, + "unslop": { + "source": "cursor/plugins", + "sourceType": "github", + "skillPath": "pstack/skills/unslop/SKILL.md", + "computedHash": "8754d12e48c69eccefd341f3ddd22e64e23975ab4bfc3087dcdabf1b718c1a62" + }, "web-design-guidelines": { "source": "vercel-labs/agent-skills", "sourceType": "github", diff --git a/templates/flutter/base/lib/src/shared/enums/app_ui_enums.dart.hbs b/templates/flutter/base/lib/src/shared/enums/app_ui_enums.dart.hbs index fa919e9..16c9759 100644 --- a/templates/flutter/base/lib/src/shared/enums/app_ui_enums.dart.hbs +++ b/templates/flutter/base/lib/src/shared/enums/app_ui_enums.dart.hbs @@ -1,4 +1,4 @@ -/// Shared size scale for App* components beyond buttons. +/// Shared size scale for App components beyond buttons. enum AppSize { small, medium, diff --git a/templates/flutter/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs b/templates/flutter/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs index 2ca4bb3..9e81168 100644 --- a/templates/flutter/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs +++ b/templates/flutter/base/lib/src/shared/widgets/ui/app/app_platform.dart.hbs @@ -52,7 +52,7 @@ class AppPlatformScope extends InheritedWidget { } extension AppPlatformX on BuildContext { - /// True when App* widgets should render Cupertino for this context. + /// True when App widgets should render Cupertino for this context. bool get isCupertinoUi { final style = AppPlatformScope.of(this); return switch (style) { diff --git a/templates/flutter/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs b/templates/flutter/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs index 4668d4c..65a940e 100644 --- a/templates/flutter/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs +++ b/templates/flutter/base/lib/src/shared/widgets/ui/ui_showcase_screen.dart.hbs @@ -1,6 +1,6 @@ import '../../../imports/imports.dart'; -/// Live gallery of App* (and optional ShadApp*) components. +/// Live gallery of App (and optional ShadApp) components. /// /// Open via [AppRoutes.uiShowcase] in debug builds. class UiShowcaseScreen extends StatelessWidget { @@ -72,7 +72,7 @@ class UiShowcaseScreen extends StatelessWidget { SizedBox(height: AppSpacing.sm), AppEmptyState( title: 'Nothing here', - subtitle: 'Empty states use App* actions.', + subtitle: 'Empty states use App actions.', actionLabel: 'Retry', onAction: () {}, ), diff --git a/templates/flutter/partials/llm/design-quick-ref.hbs b/templates/flutter/partials/llm/design-quick-ref.hbs index c75833f..9fcd938 100644 --- a/templates/flutter/partials/llm/design-quick-ref.hbs +++ b/templates/flutter/partials/llm/design-quick-ref.hbs @@ -11,5 +11,5 @@ Full reference: **[DESIGN.md](DESIGN.md)** - **Tokens:** `AppSpacing`, `AppBorders`, `AppShadows`, `AppDurations`, `AppCurves` from `lib/src/theme/theme_constants.dart` - **Extensions:** `context_extension.dart` — `width`, `height`, `isDarkMode`, `showAppDialog`, `showTypedSnackBar` {{#if flags.usesScreenutil}}- **ScreenUtil:** baseline 390×844; `.w` `.h` `.r` `.sp`; wrapper already in app — no second `ScreenUtilInit`{{else}}- **Layout:** `MediaQuery` / `LayoutBuilder` / `Flexible` — no ScreenUtil{{/if}} -- **Widgets:** reusable UI in `lib/src/shared/widgets/ui/app/` (App* adaptive kit){{#if flags.usesShadcn}}; shadcn wrappers in `ui/shadcn/`{{/if}} — use tokens not magic numbers +- **Widgets:** reusable UI in `lib/src/shared/widgets/ui/app/` (App adaptive kit){{#if flags.usesShadcn}}; shadcn wrappers in `ui/shadcn/`{{/if}} — use tokens not magic numbers - **Platform style:** `{{ui.platformStyle}}` via `AppPlatformScope` / `context.isCupertinoUi` \ No newline at end of file diff --git a/templates/flutter/partials/llm/packages-list.hbs b/templates/flutter/partials/llm/packages-list.hbs index 2072910..88f6eb4 100644 --- a/templates/flutter/partials/llm/packages-list.hbs +++ b/templates/flutter/partials/llm/packages-list.hbs @@ -24,4 +24,4 @@ {{#if flags.usesFlutterHooks}}- `flutter_hooks`{{/if}} {{#if flags.usesDotenv}}- `flutter_dotenv`{{/if}} {{#if flags.usesSkeletonizer}}- `skeletonizer`{{/if}} -{{#if flags.usesShadcn}}- `shadcn_ui` — custom component kit + `ShadApp*` wrappers{{/if}} +{{#if flags.usesShadcn}}- `shadcn_ui` — custom component kit + `ShadApp` wrappers{{/if}} diff --git a/templates/flutter/partials/llm/ui-components.hbs b/templates/flutter/partials/llm/ui-components.hbs index abb46c5..207327f 100644 --- a/templates/flutter/partials/llm/ui-components.hbs +++ b/templates/flutter/partials/llm/ui-components.hbs @@ -49,5 +49,5 @@ context.designTokens.paddingMedium {{else}} ### shadcn kit -Not enabled for this project. Re-generate with shadcn opted in from the UI & Theme step to add `ShadApp*` wrappers. +Not enabled for this project. Re-generate with shadcn opted in from the UI & Theme step to add `ShadApp` wrappers. {{/if}} diff --git a/tests/unit/ui-components.spec.ts b/tests/unit/ui-components.spec.ts index e525e94..f4920e9 100644 --- a/tests/unit/ui-components.spec.ts +++ b/tests/unit/ui-components.spec.ts @@ -81,7 +81,7 @@ describe("UI component library generation", () => { expect(app).toContain("GlobalShadLocalizations") }) - it("uses ShadApp* names in login when defaultKit is shadcn", async () => { + it("uses ShadApp names in login when defaultKit is shadcn", async () => { const config = { ...buildConfig(base), ui: {