apps/mobile is replacing shadcn_flutter with the app-owned design system in
lib/core/ui/ (see Mobile design system).
The migration runs in reviewable batches. A CI ratchet stops new imports, and
the dependency is removed once nothing imports it.
Ratchet
scripts/check-mobile-shadcn-imports.js scans apps/mobile/lib and
apps/mobile/test. It treats a direct package:shadcn_flutter import, or an
import of an in-repo shim, as a shadcn import. The shims are
shadcn_flutter_compat.dart, shadcn_material_bridge.dart,
shadcn_localizations_fallback.dart and mobile_shad_theme.dart. Each such
file must appear in apps/mobile/tool/shadcn_import_allowlist.txt.
- A new importer fails with a pointer to
package:mobile/core/ui/ui.dart. - Every URI of every
import/exportdirective is checked. A small Dart tokenizer (scripts/dart-directive-uris.js) skips comments and strings, so comments inside a directive, several directives on one line, conditionalif (dart.library.x)alternatives and adjacent string literals are all read. Dart escapes (\uXXXX,\u{...},\xHH, simple escapes) are decoded, and a URI with a malformed escape or interpolation fails closed. Relative andpackage:mobile/paths are normalised before matching a shim. - An allowlisted file that no longer imports shadcn also fails. Run
node scripts/check-mobile-shadcn-imports.js --pruneto remove migrated entries. The flag never adds entries. - Entries missing from the allowlist at the base revision fail, so a PR
cannot add an importer together with an allowlist line. CI passes the PR
base (or the previous
maincommit, elsemain) with--base. Locally the base defaults to the merge base withorigin/main. When no base resolves, the check fails closed and asks for--base. - When the list is empty, the check fails until
flutter pub remove shadcn_flutterhas run.
bun check:changed
whenever mobile Dart or the allowlist changes, and as bun check:mobile-shadcn.
Part files (part of) inherit their library’s import. A library is migrated
only when neither it nor any of its parts uses the prefix.
The initial allowlist (October 2026) has 325 files: 253 under lib/ and
72 under test/. Counting the 139 part files, 464 files use shad.
identifiers.
Component mapping
Codemod
scripts/mobile-shadcn-codemod.js performs the mechanical renames on whole
libraries, including their parts:
<prefix>.Gap( → AppGap(, <prefix>.Theme.of( →
AppTokens.of(, and <prefix>.LucideIcons.* → LucideIcons.* or
AppIcons.*. It adds package:mobile/core/ui/ui.dart and drops the shadcn
import once neither the library nor its parts use the prefix. The codemod covers
1,705 icons, 1,282 gaps and 770 theme reads, about 3,750 of the 5,270 prefixed
uses. A dry run over the whole app touches 322 files. Afterwards, 263 files
still import shadcn (62 freed).
After running it, fix values passed into APIs typed shad.ThemeData, then
run flutter analyze and the feature’s tests on the remote validation lane.
Replace the remaining ~1,500 widget uses by hand with the table above.
Batch order
Each batch is one PR, or one layer of a native stack, owned by one lane. Run the codemod for its folders, migrate the remaining widgets, prune the allowlist, and attach renders for changed screens. LOC and counts cover every file that usesshad., parts included.
Phases 2–7 can run in parallel after phase 1 merges, because they touch
disjoint folders. Phase 8 waits for 2–7. Large features (
tasks_boards,
time_tracker, finance) should be split by sub-folder into stack layers of
about 10–15 files.
Final removal
- Allowlist empty (
--pruneleaves only the header). - Delete
core/widgets/shadcn_flutter_compat.dart,shadcn_material_bridge.dart,shadcn_localizations_fallback.dartandcore/theme/mobile_shad_theme.dart. FoldAppearanceTheme.colorSchemeintoAppColorTokensdirectly. flutter pub remove shadcn_flutter, thenflutter pub get, then analyze and test on the validation lane.- Remove the ratchet step from
.github/workflows/mobile.yaml, thecheck-changedplan entry, the script, its test and the allowlist file. - Update this page and the mobile skill reference to mark the migration complete.