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
+
+
+
+
+ Blog
+
+
+ Generate Project →
+
+
+
+
+
+
+
+
+
+
+
+ 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: {