learning spec; flutter FE arch (repo/file structure)

This commit is contained in:
eutopiacode 2026-06-04 22:37:31 +03:00
parent 0fce0b62bb
commit 7c6d101756
7 changed files with 774 additions and 12 deletions

214
LEARNING-SPEC.md Normal file
View file

@ -0,0 +1,214 @@
# LEARNING-SPEC.md
context for any agent writing flutter code for eutopic.
## who is reading this code
a junior developer. no flutter or dart experience. basic understanding of frontend concepts (html/css level). goal: understand the code well enough to write business logic, modify screens, and eventually own the codebase. not a throwaway learning exercise — this is the real app.
## annotation requirements
every file must include three layers of annotation. this is non-negotiable.
### 1. file-top architecture comment
a short paragraph (36 sentences) at the top of every file, inside a block comment. must explain:
- what this file is and what it does
- why it exists as a separate file (not just "it's a widget")
- how it connects to the rest of the app (what calls it, what it calls)
example format:
```dart
/*
* feed_screen.dart
*
* the main home feed. this is what the user sees after login — a chronological
* list of photo posts from people they follow. it lives at the '/' route and is
* the first tab in the bottom nav bar.
*
* this screen owns fetching the post list from the api and passing individual
* posts down to PostCard widgets. it does not know how to render a post — that
* is PostCard's job. separation of concerns: screen = data, widget = display.
*/
```
### 2. boilerplate explanation comments
every structural/architectural decision in the boilerplate must have a comment explaining *why*, not just *what*. target: someone who has never seen flutter before can read this file and understand the pattern.
things that always need explanation:
- `StatelessWidget` vs `StatefulWidget` — explain the difference and why this file uses one over the other
- `BuildContext context` — what it is and why it's passed everywhere
- `const` constructors — why they exist, what they do for performance
- `@override` — what it means in dart
- `super.key` — what keys are and why the constructor takes one
- `Widget build(BuildContext context)` — what this method is and when flutter calls it
- any `initState` / `dispose` usage — explain the lifecycle
- any `setState(() {})` — explain what it triggers and why it's needed
- named parameters with `{}` braces vs positional — explain the difference
- `required` keyword — explain what it enforces
- `?` nullable types — explain dart's null safety model briefly on first use per file
### 3. logic TODO blocks
for every section the developer will fill in themselves, write a TODO comment that includes:
- a plain-english description of what needs to happen
- a complete dart syntax example showing the pattern to use (as a comment)
- any gotchas or things to watch out for
format:
```dart
// TODO: [plain english description]
//
// example:
// [complete working dart code snippet]
//
// note: [any gotcha or important thing to know]
```
the example must be real, working dart — not pseudocode. it should be close enough to the actual task that the developer can adapt it directly.
## hard sections — write fully, annotate heavily
these three areas are too complex for a junior developer to implement from a TODO. write them completely, but annotate every non-obvious line as if explaining to someone who has never seen async dart or flutter internals:
1. **tus resumable upload** (`shared/services/upload_service.dart`)
- background isolates, tus_client package usage, progress callbacks
- explain what an isolate is, why uploads need one, what happens without it
2. **go_router deep-link invite handling** (`shared/router/app_router.dart`)
- the `redirect` callback, how deep links arrive, how the invite code is extracted from the uri
- explain what a deep link is and the ios/android app link mechanism briefly
3. **story auto-advance timer + gesture layer** (`features/stories/story_viewer_screen.dart`)
- `Timer.periodic`, `AnimationController` for the progress bar, `GestureDetector` for tap-left/tap-right and swipe-down-to-exit
- explain the animation controller lifecycle and why dispose() matters here
## dart syntax reference (include in every file)
every file must include this block, verbatim, near the top after the file-top architecture comment. it is a standing reference so the developer never needs to leave the file to look up basic syntax.
```dart
/*
* dart syntax reference — patterns used in this file
*
* variables
* final String name = 'ada'; // runtime constant — set once, never reassigned
*
* const int max = 10; // compile-time constant — value must be known at build time
*
* String? bio; // nullable — this variable can be null (dart null safety)
*
* late String token; // late — will be assigned before first use, not at declaration
*
* functions
* String greet(String name) { return 'hi $name'; } // regular function
*
* String greet(String name) => 'hi $name'; // arrow shorthand — same as above, one expression only
*
* void log({required String msg}) { ... } // named parameter — caller writes: log(msg: 'x')
*
* void log({String msg = 'hello'}) { ... } // named parameter with default value
*
* void log(String msg) { ... } // positional parameter — caller writes: log('x')
*
* async
* Future<String> fetchName() async { // async function — returns a Future (like a JS Promise)
* final result = await apiCall(); // await pauses here until the Future resolves
* return result;
* }
*
* lists & maps
* final items = <String>['a', 'b']; // typed list
*
* final map = <String, int>{'apples': 3}; // typed map (like a JS object/dict)
*
* for (final item in items) { print(item); } // for-in loop over a list
*
* items.map((x) => x.toUpperCase()).toList() // transform every item (like JS .map())
*
* items.where((x) => x != 'a').toList() // keep items matching condition (like JS .filter())
*
* classes
* class Post {
* final String id;
* final String? caption; // optional field — may be null
* const Post({required this.id, this.caption}); // const constructor, named params
* }
*
* null safety
* bio?.length // safe access — returns null if bio is null, not an error
*
* bio ?? 'no bio' // fallback — use 'no bio' if bio is null
*
* bio! // force-unwrap — crashes if null. avoid unless certain.
*
* control flow
* if (x != null) { ... } else { ... }
*
* final label = isOwn ? 'you' : user.name; // ternary — shorthand if/else (same as JS)
*
* switch (tag) {
* case 'event': ... break;
* default: ...
* }
*
* string interpolation
* 'hello $name' // insert a variable directly
*
* 'count: ${list.length}' // insert an expression — use braces when it's more than a variable
*/
```
this block should appear in every file. agents must not remove or shorten it — the developer is learning dart as they go and will refer to it repeatedly.
## style rules
- use lowercase prose in comments (matches the project's communication style)
- no marketing language or enthusiasm ("great!", "easy!", "simply")
- be direct. if something is genuinely complex, say so — don't oversimplify
- prefer short sentences over long ones
- comment *why* over *what* — the code shows what, the comment shows why
## file order
write files in this order so concepts build on each other:
1. `lib/main.dart`
2. `lib/app.dart`
3. `lib/core/theme/app_colors.dart`
4. `lib/core/theme/app_theme.dart`
5. `lib/shared/models/` — all model files
6. `lib/core/router/app_router.dart` — hard section
7. `lib/shared/services/api_client.dart`
8. `lib/shared/services/auth_service.dart`
9. `lib/shared/services/upload_service.dart` — hard section
10. `lib/core/widgets/bottom_nav_bar.dart`
11. `lib/shared/widgets/` — shared widgets (photo_viewer, emoji_picker_sheet, loading_indicator, error_view)
12. `lib/features/feed/` — full feature, end to end (reference pattern for all other features)
13. `lib/features/onboarding/`
14. `lib/features/compose/`
15. `lib/features/posters/`
16. `lib/features/stories/` — hard section (story_viewer_screen.dart)
17. `lib/features/profile/`
18. `lib/features/settings/`
19. `lib/features/feedback/`
## project context
- app: eutopic — photo-first social app, invite-only, no notifications, no DMs, no algorithm
- stack: flutter (dart), go_router, tus_client, lottie animations
- folder structure: feature-first (see `docs/ARCHITECTURE.md` — frontend section)
- full concept: `docs/CONCEPT.md`
- screen/navigation spec: `model-tests/02-flutter-folder-structure.md`
- no notification-related code anywhere — this is a hard constraint
- no push alerts, no unread badges, no FCM/APNs
## what counts as done
a file is complete when:
- it compiles without errors
- all three annotation layers are present
- every TODO block has a working dart example
- hard sections are fully implemented with line-level annotation
- no notification-related imports or widgets exist anywhere

View file

@ -2,15 +2,36 @@
*a small good place. social media that visits you when you visit it.* *a small good place. social media that visits you when you visit it.*
photo-first social app for small trusted communities. features: photo-first social app for small trusted communities — an alternative to instagram that doesn't track you, notify you, or optimize for your attention. built for real people who already trust each other, not strangers building an audience.
- profile with bio and profile picture
- slide posts (carousel)
- stories
- emoji reactions
- "posters" event page, like a bulletin board. local events, informational posts, fundraisers
- chronological feed, no algorithm, no notifications, no tracking. invite-only.
→ [docs/CONCEPT.md](docs/CONCEPT.md) for full concept, stack, and design decisions. ## what it does
- **feed** — chronological, photos only, up to 10 per post. no algorithm, no ranking.
- **stories** — 10s auto-advance, 24h expiry, no replies.
- **reactions** — the poster picks up to 5 emoji for their post; open picker as fallback.
- **profiles** — username, display name, avatar, bio.
- **posters** — event page / bulletin board for local events, info posts, fundraisers.
- **invite-only signup** — magic-link email auth, no passwords, no server-picking.
## what it doesn't do (by design)
no push notifications. no DMs. no analytics. no algorithm. no ads. no tracking. no premium tier. no "we believe in" manifesto.
eutopic doesn't ping you. you visit when you feel like it.
## stack
**backend** — bun + elysia, sqlite (postgres-ready), activitypub via fedify (federation off by default), resumable uploads via tus.io, nginx + systemd on a njalla VPS.
**mobile** — flutter (iOS + android, single codebase), go_router, lottie animations, background upload via isolates / WorkManager / URLSession.
**infrastructure** — radicle as primary code forge, self-hosted CI runner, liberapay for funding, F-Droid + sideloaded APK for android distribution.
## funding
eutopic is donation-funded. the about page shows real monthly hosting costs and a liberapay link. no donor badges, no premium features, no nagging.
→ [docs/CONCEPT.md](docs/CONCEPT.md) for full concept, architecture decisions, and design rationale.
--- ---

View file

@ -3,3 +3,155 @@
*to be fleshed out before v0 alpha.* *to be fleshed out before v0 alpha.*
covers: backend structure, API shape, federation mechanics, background job design, endpoint inventory. covers: backend structure, API shape, federation mechanics, background job design, endpoint inventory.
---
## frontend — flutter folder structure
```
lib/
├── main.dart // entry point. calls runApp(). almost nothing lives here — just bootstraps app.dart.
├── app.dart // root widget. sets up theme, router, and any top-level providers. think of it as the <App /> in react.
├── core/ // framework-level setup — not domain-specific. no business logic here.
│ ├── theme/
│ │ ├── app_theme.dart // materialapp theme config — fonts, shape, component defaults
│ │ └── app_colors.dart // all color constants in one place. nothing hardcodes hex values elsewhere.
│ ├── router/
│ │ └── app_router.dart // go_router config. every route in the app is registered here.
│ │ // also handles deep-link invite codes (hard section — fully written).
│ ├── widgets/
│ │ └── bottom_nav_bar.dart // the five-tab nav bar. lives in core/ because it is framework plumbing —
│ │ // it knows about routes, not domain data. every screen uses it.
│ └── constants/
│ └── app_constants.dart // magic values that would otherwise be scattered: api base url, story duration, max photos, etc.
├── shared/ // domain code used by more than one feature. if only one feature uses it, it lives in that feature folder.
│ ├── models/
│ │ ├── user.dart // the User type. used everywhere — feed, profile, follow suggestions, etc.
│ │ ├── post.dart // a feed post. 110 photos, optional caption, reaction set, author.
│ │ ├── poster_post.dart // a posters post. same base as post but adds tag + event fields.
│ │ ├── story.dart // a story. 24h expiry, witness list, no replies.
│ │ ├── reaction.dart // a single reaction: user + post + emoji + timestamp.
│ │ ├── comment.dart // a comment on a post. flat list, chronological, no threading.
│ │ ├── survey.dart // an active survey. multiple choice, 36h expiry.
│ │ ├── invite_code.dart // an invite. carries the inviter's id for auto-follow suggestions.
│ │ └── social_handle.dart // signal/telegram/custom handle + visibility setting (none/mutuals/all).
│ ├── widgets/
│ │ ├── photo_viewer.dart // full-screen photo viewer used by feed, posters, and profile grid.
│ │ ├── emoji_picker_sheet.dart // bottom sheet (slides up from below) for picking an emoji reaction.
│ │ ├── loading_indicator.dart // shared spinner/skeleton — keeps loading states visually consistent.
│ │ └── error_view.dart // shared error state widget for failed api calls.
│ ├── services/
│ │ ├── api_client.dart // http client wrapper. all api calls go through here — one place to set auth headers, base url, error handling.
│ │ ├── auth_service.dart // magic-link auth flow. stores session token, exposes current user.
│ │ └── upload_service.dart // tus resumable upload. runs in a background isolate. hard section — fully written.
│ └── utils/
│ ├── date_formatter.dart // relative time formatting ("3h ago", "yesterday") used across feed, posters, stories.
│ └── validators.dart // input validation for username, email, invite code fields.
├── features/ // one folder per screen group. each folder is self-contained: screen + widgets + state.
│ │ // rule: a widget in features/feed/ should never be imported by features/profile/.
│ │ // if two features need the same widget, it moves to shared/widgets/.
│ │
│ ├── onboarding/
│ │ ├── onboarding_screen.dart // shell screen that hosts the multi-step onboarding flow.
│ │ ├── widgets/
│ │ │ ├── invite_gate.dart // step 1: invite code entry. prefilled from deep link if available.
│ │ │ ├── email_username_form.dart // step 2: email + username.
│ │ │ ├── magic_link_confirm.dart // step 3: "check your email" holding screen.
│ │ │ ├── social_handle_opt_in.dart // step 4: signal/telegram/custom. skippable.
│ │ │ └── follow_suggestions.dart // step 5: friends-of-inviter. skippable.
│ │ └── providers/
│ │ └── onboarding_provider.dart // holds onboarding state across steps: invite code, entered email, etc.
│ │
│ ├── feed/
│ │ ├── feed_screen.dart // home tab. fetches post list, renders it, handles pull-to-refresh.
│ │ ├── widgets/
│ │ │ ├── post_card.dart // a single post. renders author, photos, reactions. does not fetch — receives a Post object.
│ │ │ ├── post_photo_swipe.dart // swipeable photo carousel within a post card. up to 10 photos.
│ │ │ ├── reaction_bar.dart // emoji reactions row below a post. shows the post's reaction set.
│ │ │ ├── comment_sheet.dart // bottom sheet (slides up) showing flat comment thread for a post.
│ │ │ └── survey_card.dart // injected at slot 1 when an active survey exists. not a real post.
│ │ └── providers/
│ │ └── feed_provider.dart // manages post list state: loading, pagination, optimistic reaction updates.
│ │
│ ├── posters/
│ │ ├── posters_screen.dart // posters tab. 2-column grid, paginated, filterable by tag.
│ │ ├── widgets/
│ │ │ ├── poster_grid.dart // the 2-column grid layout. handles infinite scroll pagination.
│ │ │ ├── poster_card.dart // a single poster card. shows tag, title, event date if applicable.
│ │ │ ├── poster_filter_bar.dart // tab strip or dropdown for filtering by event/fundraiser/current affairs. TBD.
│ │ │ └── rsvp_button.dart // placeholder — rsvp interaction model not yet designed. see OPEN-QUESTIONS.md.
│ │ └── providers/
│ │ └── posters_provider.dart
│ │
│ ├── compose/
│ │ ├── compose_screen.dart // center nav tab. opens as modal or full screen (TBD). orchestrates the compose flow.
│ │ ├── widgets/
│ │ │ ├── photo_picker_tile.dart // photo selection grid. up to 10 photos.
│ │ │ ├── caption_field.dart // optional caption text input.
│ │ │ ├── destination_toggle.dart // feed vs posters toggle.
│ │ │ ├── tag_picker.dart // event / fundraiser / current affairs. required if posting to posters.
│ │ │ ├── event_fields.dart // date/time fields shown only when tag = event.
│ │ │ ├── reaction_set_picker.dart // choose up to 5 emoji for this post's reaction set.
│ │ │ ├── savable_toggle.dart // saveable on/off toggle. default off.
│ │ │ └── upload_progress_animation.dart // hand-drawn lottie animation shown while tus upload runs in background.
│ │ └── providers/
│ │ └── compose_provider.dart // compose form state + triggers upload_service on confirm.
│ │
│ ├── stories/
│ │ ├── stories_screen.dart // stories tab. shows list of active stories as thumbnails.
│ │ ├── story_viewer_screen.dart // full-screen story playback. hard section — timer, gestures, progress bar fully written.
│ │ ├── story_create_screen.dart // camera or photo picker → post. no drafts.
│ │ ├── widgets/
│ │ │ ├── story_thumbnail.dart // thumbnail shown in the stories tab list.
│ │ │ ├── story_progress_bar.dart // segmented progress bar at top of viewer. one segment per story.
│ │ │ ├── story_filter_overlay.dart // swipe-gesture filter layer. applied at view time (TBD).
│ │ │ └── witness_list_sheet.dart // bottom sheet showing who has seen your story. visible to author only.
│ │ └── providers/
│ │ └── stories_provider.dart
│ │
│ ├── profile/
│ │ ├── profile_screen.dart // own profile and others' profiles share this screen. own = shows settings. other = shows follow button.
│ │ ├── widgets/
│ │ │ ├── profile_header.dart // avatar, display name, username, bio.
│ │ │ ├── social_handle_display.dart // signal/telegram/custom handle, shown per display_social_handle setting.
│ │ │ ├── contact_prefs_display.dart // free text contact prefs ("bad texter", "weekends only").
│ │ │ ├── profile_post_grid.dart // grid of user's own posts.
│ │ │ ├── follow_button.dart // follow/unfollow. shown on others' profiles only.
│ │ │ └── settings_button.dart // link to settings. shown on own profile only.
│ │ └── providers/
│ │ └── profile_provider.dart
│ │
│ ├── settings/
│ │ ├── settings_screen.dart
│ │ ├── widgets/
│ │ │ ├── social_handle_settings.dart // edit signal/telegram/custom handles.
│ │ │ └── display_preferences.dart // display_social_handle: none / mutuals / all.
│ │ └── providers/
│ │ └── settings_provider.dart
│ │
│ └── feedback/
│ ├── feedback_screen.dart // always accessible. active survey at top if one exists, free text below always.
│ ├── widgets/
│ │ ├── active_survey_card.dart // renders the current multiple-choice survey if one is active.
│ │ ├── survey_question_tile.dart // a single survey question with answer options.
│ │ └── free_text_field.dart // permanent free-text input. always present, survey or not.
│ └── providers/
│ └── feedback_provider.dart
```
### structural decisions
**`core/` vs `shared/`** — `core/` is framework plumbing (theme, router, constants). `shared/` is domain code (models, services, reusable widgets). the distinction matters when onboarding a new contributor: core is "don't touch unless you know what you're doing," shared is "add things here when two features need the same thing."
**feature-first, strict imports** — nothing in `features/feed/` imports from `features/profile/`. cross-feature dependencies always flow through `shared/`. this keeps features independently readable and prevents tangled refactors later.
**one `*_screen.dart` per feature, more where flows are genuinely distinct** — screens own data fetching and pass data down to widgets. widgets are display-only and receive everything they need as constructor parameters. this is the flutter equivalent of container/presentational component separation in react. stories is the exception: it has three screen files (`stories_screen`, `story_viewer_screen`, `story_create_screen`) because the viewer (timer + gesture layer) and creator (camera + picker) are full-screen flows with complex independent lifecycles, not components. the rule bends for stories; it does not apply everywhere.
**`providers/` per feature** — state management sits next to the screen it serves, not in a global `state/` folder. a provider that only `feed_screen.dart` uses has no reason to be visible from `profile/`.
**no notification files anywhere** — hard constraint. no FCM, no APNs, no device tokens, no unread badge logic. if a future contributor adds one, it's a deliberate reversal of a design decision, not an accident.
**`comment_sheet` and `witness_list_sheet` are sheets, not screens** — they slide up over the current screen without replacing it. naming them `*_sheet.dart` signals this — they are never registered as routes in `app_router.dart`.

View file

@ -5,10 +5,7 @@ date: 2026-06-04
--- ---
thinking carefully... i'm looking at concept.md, open-questions.md, and the thinking carefully... i'm looking at concept.md, open-questions.md, and AGENTS.md . the core
implied context from AGENTS.md—even though i haven't read AGENTS.md yet, the other
docs give enough signal to start the high-level risk assessment for a pre-build
review, focusing on architecture rather than feature completeness yet. the core
tension seems to be "simple feel vs. robust future proofing." tension seems to be "simple feel vs. robust future proofing."
i see three high-risk areas stemming from the confluence of tech choices and i see three high-risk areas stemming from the confluence of tech choices and

View file

@ -0,0 +1,95 @@
lib/
├── main.dart
├── app.dart
├── features/
│ ├── onboarding/
│ │ ├── onboarding_screen.dart
│ │ ├── invite_gate_screen.dart
│ │ ├── email_username_screen.dart
│ │ ├── magic_link_confirm_screen.dart
│ │ ├── social_handle_optin_screen.dart
│ │ ├── follow_suggestions_screen.dart
│ │ └── widgets/
│ │ ├── invite_code_field.dart
│ │ └── follow_suggestion_card.dart
│ │
│ ├── feed/
│ │ ├── feed_screen.dart
│ │ └── widgets/
│ │ ├── post_card.dart
│ │ ├── photo_carousel.dart
│ │ ├── reaction_bar.dart
│ │ ├── comments_sheet.dart
│ │ └── survey_card.dart
│ │
│ ├── posters/
│ │ ├── posters_screen.dart
│ │ └── widgets/
│ │ ├── poster_card.dart
│ │ ├── poster_tag_filter.dart
│ │ └── event_fields.dart
│ │
│ ├── compose/
│ │ ├── compose_screen.dart
│ │ └── widgets/
│ │ ├── photo_picker.dart
│ │ ├── caption_field.dart
│ │ ├── destination_toggle.dart
│ │ ├── poster_tag_picker.dart
│ │ ├── reaction_set_picker.dart
│ │ ├── saveable_toggle.dart
│ │ └── upload_progress_animation.dart
│ │
│ ├── stories/
│ │ ├── stories_screen.dart
│ │ ├── story_viewer_screen.dart
│ │ ├── story_create_screen.dart
│ │ └── widgets/
│ │ ├── story_thumbnail.dart
│ │ ├── story_progress_bar.dart
│ │ ├── story_filter_overlay.dart
│ │ └── witness_list_sheet.dart
│ │
│ ├── profile/
│ │ ├── profile_screen.dart
│ │ └── widgets/
│ │ ├── avatar_header.dart
│ │ ├── social_handles_row.dart
│ │ ├── posts_grid.dart
│ │ └── follow_button.dart
│ │
│ ├── settings/
│ │ ├── settings_screen.dart
│ │ └── widgets/
│ │ ├── social_handle_visibility_picker.dart
│ │ └── contact_prefs_field.dart
│ │
│ └── feedback/
│ ├── feedback_screen.dart
│ └── widgets/
│ ├── survey_question_card.dart
│ └── freetext_field.dart
├── shared/
│ ├── models/
│ │ ├── user.dart
│ │ ├── post.dart
│ │ ├── story.dart
│ │ ├── poster.dart
│ │ ├── reaction.dart
│ │ ├── survey.dart
│ │ └── invite.dart
│ ├── widgets/
│ │ ├── bottom_nav_bar.dart
│ │ ├── eutopic_photo_viewer.dart
│ │ └── emoji_picker_sheet.dart
│ ├── services/
│ │ ├── auth_service.dart
│ │ ├── upload_service.dart
│ │ └── api_client.dart
│ ├── router/
│ │ └── app_router.dart
│ └── theme/
│ ├── app_theme.dart
│ └── app_colors.dart

View file

@ -0,0 +1,154 @@
# flutter folder structure — eutopic
model: deepseek-reasoner
test: instruction-following, feature-first structure, domain accuracy
---
```
lib/
├── main.dart
├── app.dart
├── core/
│ ├── theme/
│ │ ├── app_theme.dart
│ │ └── app_colors.dart
│ ├── router/
│ │ └── app_router.dart
│ └── constants/
│ └── app_constants.dart
├── shared/
│ ├── models/
│ │ ├── user.dart
│ │ ├── post.dart
│ │ ├── poster_post.dart
│ │ ├── story.dart
│ │ ├── survey.dart
│ │ ├── reaction.dart
│ │ ├── comment.dart
│ │ ├── invite_code.dart
│ │ └── social_handle.dart
│ ├── widgets/
│ │ ├── photo_grid.dart
│ │ ├── photo_swipe.dart
│ │ ├── emoji_picker.dart
│ │ ├── loading_indicator.dart
│ │ └── error_view.dart
│ ├── services/
│ │ ├── api_client.dart
│ │ ├── auth_service.dart
│ │ └── upload_service.dart
│ └── utils/
│ ├── date_formatter.dart
│ └── validators.dart
├── features/
│ ├── onboarding/
│ │ ├── onboarding_screen.dart
│ │ ├── widgets/
│ │ │ ├── invite_gate.dart
│ │ │ ├── email_username_form.dart
│ │ │ ├── magic_link_confirm.dart
│ │ │ ├── social_handle_opt_in.dart
│ │ │ └── follow_suggestions.dart
│ │ └── providers/
│ │ └── onboarding_provider.dart
│ │
│ ├── feed/
│ │ ├── feed_screen.dart
│ │ ├── widgets/
│ │ │ ├── post_card.dart
│ │ │ ├── post_photo_swipe.dart
│ │ │ ├── reaction_bar.dart
│ │ │ ├── comment_sheet.dart
│ │ │ └── survey_card.dart
│ │ └── providers/
│ │ └── feed_provider.dart
│ │
│ ├── posters/
│ │ ├── posters_screen.dart
│ │ ├── widgets/
│ │ │ ├── poster_grid.dart
│ │ │ ├── poster_card.dart
│ │ │ ├── poster_filter_bar.dart
│ │ │ └── rsvp_button.dart
│ │ └── providers/
│ │ └── posters_provider.dart
│ │
│ ├── compose/
│ │ ├── compose_screen.dart
│ │ ├── widgets/
│ │ │ ├── photo_picker_tile.dart
│ │ │ ├── caption_field.dart
│ │ │ ├── destination_toggle.dart
│ │ │ ├── tag_picker.dart
│ │ │ ├── event_fields.dart
│ │ │ ├── reaction_set_picker.dart
│ │ │ ├── savable_toggle.dart
│ │ │ └── upload_progress_animation.dart
│ │ └── providers/
│ │ └── compose_provider.dart
│ │
│ ├── stories/
│ │ ├── stories_screen.dart
│ │ ├── widgets/
│ │ │ ├── story_player.dart
│ │ │ ├── story_filter_overlay.dart
│ │ │ ├── story_capture.dart
│ │ │ └── witness_list.dart
│ │ └── providers/
│ │ └── stories_provider.dart
│ │
│ ├── profile/
│ │ ├── profile_screen.dart
│ │ ├── widgets/
│ │ │ ├── profile_header.dart
│ │ │ ├── profile_post_grid.dart
│ │ │ ├── social_handle_display.dart
│ │ │ ├── contact_prefs_display.dart
│ │ │ ├── follow_button.dart
│ │ │ └── settings_button.dart
│ │ └── providers/
│ │ └── profile_provider.dart
│ │
│ ├── settings/
│ │ ├── settings_screen.dart
│ │ ├── widgets/
│ │ │ ├── social_handle_settings.dart
│ │ │ └── display_preferences.dart
│ │ └── providers/
│ │ └── settings_provider.dart
│ │
│ └── feedback/
│ ├── feedback_screen.dart
│ ├── widgets/
│ │ ├── active_survey_card.dart
│ │ ├── survey_question_tile.dart
│ │ └── free_text_field.dart
│ └── providers/
│ └── feedback_provider.dart
└── navigation/
├── app_navigation.dart
└── bottom_tab_bar.dart
```
---
### design rationale
**feature-first** — each screen lives under `features/{name}/` with co-located widgets and state. no flat `screens/` or `widgets/` directory.
**shared models** in `shared/models/` so the `post.dart` model used by feed, posters, and compose lives in one place. feature-specific models (like invite_code for onboarding) could stay in the feature folder — here i kept them in shared since they're referenced across auth boundaries.
**no notification files** — per the spec: no notifications, no dms, no push alerts. you won't find a `notifications/` folder or a `push_service.dart`.
**story as tab** — the spec is explicit that stories are a bottom tab, not a top strip. `stories_screen.dart` lives at the same level as `feed_screen.dart`, matching the five-tab nav.
**compose as center tab** — `compose_screen.dart` is a top-level feature folder. the upload_service in `shared/services/` handles tus.io resumable upload, referenced by the compose provider.
**naming consistency** — `*_screen.dart` for entry-point widgets (one per feature), `*_provider.dart` for state (using provider or riverpod — TBD), descriptive names for sub-widgets.
**onboarding is a feature** — not stuffed into `auth/` or `screens/`. it has its own provider and five child widgets matching the five-step flow from the spec.

View file

@ -0,0 +1,129 @@
# Flutter Folder Structure — GLM-4.7
**tests:** instruction-following, feature-first structure, domain accuracy
**context:** DESIGN.md excerpt (screens + navigation)
**version:** 1
---
```
lib/
├── main.dart
├── app.dart
├── features/
│ ├── onboarding/
│ │ ├── onboarding_screen.dart
│ │ ├── widgets/
│ │ │ ├── invite_gate_widget.dart
│ │ │ ├── email_entry_widget.dart
│ │ │ ├── magic_link_confirm_widget.dart
│ │ │ ├── social_handle_optin_widget.dart
│ │ │ └── follow_suggestions_widget.dart
│ │ └── onboarding_cubit.dart
│ ├── feed/
│ │ ├── feed_screen.dart
│ │ ├── widgets/
│ │ │ ├── post_card.dart
│ │ │ ├── photo_swiper.dart
│ │ │ ├── reaction_bar.dart
│ │ │ └── survey_card.dart
│ │ └── feed_cubit.dart
│ ├── posters/
│ │ ├── posters_screen.dart
│ │ ├── widgets/
│ │ │ ├── poster_card.dart
│ │ │ └── tag_filter.dart
│ │ └── posters_cubit.dart
│ ├── compose/
│ │ ├── compose_screen.dart
│ │ ├── widgets/
│ │ │ ├── photo_picker_widget.dart
│ │ │ ├── caption_field.dart
│ │ │ ├── post_type_toggle.dart
│ │ │ ├── tag_picker.dart
│ │ │ ├── event_form.dart
│ │ │ ├── saveable_toggle.dart
│ │ │ └── reaction_picker.dart
│ │ └── compose_cubit.dart
│ ├── stories/
│ │ ├── stories_screen.dart
│ │ ├── widgets/
│ │ │ ├── story_viewer.dart
│ │ │ ├── story_carousel.dart
│ │ │ ├── story_capture_widget.dart
│ │ │ └── filter_swipe_handler.dart
│ │ └── stories_cubit.dart
│ ├── profile/
│ │ ├── profile_screen.dart
│ │ ├── widgets/
│ │ │ ├── profile_header.dart
│ │ │ ├── social_handle_display.dart
│ │ │ ├── contact_prefs_display.dart
│ │ │ ├── posts_grid.dart
│ │ │ └── settings_access_button.dart
│ │ └── profile_cubit.dart
│ ├── settings/
│ │ ├── settings_screen.dart
│ │ ├── widgets/
│ │ │ ├── social_handle_setting.dart
│ │ │ ├── display_social_handle_preference.dart
│ │ │ └── contact_prefs_editor.dart
│ │ └── settings_cubit.dart
│ ├── feedback/
│ │ ├── feedback_screen.dart
│ │ ├── widgets/
│ │ │ ├── survey_widget.dart
│ │ │ └── free_text_field.dart
│ │ └── feedback_cubit.dart
│ └── navigation/
│ ├── main_tab_bar.dart
│ └── navigation_cubit.dart
├── services/
│ ├── auth_service.dart
│ ├── posts_service.dart
│ ├── upload_service.dart
│ ├── users_service.dart
│ └── tus_client.dart
├── shared/
│ ├── models/
│ │ ├── user.dart
│ │ ├── post.dart
│ │ ├── poster.dart
│ │ ├── story.dart
│ │ ├── reaction.dart
│ │ ├── comment.dart
│ │ └── survey.dart
│ ├── widgets/
│ │ ├── error_boundary.dart
│ │ ├── loading_indicator.dart
│ │ └── pull_to_refresh.dart
│ ├── utils/
│ │ ├── date_formatter.dart
│ │ ├── validators.dart
│ │ └── image_utils.dart
│ └── constants/
│ ├── app_colors.dart
│ ├── app_text_styles.dart
│ └── navigation_routes.dart
└── routing/
├── app_router.dart
└── route_handlers.dart
```
---
**notes:**
- feature-first structure as specified
- no notification-related files
- no chat/dm-related files
- compose modeled as a screen with post destination toggles
- stories has capture widget for camera/photo picker
- posters includes tag filter (UI only, implementation details TBD)
- feedback separates survey and free-text into distinct widgets
- social handles are opt-in, modeled in onboarding and settings
- all state management uses cubit pattern (consistent across features)
- tus_client for resumable uploads
- routing handles deep links for invite codes