Project structure
How Kite is laid out, and the one architectural rule that matters.
lib/
├── main.dart usePathUrlStrategy() + ProviderScope
├── app.dart ShadApp.router, theme and locale wiring
├── kite_ui/ the ONLY folder that imports the design library
│ ├── _shadcn.dart single import surface — the swap point
│ ├── tokens.dart KiteSpace, KiteRadius, KiteBreak, KiteColors, KiteText
│ └── … button, card, chart, data_table, menu, inputs, …
├── core/
│ ├── router/ routes.dart (paths + nav) and app_router.dart
│ ├── theme/ light + dark × 3 accents
│ ├── data/ DataProvider + mock / REST / Supabase adapters
│ ├── auth/ session controller, guard wiring
│ └── l10n/ locale controller
├── l10n/ .arb sources and generated localisations
├── shared/
│ ├── layout/ AppShell — sidebar, top bar, mobile bottom bar
│ └── widgets/ empty, loading and error states
└── features/ one folder per screen group
The one rule
No shadcn_ui type may appear outside
lib/kite_ui/.
It is a pre-1.0 library (0.55.x, capped by trina_grid) that
ships breaking changes. Confining it means the blast radius of an upgrade is
one folder rather than every screen. Features import Kite widgets; Kite
widgets import the design library.
CI enforces this with a grep, so it cannot quietly rot:
if grep -rn --include='*.dart' "package:shadcn_ui" lib | grep -v '^lib/kite_ui/'; then
echo "::error::shadcn_ui imported outside lib/kite_ui/"
exit 1
fi
The practical effect: features never name a Shad* type. They read
colour through KiteColors.of(context) and type through
KiteText.of(context), both of which are extension types over the
design library's own — zero runtime cost, and the only thing that has to
change if the library does.
Adding a screen
Four edits, in this order:
- Create
lib/features/<group>/<name>_screen.dart. - Add a path constant to
Rinlib/core/router/routes.dart. - Add a
NavItemto the rightNavGroupin the same file, and a case toNavItem.labelOfif it should localise. - Add a
GoRouteinside theShellRouteinapp_router.dart.
Screens inside the shell get the sidebar, top bar and auth guard for free. Screens declared outside it (the five auth screens, the 500 page) render standalone.
Why features never import each other
Anything two features both need lives in shared/ or
kite_ui/. It keeps deletion cheap — removing a feature you do not
want is deleting its folder and its route, not untangling it from three
others.
Tests
test/
├── data_provider_test.dart the contract: totals, search, filters, sort, mutations
├── rest_provider_test.dart both HTTP adapters against a fake client
├── format_test.dart money formatting
└── session_test.dart sign in, initials, sign out
26 tests. They cover the parts where being wrong is silent — a
total that reports the page length instead of the row count still
renders a table, it just paginates wrongly.